Repository navigation
feat(channels): expose IM channel_user_id to sandbox commands as DEERFLOW_CHANNEL_USER_ID - #3926
Merged
WillemJiang merged 5 commits intoJul 4, 2026
Merged
Conversation
…W_CHANNEL_USER_ID IM-channel skills need the sender's platform identity (Feishu open_id, Slack Uxxx, ...). The channel manager already writes channel_user_id into body.context, but the Gateway whitelist dropped it. Forward it into the runtime context only (never configurable, which is checkpointed), and have bash_tool export it as a fixed env var through a shell-quoted command prefix. The identity deliberately does not ride execute_command(env=...): that channel is reserved for request-scoped secrets, and a non-empty env switches AioSandbox onto the bash.exec path (fresh session per call, image >= 1.9.3 required), which would have broken every IM bash command on older sandbox images and abandoned persistent-shell semantics on new ones. A command-string export keeps the legacy path, stays visible in audit logs (it is an identifier, not a secret), and gives per-call correctness in group chats where one thread and sandbox are shared by senders with different platform ids. Skipped on the Windows local sandbox, whose PowerShell/cmd.exe fallback has no POSIX export. Part of bytedance#3914
Review findings from the pre-PR verification pass: - Subagent delegation dropped the sender identity: task_tool now captures channel_user_id from the parent runtime context and the executor forwards it into the subagent's context, mirroring the guardrail attribution fields (user_role/oauth_*/run_id). Without this, bash commands delegated via task lost the group-chat sender's id. - body.context is client-writable on web requests, so values over 256 chars are ignored instead of bloating every command string sent to the sandbox.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end propagation of IM-channel sender identity (channel_user_id) from incoming IM runs into the runtime context and exposes it to sandbox bash commands via DEERFLOW_CHANNEL_USER_ID, including propagation through task delegation to subagents. This fits the DeerFlow harness/gateway boundary by keeping the identifier in runtime-only context (not checkpointed state) while enabling skills/scripts to read the sender identity inside the sandbox.
Changes:
- Gateway: forward
body.context.channel_user_idintoconfig["context"](runtime context only,setdefaultsemantics; not written toconfigurable). - Sandbox: prepend an identity export prefix for
DEERFLOW_CHANNEL_USER_IDto bash commands (with quoting + length cap + Windows-local-sandbox skip). - Subagents: capture
channel_user_idintask_tooland propagate it into the subagent’s runtime context; add unit tests covering the propagation chain.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/app/gateway/services.py | Forwards channel_user_id from body.context into runtime context only. |
| backend/packages/harness/deerflow/sandbox/tools.py | Exposes channel_user_id to bash commands via DEERFLOW_CHANNEL_USER_ID prefix logic. |
| backend/packages/harness/deerflow/tools/builtins/task_tool.py | Captures and forwards channel_user_id into subagent executor kwargs. |
| backend/packages/harness/deerflow/subagents/executor.py | Accepts channel_user_id and propagates it into subagent execution context. |
| backend/tests/test_channel_user_id_env.py | Adds unit tests for gateway passthrough + bash-tool identity exposure behavior. |
| backend/tests/test_task_tool_core_logic.py | Adds a test ensuring task_tool forwards channel_user_id to the executor. |
| backend/tests/test_subagent_executor.py | Adds a test ensuring executor propagates channel_user_id into subagent context. |
| backend/AGENTS.md | Documents the new propagation and exposure behavior and its trust model. |
willem-bd
reviewed
Jul 4, 2026
…egardless of AIO session persistence Review (willem-bd): the identity export could leak across senders in a shared group-chat AIO sandbox. The AIO no-env path reuses a persistent shell session (the class-lock reason, bytedance#1433), and the 256-char/type guard made some commands carry no prefix — so a dropped-id command could resolve the id a previous sender exported. Make per-call correctness independent of session semantics: an IM-channel command (channel_user_id present in context) now always carries an explicit prefix — export VAR=<quoted> for a valid id, or unset VAR for an unusable one (empty / non-str / over the cap). Non-IM runs (no key) are untouched. A prefix unset has none of the '& ; unset' suffix hazard raised earlier. Verified on a real AIO 1.11.0 container: the no-id shell path auto-creates a session per call (does not persist today), but an explicit shared session DOES persist (export stale-A -> readback [stale-A]); the unset prefix clears it (-> []). So the fix holds even on an image whose no-id path persists. Regression tests cover the dropped-id group-chat window and the non-IM passthrough. Part of bytedance#3914
…rn shape The merge from main changed task_tool to return a Command(update=...) instead of a plain string; update the assertion to extract the tool message via the existing _task_tool_message helper, matching the sibling tests. Fixes the CI backend-unit-tests failure introduced by the merge.
WillemJiang
approved these changes
Jul 4, 2026
marvin9551
pushed a commit
to marvin9551/deer-flow
that referenced
this pull request
Aug 21, 2026
…FLOW_CHANNEL_USER_ID (bytedance#3926) * feat(channels): expose channel_user_id to sandbox commands as DEERFLOW_CHANNEL_USER_ID IM-channel skills need the sender's platform identity (Feishu open_id, Slack Uxxx, ...). The channel manager already writes channel_user_id into body.context, but the Gateway whitelist dropped it. Forward it into the runtime context only (never configurable, which is checkpointed), and have bash_tool export it as a fixed env var through a shell-quoted command prefix. The identity deliberately does not ride execute_command(env=...): that channel is reserved for request-scoped secrets, and a non-empty env switches AioSandbox onto the bash.exec path (fresh session per call, image >= 1.9.3 required), which would have broken every IM bash command on older sandbox images and abandoned persistent-shell semantics on new ones. A command-string export keeps the legacy path, stays visible in audit logs (it is an identifier, not a secret), and gives per-call correctness in group chats where one thread and sandbox are shared by senders with different platform ids. Skipped on the Windows local sandbox, whose PowerShell/cmd.exe fallback has no POSIX export. Part of bytedance#3914 * feat(channels): propagate channel_user_id to subagents; cap value length Review findings from the pre-PR verification pass: - Subagent delegation dropped the sender identity: task_tool now captures channel_user_id from the parent runtime context and the executor forwards it into the subagent's context, mirroring the guardrail attribution fields (user_role/oauth_*/run_id). Without this, bash commands delegated via task lost the group-chat sender's id. - body.context is client-writable on web requests, so values over 256 chars are ignored instead of bloating every command string sent to the sandbox. * fix(channels): set-or-unset channel_user_id so identity is per-call regardless of AIO session persistence Review (willem-bd): the identity export could leak across senders in a shared group-chat AIO sandbox. The AIO no-env path reuses a persistent shell session (the class-lock reason, bytedance#1433), and the 256-char/type guard made some commands carry no prefix — so a dropped-id command could resolve the id a previous sender exported. Make per-call correctness independent of session semantics: an IM-channel command (channel_user_id present in context) now always carries an explicit prefix — export VAR=<quoted> for a valid id, or unset VAR for an unusable one (empty / non-str / over the cap). Non-IM runs (no key) are untouched. A prefix unset has none of the '& ; unset' suffix hazard raised earlier. Verified on a real AIO 1.11.0 container: the no-id shell path auto-creates a session per call (does not persist today), but an explicit shared session DOES persist (export stale-A -> readback [stale-A]); the unset prefix clears it (-> []). So the fix holds even on an image whose no-id path persists. Regression tests cover the dropped-id group-chat window and the non-IM passthrough. Part of bytedance#3914 * test(channels): align channel_user_id task test with new Command return shape The merge from main changed task_tool to return a Command(update=...) instead of a plain string; update the assertion to extract the tool message via the existing _task_tool_message helper, matching the sibling tests. Fixes the CI backend-unit-tests failure introduced by the merge.
jihtsan
pushed a commit
to jihtsan/dnx-deer-flow
that referenced
this pull request
Aug 29, 2026
…FLOW_CHANNEL_USER_ID (bytedance#3926) * feat(channels): expose channel_user_id to sandbox commands as DEERFLOW_CHANNEL_USER_ID IM-channel skills need the sender's platform identity (Feishu open_id, Slack Uxxx, ...). The channel manager already writes channel_user_id into body.context, but the Gateway whitelist dropped it. Forward it into the runtime context only (never configurable, which is checkpointed), and have bash_tool export it as a fixed env var through a shell-quoted command prefix. The identity deliberately does not ride execute_command(env=...): that channel is reserved for request-scoped secrets, and a non-empty env switches AioSandbox onto the bash.exec path (fresh session per call, image >= 1.9.3 required), which would have broken every IM bash command on older sandbox images and abandoned persistent-shell semantics on new ones. A command-string export keeps the legacy path, stays visible in audit logs (it is an identifier, not a secret), and gives per-call correctness in group chats where one thread and sandbox are shared by senders with different platform ids. Skipped on the Windows local sandbox, whose PowerShell/cmd.exe fallback has no POSIX export. Part of bytedance#3914 * feat(channels): propagate channel_user_id to subagents; cap value length Review findings from the pre-PR verification pass: - Subagent delegation dropped the sender identity: task_tool now captures channel_user_id from the parent runtime context and the executor forwards it into the subagent's context, mirroring the guardrail attribution fields (user_role/oauth_*/run_id). Without this, bash commands delegated via task lost the group-chat sender's id. - body.context is client-writable on web requests, so values over 256 chars are ignored instead of bloating every command string sent to the sandbox. * fix(channels): set-or-unset channel_user_id so identity is per-call regardless of AIO session persistence Review (willem-bd): the identity export could leak across senders in a shared group-chat AIO sandbox. The AIO no-env path reuses a persistent shell session (the class-lock reason, bytedance#1433), and the 256-char/type guard made some commands carry no prefix — so a dropped-id command could resolve the id a previous sender exported. Make per-call correctness independent of session semantics: an IM-channel command (channel_user_id present in context) now always carries an explicit prefix — export VAR=<quoted> for a valid id, or unset VAR for an unusable one (empty / non-str / over the cap). Non-IM runs (no key) are untouched. A prefix unset has none of the '& ; unset' suffix hazard raised earlier. Verified on a real AIO 1.11.0 container: the no-id shell path auto-creates a session per call (does not persist today), but an explicit shared session DOES persist (export stale-A -> readback [stale-A]); the unset prefix clears it (-> []). So the fix holds even on an image whose no-id path persists. Regression tests cover the dropped-id group-chat window and the non-IM passthrough. Part of bytedance#3914 * test(channels): align channel_user_id task test with new Command return shape The merge from main changed task_tool to return a Command(update=...) instead of a plain string; update the assertion to extract the tool message via the existing _task_tool_message helper, matching the sibling tests. Fixes the CI backend-unit-tests failure introduced by the merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Skills running for IM-channel users cannot see who is talking to them. The channel workers already write the sender's raw platform id (
channel_user_id: Feishuopen_id, SlackUxxx, ...) into the run'sbody.context, but the Gateway'smerge_run_context_overrideswhitelist drops it, and nothing exposes it to sandbox commands. Gap 3 of #3914.Solution
Two halves, following the design converged on in the issue thread:
merge_run_context_overridesforwardschannel_user_idinto the runtime context only, symmetric with the existinguser_idhandling:setdefault(a server-stamped value wins over the client-supplied one) and never intoconfigurable, which is checkpointed with the thread.bash_toolprepends a shell-quotedexport DEERFLOW_CHANNEL_USER_ID=<id>;to the command string.Why a command prefix and not
execute_command(env=...): theenv=channel is reserved for request-scoped secrets (#3861/#3871) and has a hard side effect onAioSandbox— any non-empty env switches execution to thebash.execAPI, which (a) 404s on sandbox images older than 1.9.3 (the #3921/#3922 capability gap: every IM bash command would have failed on the default image) and (b) abandons the persistent-shell session semantics. Anexportprefix keeps the legacy shell path byte-identical for non-IM runs, is fine to have visible in audit logs (an identifier, not a secret), and is per-call correct in group chats, where one thread/sandbox is shared by senders with different platform ids.Also from the pre-PR verification pass:
task_toolcaptures the dispatching turn'schannel_user_idand the subagent executor forwards it into the subagent's runtime context (mirroring the guardrail-attribution fields), so bash commands delegated viataskkeep the sender identity.body.contextis client-writable on web requests, so values over 256 chars are ignored rather than bloating every command string.os.name == "nt") because the local sandbox may fall back to PowerShell/cmd.exe, where POSIXexportis not valid syntax.Trust note (documented in AGENTS.md): the var is informational, never authorization-grade — any bash command can overwrite it and web clients can set
body.context.channel_user_id. Skills must not treat it as authenticated identity.Validation
Unit tests (12 new in
tests/test_channel_user_id_env.py+ 1 each intest_task_tool_core_logic.py/test_subagent_executor.py; all failed onmainbefore the implementation):Full backend suite: 5793 passed; the only 7 failures are pre-existing on clean
main(verified by reverting the changed files in-place and re-running: identical failures).Real-channel evidence — the actual
bash_tooldriven against a real Local sandbox and a real AIO container (image1.11.0):Independent adversarial verification (fresh reviewer agent, pre-PR) — all contract items passed:
env=: all identity calls hit the sandbox withenv=None; combined identity + active-skill-secrets kept secrets inenv(masked in output) and identity in the command string.x'; touch /tmp/pwned; '+ backticks +$(id)+ newline) against the real container: no canary file created,$(id)not executed, value round-tripped verbatim —shlex.quoteheld.channel_user_idnever entersconfigurable; non-IM commands are byte-for-byte unchanged.channels/manager.py→runs.stream/wait(context=...)→merge_run_context_overrides→worker._build_runtime_context→runtime.context→bash_tool.Not verified (fail-loud): real Windows host behavior (unit-simulated only via the
_is_windowsseam); a true platform-webhook-to-sandbox IM run (each link verified individually — code-traced up toruntime.context, live from there to the sandbox — but no real Feishu/Slack message was driven end to end); provisioner/K8s sandbox flavor (the change is sandbox-agnostic string prepending).Part of #3914 (gap 3). Gap 1 (autonomous skill-invocation secrets binding) stays open pending maintainer input on the binding-point question; gap 2 was withdrawn by the issue author.