agentscope-ai / agentscope-ai/QwenPaw
[Feature] Inject per-message context into shell tool subprocess env
- Langage dominant
- Python
- Étoiles
- 34.9k
- Forks
- 3.1k
- Merge moyen
- 1 j 15 h
- PR mergées (30 j)
- 225
Description
## Summary
When the LLM agent invokes the shell tool to run a skill script, the
subprocess inherits a static `os.environ.copy()` snapshot from the
QwenPaw worker process. Per-message context (Matrix sender, room id,
session id) reaches the LLM via `build_env_context(...)`
(system-prompt string), but **does not** reach the subprocess as env
— making it impossible for skill scripts to attribute the current
invocation to the originating user.
## Component(s) Affected
- [x] Core / Backend (`agents/tools/shell.py`)
- [x] Skills (downstream consumers of per-message attribution)
## Problem / Motivation
In multi-tenant scenarios where one Worker serves many users, audit
trails and access checks performed by skill scripts need a reliable
way to know "who triggered this invocation". The data is already in
the runtime payload:
- `copaw/src/matrix/channel.py:1571` constructs
`payload.meta = {room_id, sender_id, event_id, ...}`
- `qwenpaw/app/runner/runner.py:350` already calls
`build_env_context(session_id=..., user_id=..., channel=..., ...)`
But `qwenpaw/agents/tools/shell.py:327` does:
```python
env = os.environ.copy()
...
proc = await asyncio.create_subprocess_shell(cmd, ..., env=env, ...)
```
— a process-wide snapshot, not per-message. So the LLM sees the
user_id but the subprocess does not.
## Proposed Solution
Merge a per-call context dict on top of `os.environ.copy()` before
fork. The dict could be sourced from a contextvar populated by the
runner just before the shell tool is invoked (or any other mechanism
the maintainers prefer).
Suggested env names (project-prefixed, naming aligned with existing
runner fields):
- `QWENPAW_USER_ID` — user identity (always present once known)
- `QWENPAW_SESSION_ID` — session identity (always present)
- `QWENPAW_CHANNEL` — channel name (matches the existing `channel`
argument to `build_env_context`); always present
- `QWENPAW_ROOM_ID` — **optional**; set when available (Matrix and
similar room-based channels). Not all channels have this concept
- `QWENPAW_EVENT_ID` — **optional**; set when available (Matrix
event ID). Not all channels expose a stable event id
Skill scripts that don't care can ignore them; scripts needing
attribution can read them via `os.environ.get(...)`.
## Why not LLM-side splicing
A tempting alternative is asking the LLM to splice
`USER_ID=` into bash invocations from `build_env_context`'s
prompt text. We considered and rejected this for our project: audit
fields written by an LLM are prompt-injectable and unreliable; a fake
user_id is worse than absent attribution. Runtime-level injection is
the only design that audit infrastructure can actually trust.
## Use case
Downstream project (haopaw, multi-agent platform on HiClaw v1.1.0
GA): domain-service skills sign HMAC confirmation tokens and need the
originating Matrix user in their audit logs. Currently the skill
scripts fall back to `"unknown"` for actor headers — functional, but
loses traceability for compliance review.
## Acceptance test
Within a skill script:
- `os.environ.get("QWENPAW_USER_ID")` matches the message originator
(e.g. Matrix `event.sender`)
- Concurrent messages in different rooms must not see each other's
values
- For channels that do not expose `room_id` / `event_id`,
`QWENPAW_ROOM_ID` / `QWENPAW_EVENT_ID` should be **unset** (not
`"unknown"` or `""`). The runtime injection layer should never
manufacture placeholder values; downstream code is responsible for
whatever fallback policy makes sense for it.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.