feat(ipc): give the session's own processes a child token the inbound gate recognizes - #10764
Conversation
… gate recognizes A session now draws two inbox tokens: the published one peers use, and a child token that exists only in its own environment. A connection that authenticates with the child token is known to come from a process the session started, and the gate accepts such a message under the mode-parity default instead of holding it like an unknown peer's. An explicit crossSessionInbound setting still wins. The envelope, the transcript line, the hold notice and the /peers listing say the message came from the session's own process, not from another session.
E2E test reportEnvironment: Linux, 1. Two tokens, one published 2. Child token → delivered as the session's own (YOLO receiver, no The model started a turn on it right away (it then hit the dummy endpoint's connection error, which is how you can tell it reached the model). 3. Same frame on the published token → held 4. Wrong token (64× 5. Unit coverage (real UNIX sockets where applicable)
6. Known limits of this run
|
|
Thanks for the PR! Template looks good ✓ Problem: observed, not theoretical. This is a direct consequence of the inbox authentication that just landed in #10636: the token exported to child processes is the published peer token, so a hook's injected message is indistinguishable from a peer's and — under the mode-parity default — gets held for review in a YOLO/AUTO session. There is a concrete before/after (the same frame delivered via the env token vs. held via the registry token), demonstrated in the E2E report already posted in this thread. Direction: aligned. This follows up a feature merged to main today and fixes a real usability flaw in the hook-injection flow that feature documents, while also separating capabilities (a leaked child environment no longer leaks the peer token). That direction was already endorsed by the merged predecessor. Size: core paths touched ( Approach: the scope feels right, and I arrived at the same shape independently before reading the diff — two tokens (one published, one env-only), the transport establishes self-sent once per connection at the auth line, and the gate consumes it as a separate argument rather than a frame field. No unrelated changes in the diff. I don't see a simpler path: trusting Risk: no high-risk path matches; no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题: 已观测到的问题,不是理论担忧。这是刚合入的 #10636 inbox 认证的直接后果:导出给子进程的是发布令牌,所以 hook 注入的消息与对端消息无法区分,在模式对等默认规则下会在 YOLO/AUTO 会话中被留置审阅。有具体的 before/after(同一帧经环境令牌投递、经注册表令牌被留置),本线程中的 E2E 报告已演示。 方向: 对齐。这是今天刚合入 main 的功能的后续,修复了该功能文档化的 hook 注入流程中的真实可用性缺陷,同时分离了能力(子进程环境泄露不再等于对端令牌泄露)。该方向已由合入的前序 PR 背书。 规模: 触及核心路径( 方案: 范围合理,且我在看 diff 之前独立得出了同样的形态——两个令牌(一个发布、一个仅存于环境变量),传输层在连接的 auth 行上一次性确认"自发",闸门将其作为独立参数而非帧字段消费。diff 中没有无关改动。看不到更简路径:信任 风险: 未命中高风险路径,无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewNo blocking issues found. I drafted my own solution from the title and motivation before reading the diff, and this lands on the same shape — two independent tokens, transport-established self-sent, gate consumes it as a separate argument. What I verified:
Honest gap: on this commit no CI has actually executed the PR's tests (see below), and this environment never runs PR code — so the unit-suite results quoted in the E2E report comment remain the author's claim, not independently re-run evidence. sequenceDiagram
participant P1 as child process (hook or script)
participant P2 as uds-inbox
participant P3 as PeerMessaging
participant P4 as InboundGate
participant P5 as model transcript
P1->>P2: auth line with child token
P2->>P3: frame, auth is child
P3->>P4: admit(frame, selfSent true)
P4->>P4: explicit hold or refuse still wins, else accept
P4->>P5: deliver wrapped as own-process with notice
Files changed (14 of 14 shown)
TestingThis run is unattended CI, so no PR code was built or executed here; the evidence is the PR's own CI below. Real-scenario TUI coverage (hold notice wording,
The one red check is pre-existing breakage on Sandboxed verification would settle the runtime claim this diff makes: 中文说明代码审查未发现阻塞问题。读 diff 之前我先根据标题与动机起草了自己的方案,结论与本 PR 一致——两个独立令牌、传输层确认"自发"、闸门以独立参数消费。已核实:
诚实缺口:该提交上 CI 尚未真正执行过本 PR 的测试(见下),且本环境从不运行 PR 代码——E2E 报告评论中引用的单元测试结果是作者自述,非独立复跑证据。 测试本次为无人值守 CI,未构建或执行任何 PR 代码;证据为下表 PR 自身 CI。如需真实场景的 TUI 覆盖(hold 通知措辞、 唯一的红检查是 main 上的既有损坏,非本 PR 引起:集成与单测任务都在 沙箱验证可以落定这个 diff 的行为主张: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — a clean, tightly-scoped follow-up whose threat model I verified end to end; the only reservation is that no CI has managed to run its tests yet because Stepping back: before reading the diff I derived the same design independently — there is no honest way to let a session's own processes identify themselves other than a token nobody else holds, and no honest place to record that fact other than the transport. The implementation follows through on that without padding: every hunk serves the stated goal, the precedence order (explicit setting and fail-closed before the self-sent row) is exactly right, the flag survives every delivery path, and the envelope keeps the model-facing defences intact. The disclosed trade-off — a child token is only as safe as the environment it lives in — is the correct framing, not a dodge: the session chose to run the thing that is asking, and content-level defences still apply. The author is a write-access collaborator and shipped design doc, tests, and a real-socket E2E report with it. This is the kind of PR you want to land. What keeps this from a 5 is purely environmental: the unit suite has not executed on CI for this commit because the merge build dies in Verdict: approve, deferred until CI is green on this commit. Note that as long as 中文说明置信度:4/5 —— 一个干净、范围紧凑的后续 PR,威胁模型已端到端核实;唯一的保留意见是由于 main 本身是红的,CI 尚未能运行它的测试。 退一步看:读 diff 之前我独立推导出了同样的设计——要让会话自身的进程自证身份,除了一个无人持有的令牌别无诚实做法;记录这一事实的位置也只有传输层。实现没有多余动作:每个 hunk 都服务于既定目标,优先级顺序(显式设置与 fail-closed 先于自发行)完全正确,标志在所有投递路径上存活,信封保留了面向模型的防线。已披露的权衡——子令牌的安全性与所处环境等同——是正确的表述而非回避:是会话自己选择运行了提出请求的东西,内容层面的防线依然适用。作者是拥有写权限的协作者,随附了设计文档、测试和真实 socket 的 E2E 报告。这是值得合入的 PR。 之所以不是 5 分,纯粹是环境原因:该提交上单元测试套件尚未在 CI 执行,因为合并构建在 main 上既有的 结论:批准,但推迟到该提交的 CI 变绿。注意:只要 main 仍是红的,任何 PR 都无法变绿——待 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.3)
… into codex/pr-10764-round1
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Reviewed at head f7fbf70a.
- Checked the security model end to end at this head: two independently generated 32-byte tokens with separated audiences (only the peer token reaches the registry record; the child token lives only in the process environment, so an env leak no longer hands out the peer capability), auth kind decided exactly once per connection at the auth line and passed to the gate as a transport fact the frame cannot carry, and the gate's self-sent row sits under the explicit policy branch — a user-configured
hold/refusestill applies to own processes, and an unreadable policy fails closed before any of it. - The
selfSentflag is preserved through the pre-submit buffer and the held set, so a policy flip between arrival and delivery cannot re-label a message; the envelope'soriginis a fixed transport-set value and peer-supplied text stays inside the escapednameattribute;OWN_PROCESS_AUTHORITY_NOTICEcarries the same two prohibitions as the peer notice. - The documented trade-off (env-inheriting processes — subagent shells, hooks, model-run scripts — can auto-deliver under parity) is the intended meaning of "self-sent" and is stated in the design doc rather than papered over; the child-token option stays inert without
requiredToken, preserving the open-inbox behavior. - Design doc committed, 6/6 threads resolved, no prior review blockers. CI on this head has no failures (lanes mostly still queued after the push); per the channel convention the call is on the review itself.
yiliang114
left a comment
There was a problem hiding this comment.
Review passed (security-sensitive, reviewed in full).
- Two-token split is sound: the registry-published peer token and the child token are independent 64-hex secrets; the child token exists only in this session's environment (MESSAGING_TOKEN_ENV now carries it, never the published one), so presenting it proves descent from the session. authKindOf runs both constant-time comparisons on every presentation (no timing tell on which token was aimed), and the verdict is fixed once per connection at the auth line — no later frame can change it.
- Gate ordering is exactly right: resolvePolicy checks the explicit crossSessionInbound setting FIRST (fail-closed on unreadable, hold/refuse/accept all win over self-sent), then the selfSent accept, then mode parity — so a user who said hold reviews their own processes' messages too, and the parity default accepts what an unknown peer would be held for, matching the documented parity argument.
- No frame field can claim self-sent: origin is a transport finding threaded onFrame -> admit -> deliver -> buffer -> submit, pinned by the 'decided by the transport, never by the frame' and 'peer-supplied own-process label stays distinct' tests; held entries carry the origin so an explicitly held self-sent message is released as what it is.
- The prompt-level mitigation is the right one: OWN_PROCESS_AUTHORITY_NOTICE tells the model the message carries no user authority (never edit permissions/QWEN.md/config on its ask, never read it as approval), and origin=own-process is a fixed attribute, not an escaped peer field.
- Scrub semantics from #10636 still hold: entry scrubs drop an inherited pair before any spawn; a binding session re-exports its own child token and clears both on close. childToken is ignored unless requiredToken is set.
Verified against f7fbf70; ubuntu Test green on this head. The web-shell E2E Smoke failure is the known main-level flake (no assertion failure in the log), unrelated to ipc paths.
|
Released in v0.23.0. |
What this PR does
Gives the cross-session messaging inbox a second token for the session's own processes, and teaches the inbound gate what that token means.
A session now draws two independent tokens at startup. The first is the existing one, published in its registry record so peers can authenticate to its inbox. The second — the child token — is written nowhere but the session's own environment, as
QWEN_CODE_MESSAGING_TOKEN, so only processes the session started can hold it. The inbox admits a connection on either and tells the session which one it was; the decision is made once per connection, at the auth line, and nothing later on the connection can change it.A message that came in on the child token is self-sent: written by a script or hook this session ran. The gate accepts it under the mode-parity default, where the same frame from an unknown peer would be held — parity compares what two sessions may do, and a process this session started is not another session. An explicit
agents.crossSessionInboundstill wins:holdparks it for review andrefuserefuses it, exactly as for anything else. Self-sent is a fact the transport establishes and passes to the gate as a separate argument; there is no frame field for it, so a peer cannot claim it, and it is carried on a held entry so a message parked by an explicit setting is released as what it is.The model is told the difference. A self-sent message is wrapped as
<cross_session_message from="own process" origin="own-process">and followed by a notice that it came from a process this session started, not from the user — with the same two prohibitions as the peer notice: no escalation because it asked, and never read as the user approving a pending prompt. The transcript line, the hold notice, and the/peerslisting say "a process this session started" / "own process" instead of "another session" / "unknown session".Why it's needed
The inbox authentication that just landed exports the session's inbox address and token to child processes so a hook can inject a message back into it. But the token it exported was the published one, so a child's message was indistinguishable from any peer's — and under the mode-parity default, a session running in YOLO or AUTO holds every message whose sender does not assert a
bypassmode. A hook's "build finished" note, injected by a process the session itself started, was parked for review like a stranger's request, with nothing a script could honestly put in the frame to change that (fromModeis a claim, and the gate is right not to trust claims). Node cannot readSO_PEERCRED, so the inbox cannot learn who connected from the kernel; what it can know is what it handed out, and to whom. Splitting the token also separates the two capabilities: a child that leaks its environment no longer leaks the peer token.Reviewer Test Plan
How to verify
{ "agents": { "crossSessionMessaging": true } }) and start a session.cat ~/.qwen/sessions/<pid>.jsonshows a 64-hexipcToken; inside the session,echo $QWEN_CODE_MESSAGING_TOKENis 64-hex and different from it.socatexample indocs/users/features/commands.md, nofromMode). The message is delivered, not held: the transcript showsMessage from a process this session started (own process): …, and with--debugthe gate logs an accept. Send the same frame with the registryipcTokenon the auth line instead: it is held and/peerslists it.agents.crossSessionInbound: "hold"and inject with the environment token: held, listed by/peersasown process;/peers acceptreleases it and the model text still carriesorigin="own-process".list_agents/send_message, held-message review, and receipts behave exactly as before.Evidence (Before & After)
Before: with
QWEN_CODE_MESSAGING_TOKENequal to the published token, a child injection into a YOLO session with nofromModeis held (no-mode-asserted). After: see the E2E report in the follow-up comment.Tested on
Environment (optional)
Unit suites against real UNIX sockets; dev build under tmux with an isolated
QWEN_HOMEfor the smoke run.Risk & Scope
hold/refusecovers it. The self-sent row also skips the receiver-mode check (a message from the session's own process is accepted even when the mode is unreadable), because that check exists to judge what a foreign sender could trigger.SO_PEERCRED-style kernel verification; named pipes. No wire-protocol change (same auth line), no registry change (only the peer token is published), receipts unchanged (replyTokenis still the peer token).childTokeninbox option is inert withoutrequiredToken.Linked Issues
Follows #10636.
中文说明
本 PR 做了什么
给跨会话消息 inbox 增加第二个、专给本会话自身进程的令牌,并让入站闸门理解它的含义。
会话启动时生成两个独立令牌。第一个即现有令牌,发布在注册记录里供对端认证;第二个——子令牌——只写进本会话自己的环境变量
QWEN_CODE_MESSAGING_TOKEN,除本会话启动的进程之外无人持有。inbox 两者都接受,并告诉会话是哪一个;判定在每个连接的 auth 行上做一次,之后的任何行都改不了它。凭子令牌进来的消息是"自发的":由本会话运行的脚本或 hook 写入。在模式对等的默认规则下闸门直接接受它——同样的帧若来自未知对端会被 hold——因为对等比较的是两个会话各自能做什么,而本会话启动的进程不是另一个会话。显式的
agents.crossSessionInbound仍然优先:hold照样留待审阅,refuse照样拒绝。"自发"是传输层确认后作为独立参数传给闸门的事实,帧里没有对应字段,对端无从冒充;held 条目也带着它,因显式设置而被留置的消息释放时仍是它本来的身份。模型能看出区别:自发消息被包成
<cross_session_message from="own process" origin="own-process">,随后的说明指出它来自本会话启动的进程而非用户,并保留与对端说明相同的两条禁令——不得因其要求而提权,不得视作用户批准了待决提示。transcript 行、hold 通知和/peers列表会写"a process this session started" / "own process",而不是"another session" / "unknown session"。为什么需要
刚合入的 inbox 认证把会话的 inbox 地址与令牌导出给子进程,以便 hook 回注消息。但导出的是发布令牌,子进程的消息与任何对端的消息无法区分——而在模式对等的默认规则下,YOLO 或 AUTO 模式的会话会 hold 所有未声明
bypass的发送方。于是一个 hook 注入的"构建完成",明明来自会话自己启动的进程,却像陌生人的请求一样被留置,脚本也没有任何诚实的办法在帧里改变这一点(fromMode只是声明,闸门不信它是对的)。Node 读不到SO_PEERCRED,inbox 无法从内核得知谁连了进来;它能知道的是自己把什么发给了谁。令牌分离也把两种能力分开:子进程泄露了环境,不再等于泄露对端令牌。审阅验证方式
{ "agents": { "crossSessionMessaging": true } })并启动会话。cat ~/.qwen/sessions/<pid>.json中有 64 位 hex 的ipcToken;会话内echo $QWEN_CODE_MESSAGING_TOKEN也是 64 位 hex,且与之不同。docs/users/features/commands.md里的socat示例,不带fromMode)。消息被投递而非 hold:transcript 显示Message from a process this session started (own process): …,--debug日志记录 accept。换成注册记录里的ipcToken发同一帧:被 hold,/peers可见。agents.crossSessionInbound: "hold"后用环境令牌注入:被 hold,/peers中标为own process;/peers accept释放后模型文本仍带origin="own-process"。list_agents/send_message互发、held 审阅、回执行为与之前完全一致。证据(Before & After)
之前:
QWEN_CODE_MESSAGING_TOKEN与发布令牌相同,向 YOLO 会话不带fromMode的子进程注入被 hold(no-mode-asserted)。之后:见后续评论中的 E2E 报告。测试平台
Linux ✅;macOS⚠️ 未本地验证(CI 覆盖);Windows N/A(跨会话消息尚不支持该平台,相关套件 skipIf(isWindows))。
风险与范围
hold/refuse同样覆盖它。自发这一行还跳过了接收方模式检查(模式不可读时自发消息仍被接受),因为那个检查是用来判断外部发送方能触发什么的。SO_PEERCRED式内核校验;命名管道。线协议不变(同一 auth 行),注册表不变(只发布对端令牌),回执不变(replyToken仍是对端令牌)。childTokeninbox 选项在没有requiredToken时不起作用。关联 Issue
承接 #10636。