ybh1291747665/dsh-peer-bus ↗★ 1

dsh-peer-bus

Cross-session message bus for DeepSeek Harness: let independent sessions address and wake each other. 适合需要构建多Agent协同、实现会话间自动化通信的高级用户。

패키지
dsh-peer-bus
호환성
미검증
Harness peer 범위
>=0.1.7-rc.2 <0.3.0-0
Cordis peer 범위
~4.0.4
버전
0.2.0
라이선스
MIT
최근 업데이트
2026. 10. 2.

설치

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ybh1291747665/dsh-peer-bus

Configuration

KeyDefaultMeaning
allow[]Permission allowlist. Default deny — an empty list permits nothing.
maxMessageBytes16384Maximum UTF-8 bytes in one message body.
maxSendsPerWindow10Send ceiling per sender→target pair per window. A send that fails to deliver does not count.
rateWindowMs60000Length of that window in milliseconds.
rosterScope'allowed'What bus_roster shows: 'allowed' = the caller plus sessions it may message; 'all' = every session, flagged allowed/not-allowed.
waitTimeoutMs60000bus_wait timeout when the caller passes none, and bus_ask's when the target is idle.
maxWaitMs600000Upper bound on any bus_wait or bus_ask timeout, so one call cannot hold a turn open indefinitely.
askBusyTimeoutMs30000How long bus_ask waits when the target was already running, before it returns pending.
resumedIdleMs600000Release a session the bus cold-resumed itself once it has been idle this long, freeing its log lock. 0 keeps such sessions loaded. Sessions resumed through the host's own lookup are host-owned and unaffected.
crossProcessfalseReach sessions held live by another DSH process over a local socket. Off by default on purpose — see Cross-process.
crossProcessTimeoutMs2000Timeout for one remote control-plane query (roster merge, bus_status, ask cancellation). Short on purpose: a wedged peer must degrade to "looks stored", not stall a turn.
crossProcessDeliverTimeoutMs60000Timeout for one forwarded delivery, which may have to cold-resume the target on the far side.
crossProcessRosterCacheMs3000How long this process may reuse the last answer to "which peer holds what". Every roster asks every peer, so a burst of sends would otherwise cost a query per send per peer. A stale answer is safe: the peer that no longer holds the session says so and the sender re-resolves. 0 asks every time.

Keys you leave out of a profile patch fall back to these defaults.

Cross-process

Off by default. Turning it on widens the trust boundary from "this process" to "every DSH process of this OS user over this DSH_HOME", which is why it has to be a deliberate act rather than something that quietly starts working.

What that boundary is, exactly:

PropertyHow it holds
Which processes can see each otherOnly those sharing one DSH_HOME. Two homes on one machine are two separate worlds.
Who can connectThe endpoint directory is 0700 and every file in it 0600, so another OS user cannot enumerate peers or read a token. When a long DSH_HOME forces the socket into the shared temp directory, that directory is verified rather than trusted — a real directory, not a symlink, owned by this user, mode 0700 — because on Linux /tmp can be pre-created by anyone.
Who can talkA handshake token, read from the peer's own 0600 file. On Windows this is load-bearing rather than defence in depth: a named pipe has no filesystem permission model of its own.
What a peer may doNothing it could not do locally. The receiving process re-runs its own allowlist, archive check, and rate ceiling; a sender cannot talk its way past them.
Whether it leaves the machineNo. A Unix domain socket or a Windows named pipe, both local. There is no network listener and no port.

Who decides permission across processes. The receiving process, always. A grant means "this session may message me", and it is made with /bus allow inside the process that holds the session being messaged — so the sender's process cannot see it and does not try to. For a target held elsewhere the sender forwards and lets that process apply its own allowlist, archive state, and rate ceiling; for a target held locally it checks as before, because there the receiver and the sender are the same allowlist. This is what makes a cross-process pair work with nothing but the receiver's consent — needing the rule written into both processes' config would mean a consent that was actually given had no effect.

Two consequences worth stating plainly. First, a peer that lies about itself is inside the trust boundary already — the receiver prefers its own view of the sender's session when it has one, and falls back to the sender's claim only when it cannot see that session at all. Second, enabling the transport does not grant anything: with an empty allow list, two processes that can see each other still cannot message each other.

Two allowlist rule shapes

allow:
  # 1. id patterns — exact, trailing-* prefix, or '*' for any
  - from: 'session-abc'
    to: 'session-worker-*'

  # 2. same workspace — any two root sessions sharing a workspace
  - sameWorkspace: true

  # ...narrowed to one workspace
  - sameWorkspace: true
    cwd: '/Users/you/project'

  # ...and letting subagent children in too (off by default)
  - sameWorkspace: true
    includeSubagents: true

Id patterns are not globs: * is only meaningful at the end.

The workspace rule exists because a session id is a fresh UUID — you cannot write an id rule for a session that does not exist yet, which makes "let me test two sessions talking" need a config edit and a restart after the fact. It is also narrower than '*': a session in an unrelated project does not match.

Both sides need a recorded workspace for a workspace rule to match. Two sessions whose cwd DSH did not record are not "in the same workspace", they are simply unlocated, and the rule refuses them rather than silently authorizing every such session.

A workspace rule also refuses a subagent child on either side unless it sets includeSubagents: true. dsh-subagent gives every child its parent's cwd, so without this exclusion a subagent reading an untrusted file or web page could instruct — and cold-resume — every root session in the project. A child is recognised by its header (origin: 'subagent' or a positive delegationDepth); a user-initiated fork is a peer and still matches. Id rules are explicit and unaffected.

Why default-deny

A bus message is delivered as a user message, and a model treats user messages as instructions. A permissive bus is therefore a prompt-injection path: any session — including a subagent you spawned to read an untrusted file — could instruct your main session. Default-deny with an explicit allowlist keeps that a deliberate choice, and the workspace rule's subagent exclusion keeps the convenient rule from reopening that path.

Changing the allowlist at runtime: /bus

The config file is the baseline, and it cannot be written ahead of time — so /bus edits the allowlist from inside a session, with no restart and no GUI:

/bus id                    show this session's id
/bus list                  who may message this session, and why
/bus allow        let  message this session
/bus revoke       take that back

`` is a full id or an unambiguous id prefix, resolved the same way bus_send resolves a target — a typo fails loudly instead of persisting a grant that can never match.

The grant is receiver consent. /bus allow run in session A means that peer may message A. Each side's user decides who may instruct it, so a two-way conversation takes one grant on each side. The alternative — one grant opening both directions — would let A's user decide, on B's behalf, who may instruct B.

Runtime grants are additive and revocable, and they persist: they are written to a peer_bus_allowlist storage domain, so they survive a restart. /bus revoke can only take back a grant made this way — it cannot subtract from the config, and it says so rather than claiming a success it did not achieve:

$ /bus revoke session-abc
Runtime grant revoked, but the config allowlist still permits session-abc to message this session.

No model tool can do this. /bus is registered on the human command registry only; there is deliberately no bus_allow tool, so an agent cannot widen its own permissions. A bus message whose body starts with /bus allow … is delivered as text and is never interpreted — the e2e asserts that the command registry is not invoked at all during a delivery.

A profile that mounts no command registry, or that cannot resolve zod (which the storage domain layer validates with), still has a working bus: the command is skipped and runtime grants stay in memory for the life of the process, with a warning that says which of the two happened.

The per-pair rate ceiling is the second guard: two agents that both auto-reply quickly would otherwise loop forever, spending tokens on every hop. It bounds the rate, not the length, of a conversation.