dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Session-aware mesh relay for DSH agents - Ed25519-signed receive route, delivery policy, outbound and registry libraries
Session-aware mesh relay for DSH agents.
Standard agent-to-agent messaging is request/response — fine for one-shot jobs, inadequate for
conversational coordination. dsh-fleet-mesh adds session-preserving communication: when
one agent sends to another, the recipient knows who asked, what they are responding to, and
whether an answer is wanted. An inbound envelope is not a notification; it is a prompt.
One plugin serves the whole substrate, not one agent: agents are configured in a single
agents: map, and one port dispatches by the envelope's [to:] token. A second DSH agent
needs no second plugin and no second listener.
Sender: mesh_send(agent="ada", message="Review this plan", action="do", reply="yes")
│
├─ 1. Resolves Ada's identity from the peer directory
├─ 2. Pads [mesh][v:1][from:lily][to:ada][id:uuid][action:do][reply:yes]
├─ 3. Ed25519-signs "{timestamp}\n{body}" with the sender's own private key
└─ 4. POSTs to Ada's mesh endpoint, inside one shared 10 s delivery budget
Both halves are complete and tested — 170 assertions across four suites.
Receiving: signature verification, the delivery matrix, the wake list, the replay window, per-envelope dedupe, and the runaway breaker.
Sending: the five tools below, built on the same tested libraries — envelope signing, the delivery retry budget, the peer directory, and a registry client.
Named exactly as hermes-mesh names hers, so muscle memory transfers between substrates.
| tool | purpose |
|---|---|
mesh_list | list peers from the local identity store — read-only, no network |
mesh_send | sign and deliver one envelope; reports delivered or a failure, never queues silently |
mesh_sync | fetch peer identities from the registry into this deployment's own cache |
mesh_register | publish this agent's row — name, receive URL and public key — to the registry |
mesh_deregister | withdraw this agent's row |
mesh_send returns a result rather than a promise of one: state is delivered or error,
and a failure names its cause (unreachable, unauthorized, unknown-peer, not-configured).
There is no queue behind it — see below.
session.cancel. Interrupting an agent's work is a human's call, never a peer's.pnpm add dsh-fleet-mesh
Then add it to your profile's bundles. The plugin ships a neutral default: it inserts its row with no config, so a fresh install opens nothing and serves nobody.
Supply your own values as an id-targeted override in your profile's patch layer:
- id: dsh-fleet-mesh
config:
host: '0.0.0.0' # loopback-only by default
port: 8760
allow: # the real door, checked before anything else runs
- 192.168.0.0/23
durableRoute: '...' # named in an "unreachable" reply, so a sender is told
# where to write instead of retrying into a void
agents:
<agent-name>:
preset: <preset> # the session is resolved by PRESET, never by a pinned id
sessionId: null # optional override for the odd case
keyPath: '...' # this agent's Ed25519 private key (PKCS8 PEM)
wake: [] # who may START a turn — empty ships by default
The plugin ships no subnets and no names: which networks may reach a listener, and who may wake an agent, are a deployment's decisions.
The obvious implementation is to register a route on the harness's own web server. It cannot
work, and the reason is measured rather than assumed: that server binds 127.0.0.1 only, so
a peer's POST never completes a TCP connection. Widening it to 0.0.0.0 would expose the
entire web surface — GUI, API, and every other plugin's routes — which is a far larger act than
opening one mesh route.
So this plugin owns its listener and exposes only /mesh/*.
| route | purpose |
|---|---|
POST /mesh/receive | verify an Ed25519-signed envelope, then deliver it as a prompt |
GET /mesh/health | whether delivery is actually possible, not merely that the process is alive |
Node can bind an address or all interfaces; it cannot bind a subnet. So the network limit lives in the request path, checked before JSON parsing, before the peer lookup, and before any signature work — an unwanted caller cannot make this process do work.
192.168.0.0/23 is "192.168.0.* and 192.168.1.*": a /23 spans 192.168.0.0 through
192.168.1.255. One rule rather than two, so the halves cannot drift apart. Loopback is always
admitted.
Two details are load-bearing:
::ffff:192.168.1.34; without normalization the allow list would silently reject
every legitimate LAN caller — a failure that looks exactly like "the peer cannot reach me".Bracketed header, then a free-form body:
[mesh][v:1][from:<sender>][to:<recipient>][id:<uuid>][session:?][from_session:?][action:do|info][reply:yes|no|end][ref:?]
action and reply are optional on receive (a missing value defaults to info/no) but
required on send — omitting them is how a tolerant receiver silently downgrades a request
into an acknowledgement. to: '*' is a broadcast, and a broadcast never wakes anyone.
Each policy decision maps to one word — the name of an agent method — so the two cannot drift:
| situation | call | effect |
|---|---|---|
| idle, or busy + new thread | agent.followup(msg) | next-turn + wake — a whole turn, now or at the boundary |
busy + same thread (ref), or action: do from a listed peer | agent.steer(msg) | next-step + wake — joins the work in progress |
| off the wake list, or a broadcast | agent.inject(msg) | next-step, no wake — lands at the next step |
The inbox is a durable projection over session events, so a queued message survives a restart. A spool held in plugin memory would not.
"{X-Mesh-Timestamp}\n{body}", with a fallback to the body alone for senders that omit the
timestamp.sec-fetch-site and no browser origin, so a same-origin check would
reject every legitimate sender.unreachable and
names the durable route, so a sender never acts on a lie.node test/mesh.test.mjs # envelope, crypto, access, policy, delivery
node test/outbound.test.mjs # signing, retries, the peer directory, registry canonical JSON
node test/smoke.mjs # the route table, driven with real Ed25519 signatures
node test/roundtrip.test.mjs # the tools driving this plugin's own receive route
The round trip is the one that makes the halves one thing: it stands up the real receive handler on a loopback port, a real identity store in a temp directory, and a stub registry that verifies the registration signature rather than accepting it — then drives all five tools through it. Nothing is mocked at the boundary that matters.
No dependencies to install: this plugin uses node: builtins only, and CI runs the suites on
Node 20, 22 and 24 with no install step.
To test against a real identity store, point DSH_MESH_LIVE_STORE at a directory of
<name>/identity.yaml entries. It is opt-in on purpose: a published plugin's tests must
pass on someone else's machine.
Publishing uses npm trusted publishing (OIDC) — there is no npm token anywhere, in the repo or in a secret. npm exchanges the workflow's OIDC identity for a short-lived credential, which is also why classic tokens are irrelevant here: npm disabled their creation in November 2025 and revoked the existing ones in December 2025.
One-time setup. A package cannot have a trusted publisher before it exists, so the first publish is manual:
Publish the first version once from a machine logged into npm:
npm login
npm publish --access public
Configure the trusted publisher. Two prerequisites that are easy to miss and are not validated when you save the form:
403 … OIDC permission denied for this action, which reads like an
authentication problem and is not one.The unambiguous route is the CLI (needs npm ≥ 11.15.0; the registry allows only one configuration per package, so revoke an existing one first):
npm trust list dsh-fleet-mesh # what npm actually has, and its permissions
npm trust revoke --id <id> dsh-fleet-mesh
npm trust github dsh-fleet-mesh \
--repo emiltsoi/dsh-fleet-mesh \
--file publish.yml \
--allow-publish
Or via the website — https://www.npmjs.com/package/dsh-fleet-mesh/access → Add a trusted
publisher:
| field | value |
|---|---|
| Publisher | GitHub Actions |
| Organization or user | emiltsoi |
| Repository | dsh-fleet-mesh |
| Workflow file | publish.yml |
| Environment | (leave empty — this job declares none) |
publish.yml, not the full path. npm's own CLI documents --file as the "name of
workflow file within a repository's .github folder", and a full path is a mismatch that only
surfaces at publish time.
Every release after that is automatic: publish a GitHub release, and the workflow runs the
suites and then npm publish --provenance.
Two requirements worth knowing: npm CLI ≥ 11.5.1 (the workflow installs the latest), and
id-token: write in the job's permissions — already set.
A peer says it cannot reach me. Check the bind with a local socket query
(Get-NetTCPConnection -LocalPort <port> on Windows, ss -ltnp elsewhere). A shell HTTP probe
can be intercepted by an egress proxy and will report a 403 that has nothing to do with your
listener.
Everything returns 401. The peer's public key probably did not parse. Public keys appear
both as | block scalars and as quoted scalars spread over several lines; a parser that
reads only the first line returns -----BEGIN PUBLIC KEY----- and fails every peer while the
code still looks correct. This suite asserts against real files for exactly that reason.
The port is open but nothing arrives. On Windows, look for a program-scoped firewall rule covering the process that owns the listener — a port-scoped query misses it, and the owning process is often not the one you expect.
A peer is never woken. The wake list ships empty, and a broadcast never wakes by design. Check both before suspecting the transport.
A wake-listed peer stopped being woken. That is the runaway breaker. It clears on a real user message in the session — by design, mesh traffic cannot clear it.
403 … OIDC permission denied for this action when publishing. This one is worth reading
carefully, because it looks like an authentication failure and is not. The tell is in the log:
npm signs a provenance statement and publishes it to the Sigstore transparency log before the
PUT is rejected — and provenance is signed locally from the GitHub token, so it proves only
that id-token: write works. It says nothing about npm's config.
So the trust exists and authenticated, and the publish action was denied. Check, in order:
publish.yml), not a path.npm trust list <package> shows what npm actually has. A 404 or ENEEDAUTH instead means npm
never got an OIDC token at all — a different problem: look at the job's id-token: write
permission, and make sure NODE_AUTH_TOKEN is not set to an empty string, which makes npm take
the token path and never fall through to trusted publishing.
MIT