feat(ipc): authenticate cross-session inbox connections with per-session tokens - #16
feat(ipc): authenticate cross-session inbox connections with per-session tokens#16qqqys wants to merge 1 commit into
Conversation
…ion tokens Each peer-messaging inbox now generates a random token, published in the session's 0600 registry record beside the socket address, and requires it on the first line of every connection before any frame is read. This narrows "can reach the socket path" to "can read this session's registry record", is the foundation a permissionless transport (Windows named pipes) needs, and lets a session export QWEN_CODE_MESSAGING_SOCKET/_TOKEN so its own child processes can inject messages back through the same inbound gate. User frames carry a replyToken so delivery receipts authenticate on the way back. Receipts, discovery, gate policy and the misaddressed path are unchanged for same-build sessions; a pre-token sender's frames to a new inbox are dropped (documented experimental break), while a new sender still reaches a pre-token inbox. qwen sessions ps --json strips the token — it is a credential, not data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5JUqFcorwYzUSpH7nxqoT
E2E test reportEnvironment: Linux, 1. Registry record publishes the credential (mode 600) 2. Wrong token → silent drop, no delivery, no receipt The injector connected, sent an auth line with a 64-hex token of all 3. Correct token → frame admitted, gate policy applied, receipt authenticated back Receiver was in auto mode and the injected frame asserted no mode class, so the mode-parity default holds it — and the 4. Unit coverage (all against real UNIX sockets where applicable)
5. Known limits of this run
🤖 Generated with Claude Code |
|
Superseded by the upstream PR: QwenLM#10636 |
What this PR does
Adds connection-level authentication to the experimental cross-session messaging inbox. Every session's inbox now generates a random per-session token and publishes it in the session's registry record (0600, owner-only) beside the socket address it already advertises. A connection to the inbox must present that token on its first line before any message is read; a connection that fails to authenticate is dropped immediately and permanently, with nothing after the failed line ever parsed. Messages a session sends now carry a reply token of their own, so the delivery receipts flowing back (held / delivered / denied / expired / misaddressed) authenticate in the reverse direction through the same mechanism.
The session also exports its inbox address and token to child processes as
QWEN_CODE_MESSAGING_SOCKETandQWEN_CODE_MESSAGING_TOKEN, so a script or hook the session runs can inject a message back into it. An injected message goes through the same inbound gate as one from another session — it is marked as not coming from the user, and theagents.crossSessionInboundpolicy (or the mode-parity default) decides whether it is delivered or held for review. The user docs gain a short section with a working injection example.qwen sessions ps --jsonstrips the token from its output: it is a credential, not data — tooling that genuinely needs it can read the record file, but it must not spill into logs and pipelines by default.Why it's needed
Today the inbox's entire access-control story is filesystem permissions (0700 directory, 0600 socket). That holds on POSIX, but it cannot carry over to a transport without those semantics — native named-pipe support needs connection-level authentication as its foundation — and the socket path itself is guessable (keyed by PID), while the registry record is not readable without owner access. Requiring a token that lives in the record narrows "can reach the socket path" to "can read this session's registry record", which is also the same capability discovery already requires, so senders get address and credential in one read with no extra round trip. It is also what makes officially opening the inbox to the session's own child processes safe: without authentication, exporting the socket address alone would have widened the unauthenticated surface.
Reviewer Test Plan
How to verify
settings.json({ "agents": { "crossSessionMessaging": true } }) and start two interactive sessions. Messaging between them vialist_agents/send_message, held-message review via/peers, and the receipt lines in the sender's transcript should all behave exactly as before — same-build sessions are unaffected.cat ~/.qwen/sessions/<pid>.jsonfor a running session: it should containipcPathplus a 64-hexipcToken, with file mode 600.socator a small node script): nothing should be delivered or held, and with--debugthe log names the unauthenticated drop.{"msgV":1,"type":"auth","token":"'$QWEN_CODE_MESSAGING_TOKEN'"}, second line a normal user frame, written to$QWEN_CODE_MESSAGING_SOCKET. The message should arrive through the inbound gate (delivered, or held and visible in/peers, depending on the mode).qwen sessions ps --jsonshould not containipcToken.Evidence (Before & After)
Verified against a live dev-build session (Linux): the registry record carried
ipcPath+ a 64-hexipcTokenat mode 600; an injection with a wrong token produced no delivery and no receipt; an injection with the correct token (receiver in auto mode, sender asserting no mode class) came back with aheldreceipt to the injector's own socket, matching the existing mode-parity policy. Full log in the follow-up comment.Tested on
Environment (optional)
npm run devunder tmux with an isolatedQWEN_HOME; unit suites run against real UNIX sockets.Risk & Scope
misaddressedreceipt — the window is milliseconds because every send re-reads the registry, and same-process session swaps (/clear,/resume) still producemisaddressedas before.fromstays reply-routing-only, and the inbound gate plus envelope remain the authority model. Kernel-level peer credentials and named-pipe transport are follow-ups.ipcTokenis an optional field older readers ignore.Linked Issues
None.
中文说明
本 PR 做了什么
为实验性的跨会话消息 inbox 增加连接级认证。每个会话的 inbox 生成一个随机的会话级令牌,与已发布的 socket 地址一起写进该会话的注册记录(0600,仅属主可读)。连接 inbox 必须在首行出示该令牌,之后消息才会被读取;认证失败的连接立即被断开且永久拒绝,失败行之后的内容一概不解析。会话发出的消息带有自己的回复令牌,使回程的投递回执(held / delivered / denied / expired / misaddressed)通过同一机制反向认证。
会话还会把自己的 inbox 地址与令牌以
QWEN_CODE_MESSAGING_SOCKET和QWEN_CODE_MESSAGING_TOKEN环境变量导出给子进程,会话运行的脚本或 hook 因此可以向本会话回注消息。注入的消息与来自其他会话的消息走同一入站闸门——被标记为并非来自用户,由agents.crossSessionInbound策略(或模式对等的默认规则)决定投递还是留待审阅。用户文档新增了带可用注入示例的小节。qwen sessions ps --json会从输出中剥离令牌:它是凭据而非数据——确实需要它的工具可以直接读记录文件,但它不应默认流入日志与管道。为什么需要
目前 inbox 的全部访问控制是文件权限(目录 0700、socket 0600)。这在 POSIX 上成立,但无法迁移到没有这类语义的传输上——原生命名管道支持需要以连接级认证为基础——而且 socket 路径本身按 PID 可猜测,注册记录则没有属主权限读不到。要求出示存放在记录里的令牌,把"能连上 socket 路径"收紧为"能读到该会话的注册记录",而这恰好也是发现机制本就要求的能力,发送方一次读取同时获得地址与凭据,没有额外往返。这也是安全地向会话自身子进程开放 inbox 的前提:没有认证,仅导出 socket 地址就会扩大未认证暴露面。
审阅验证方式
settings.json开启实验特性({ "agents": { "crossSessionMessaging": true } })并启动两个交互式会话。经list_agents/send_message互发、/peers审阅 held 消息、发送方 transcript 中的回执行为应与之前完全一致——同版本会话不受影响。cat ~/.qwen/sessions/<pid>.json:应包含ipcPath与 64 位 hex 的ipcToken,文件权限 600。--debug日志会记录未认证丢弃。{"msgV":1,"type":"auth","token":"'$QWEN_CODE_MESSAGING_TOKEN'"},次行普通用户帧,写入$QWEN_CODE_MESSAGING_SOCKET。消息应经入站闸门到达(按模式投递或 held 并在/peers可见)。qwen sessions ps --json输出不应包含ipcToken。证据(Before & After)
已在 Linux 上对 dev 构建的真实会话验证:注册记录携带
ipcPath与 64 位 hexipcToken(权限 600);错误令牌注入无投递无回执;正确令牌注入(接收方 auto 模式、发送方未声明模式类)收到送达注入方自建 socket 的held回执,与既有模式对等策略一致。完整日志见后续评论。测试平台
Linux ✅;macOS⚠️ 未本地验证(CI 覆盖);Windows N/A(跨会话消息尚不支持该平台,相关测试套件 skipIf(isWindows))。
风险与范围
misaddressed回执——每次发送都会现读注册表,窗口只有毫秒级,同进程内的会话切换(/clear、/resume)仍照旧产生misaddressed。from仍仅用于回复路由,入站闸门与信封仍是权限模型的主体。内核级对端凭证与命名管道传输属后续工作。ipcToken为可选字段,旧读者忽略。关联 Issue
无。
🤖 Generated with Claude Code
https://claude.ai/code/session_01B5JUqFcorwYzUSpH7nxqoT