Skip to content

feat(ipc): authenticate cross-session inbox connections with per-session tokens - #16

Closed
qqqys wants to merge 1 commit into
mainfrom
feat/peer-messaging-inbox-auth
Closed

feat(ipc): authenticate cross-session inbox connections with per-session tokens#16
qqqys wants to merge 1 commit into
mainfrom
feat/peer-messaging-inbox-auth

Conversation

@qqqys

@qqqys qqqys commented Aug 31, 2026

Copy link
Copy Markdown
Owner

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_SOCKET and QWEN_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 the agents.crossSessionInbound policy (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 --json strips 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

  1. Enable the experimental feature in settings.json ({ "agents": { "crossSessionMessaging": true } }) and start two interactive sessions. Messaging between them via list_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.
  2. cat ~/.qwen/sessions/<pid>.json for a running session: it should contain ipcPath plus a 64-hex ipcToken, with file mode 600.
  3. Write a raw user frame to the socket without an auth line (e.g. with socat or a small node script): nothing should be delivered or held, and with --debug the log names the unauthenticated drop.
  4. From a shell inside a session, inject through the exported environment: first line {"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).
  5. qwen sessions ps --json should not contain ipcToken.

Evidence (Before & After)

Verified against a live dev-build session (Linux): the registry record carried ipcPath + a 64-hex ipcToken at 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 a held receipt to the injector's own socket, matching the existing mode-parity policy. Full log in the follow-up comment.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows N/A
🐧 Linux

Environment (optional)

npm run dev under tmux with an isolated QWEN_HOME; unit suites run against real UNIX sockets.

Risk & Scope

  • Main risk or tradeoff: mixed-build machines during the experimental phase. A pre-token sender's frames to a token-requiring inbox are dropped (documented break; the feature is off by default). The reverse direction still works: the auth line is shaped so a pre-token inbox skips it as an unparseable line and reads the frames after it. Additionally, when a PID is recycled and the address changes hands, a sender holding the stale record now gets a silent drop instead of a misaddressed receipt — the window is milliseconds because every send re-reads the registry, and same-process session swaps (/clear, /resume) still produce misaddressed as before.
  • Not validated / out of scope: sender identity remains unauthenticated — the token authenticates the connection to an inbox, not who wrote the frame; from stays reply-routing-only, and the inbound gate plus envelope remain the authority model. Kernel-level peer credentials and named-pipe transport are follow-ups.
  • Breaking changes / migration notes: none for users (experimental flag, off by default). No registry schema bump — ipcToken is an optional field older readers ignore.

Linked Issues

None.

中文说明

本 PR 做了什么

为实验性的跨会话消息 inbox 增加连接级认证。每个会话的 inbox 生成一个随机的会话级令牌,与已发布的 socket 地址一起写进该会话的注册记录(0600,仅属主可读)。连接 inbox 必须在首行出示该令牌,之后消息才会被读取;认证失败的连接立即被断开且永久拒绝,失败行之后的内容一概不解析。会话发出的消息带有自己的回复令牌,使回程的投递回执(held / delivered / denied / expired / misaddressed)通过同一机制反向认证。

会话还会把自己的 inbox 地址与令牌以 QWEN_CODE_MESSAGING_SOCKETQWEN_CODE_MESSAGING_TOKEN 环境变量导出给子进程,会话运行的脚本或 hook 因此可以向本会话回注消息。注入的消息与来自其他会话的消息走同一入站闸门——被标记为并非来自用户,由 agents.crossSessionInbound 策略(或模式对等的默认规则)决定投递还是留待审阅。用户文档新增了带可用注入示例的小节。

qwen sessions ps --json 会从输出中剥离令牌:它是凭据而非数据——确实需要它的工具可以直接读记录文件,但它不应默认流入日志与管道。

为什么需要

目前 inbox 的全部访问控制是文件权限(目录 0700、socket 0600)。这在 POSIX 上成立,但无法迁移到没有这类语义的传输上——原生命名管道支持需要以连接级认证为基础——而且 socket 路径本身按 PID 可猜测,注册记录则没有属主权限读不到。要求出示存放在记录里的令牌,把"能连上 socket 路径"收紧为"能读到该会话的注册记录",而这恰好也是发现机制本就要求的能力,发送方一次读取同时获得地址与凭据,没有额外往返。这也是安全地向会话自身子进程开放 inbox 的前提:没有认证,仅导出 socket 地址就会扩大未认证暴露面。

审阅验证方式

  1. settings.json 开启实验特性({ "agents": { "crossSessionMessaging": true } })并启动两个交互式会话。经 list_agents / send_message 互发、/peers 审阅 held 消息、发送方 transcript 中的回执行为应与之前完全一致——同版本会话不受影响。
  2. cat ~/.qwen/sessions/<pid>.json:应包含 ipcPath 与 64 位 hex 的 ipcToken,文件权限 600。
  3. 不带 auth 行直接向 socket 写入原始用户帧:不应有任何投递或 held,--debug 日志会记录未认证丢弃。
  4. 在会话内的 shell 中通过导出的环境变量注入:首行 {"msgV":1,"type":"auth","token":"'$QWEN_CODE_MESSAGING_TOKEN'"},次行普通用户帧,写入 $QWEN_CODE_MESSAGING_SOCKET。消息应经入站闸门到达(按模式投递或 held 并在 /peers 可见)。
  5. qwen sessions ps --json 输出不应包含 ipcToken

证据(Before & After)

已在 Linux 上对 dev 构建的真实会话验证:注册记录携带 ipcPath 与 64 位 hex ipcToken(权限 600);错误令牌注入无投递无回执;正确令牌注入(接收方 auto 模式、发送方未声明模式类)收到送达注入方自建 socket 的 held 回执,与既有模式对等策略一致。完整日志见后续评论。

测试平台

Linux ✅;macOS ⚠️ 未本地验证(CI 覆盖);Windows N/A(跨会话消息尚不支持该平台,相关测试套件 skipIf(isWindows))。

风险与范围

  • 主要风险/权衡:实验期内同机混用新旧版本。旧发送端发往需要令牌的 inbox 会被丢弃(已文档化的破坏;特性默认关闭)。反方向仍然可用:auth 行的形状使旧 inbox 将其当作无法解析的行跳过并继续读后续帧。另外,PID 复用使地址换主时,持有陈旧记录的发送方现在得到静默丢弃而非 misaddressed 回执——每次发送都会现读注册表,窗口只有毫秒级,同进程内的会话切换(/clear/resume)仍照旧产生 misaddressed
  • 未验证/范围外:发送方身份仍未认证——令牌认证的是到 inbox 的连接,不是帧的作者;from 仍仅用于回复路由,入站闸门与信封仍是权限模型的主体。内核级对端凭证与命名管道传输属后续工作。
  • 破坏性变更/迁移说明:对用户无(实验开关,默认关闭)。注册表 schema 未升版——ipcToken 为可选字段,旧读者忽略。

关联 Issue

无。

🤖 Generated with Claude Code

https://claude.ai/code/session_01B5JUqFcorwYzUSpH7nxqoT

…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
@qqqys

qqqys commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

E2E test report

Environment: Linux, npm run dev (tsx, dev build of this branch) under tmux with an isolated QWEN_HOME; no model credentials (the TUI sat at the provider-connect dialog, which does not block inbox startup); injection driven by a small node script (no socat on the host).

1. Registry record publishes the credential (mode 600)

$ cat $QWEN_HOME/sessions/43157.json   # token truncated
{'schemaVersion': 1, 'pid': 43157, ..., 'ipcPath': '/run/user/1001/qwen-socks/43157.sock', 'ipcToken': '3a4b1f181aacd068...'}
token-len 64
$ stat -c '%a' $QWEN_HOME/sessions/43157.json
600

2. Wrong token → silent drop, no delivery, no receipt

The injector connected, sent an auth line with a 64-hex token of all f, then a valid user frame, and listened on its own socket for receipts:

bad receipts: (none)

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 held receipt arrived at the injector's own listener:

good receipts: {"msgV":1,"msgId":"00b0ce88-...","type":"control","action":"delivery_status","status":"held","origMsgId":"smoke-good-1788179926641","from":"/run/user/1001/qwen-socks/43157.sock","reason":"Your message is held for the recipient user to review before it reaches their Qwen Code session."}

4. Unit coverage (all against real UNIX sockets where applicable)

  • packages/coresrc/ipc/ full suite + config.test.ts + list-agents.test.ts + send-message.test.ts: 919 passed; session-registry.test.ts: 118 passed (includes new token round-trip).
  • packages/clipeerMessaging/peer-messaging.test.ts: 38 passed (includes unauthenticated-drop, token publication + env export/cleanup, and authenticated-receipt cases); commands/sessions/ps.test.ts: 17 passed (includes token stripped from --json); ui/startInteractiveUI.test.tsx: 11 passed.
  • New negative case worth noting: a connection whose first line is a frame is refused terminally — a valid auth line arriving later on the same connection cannot resurrect it (this started as a red test and drove a fix during review).

5. Known limits of this run

  • macOS not run locally (CI covers it); Windows N/A — the feature is not available there and the suites are skipIf(isWindows).
  • Graceful-exit cleanup (socket unlink + record clear) is covered by unit tests; the tmux hard-kill at the end of the smoke run leaves the record behind by design, to be reaped by the next enumeration's liveness sweep (pre-existing behavior).

🤖 Generated with Claude Code

https://claude.ai/code/session_01B5JUqFcorwYzUSpH7nxqoT

@qqqys

qqqys commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the upstream PR: QwenLM#10636

@qqqys qqqys closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant