@nelsonlongxiang/dsh-open-a2a-net 0.6.37 → 0.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +340 -335
  2. package/README_zh.md +241 -236
  3. package/assets/nexusDist/assets/index-C7HagaGy.css +1 -1
  4. package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -9
  5. package/cli/a2a.mjs +44 -6
  6. package/cli/serve.mjs +132 -0
  7. package/cordis.patch.yml +21 -21
  8. package/lib/canvas-discovery.js +29 -0
  9. package/lib/canvas-id.js +27 -0
  10. package/lib/canvas-store.js +7 -1
  11. package/lib/client.js +63 -63
  12. package/lib/config.js +125 -0
  13. package/lib/direct-forward.js +105 -37
  14. package/lib/faces/admin.js +14 -1
  15. package/lib/faces/wire.js +41 -10
  16. package/lib/flood-guard.js +3 -1
  17. package/lib/index.js +116 -155
  18. package/lib/live-roots.js +39 -0
  19. package/lib/origin-claim.js +4 -3
  20. package/lib/peer-directory.js +1 -1
  21. package/lib/queue-guard.js +3 -1
  22. package/lib/skill/references/cli.md +214 -199
  23. package/lib/skill/references/pitfalls.md +1 -1
  24. package/lib/skill/references/routing.md +4 -0
  25. package/lib/skill/references/setup.md +5 -9
  26. package/lib/task-auth.js +15 -11
  27. package/lib/task-ledger-rows.js +32 -4
  28. package/lib/task-ledger.js +11 -5
  29. package/lib/tools/info.js +12 -5
  30. package/lib/types/canvas-discovery.d.ts +20 -0
  31. package/lib/types/canvas-id.d.ts +9 -0
  32. package/lib/types/config.d.ts +278 -0
  33. package/lib/types/direct-forward.d.ts +33 -2
  34. package/lib/types/faces/admin.d.ts +12 -0
  35. package/lib/types/faces/wire.d.ts +10 -1
  36. package/lib/types/index.d.ts +3 -277
  37. package/lib/types/live-roots.d.ts +35 -0
  38. package/lib/types/origin-claim.d.ts +5 -1
  39. package/lib/types/queue-guard.d.ts +2 -0
  40. package/lib/types/task-auth.d.ts +5 -0
  41. package/lib/types/task-ledger-rows.d.ts +16 -5
  42. package/lib/types/task-ledger.d.ts +3 -0
  43. package/lib/types/tools/info.d.ts +3 -0
  44. package/lib/types/types.d.ts +2 -0
  45. package/nexus-stage/package-lock.json +2262 -2262
  46. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
  47. package/nexus-stage/src/edges.ts +72 -72
  48. package/nexus-stage/src/layout-wire.ts +158 -158
  49. package/nexus-stage/src/seat.ts +35 -35
  50. package/nexus-stage/src/stage-name.ts +13 -13
  51. package/nexus-stage/src/tokens.ts +31 -31
  52. package/package.json +218 -218
package/README.md CHANGED
@@ -1,335 +1,340 @@
1
- # @nelsonlongxiang/dsh-open-a2a-net
2
-
3
- English | [中文](README_zh.md)
4
-
5
- [![npm](https://img.shields.io/npm/v/@nelsonlongxiang/dsh-open-a2a-net?label=npm)](https://www.npmjs.com/package/@nelsonlongxiang/dsh-open-a2a-net)
6
-
7
- Open A2A network plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): turns a DSH
8
- deployment into one node of a decentralized agent network — no central server, no registry.
9
-
10
- Each node publishes a **signed, expiring agent card** at `/.well-known/agent-card.json`, discovers peers through
11
- **seed URLs and referrals** (each card lists the peers it knows), resolves names through **zone delegations**
12
- (GNUnet GNS-style: a card may delegate a name to another zone), and routes **directly** to a peer's team with
13
- failover across the reachable half of candidates. Every main session can additionally be exposed as a **session
14
- node** other hosts can discover and join from the web sidebar.
15
-
16
- | The sidebar network panel | Context message display |
17
- | --- | --- |
18
- | ![Sidebar network panel](images/image-01.png) | ![Context message display](images/image-02.png) |
19
-
20
- ## Install
21
-
22
- ```sh
23
- dsh plugin --profile web add @nelsonlongxiang/dsh-open-a2a-net
24
- ```
25
-
26
- Restart `dsh web`, then open the sidebar network panel (footer action).
27
-
28
- (As a package specifier, a local path, or a Git URL; the package runs `pnpm build` in `prepare`, so a source
29
- install builds on install. Note: a standalone Git install currently fails to build outside a DSH profile —
30
- the official `@deepseek-ai/*` npm packages lag the harness source this plugin compiles against, so install
31
- from the registry or inside a profile whose harness provides current `@deepseek-ai` peers.) The plugin composes
32
- beside the stock
33
- webserver row: it registers its card, state, and control routes on the shared listener and its model tools on the
34
- shared tool runtime.
35
-
36
- ## What you get
37
-
38
- - **Model tools** (see the table below) — `a2a_teams` lists the teams this node can see (its own rows
39
- plus peer teams inside the joined-team scope); `a2a_route`
40
- sends one message to a team, reuses `context_id` to continue a remote conversation, and fails over across
41
- candidates when a peer is unreachable. `a2a_tasks` reconciles the receipt contract across three tiers: every route that leaves a
42
- task owed a reply (async delivery, a released wait) stays in the owed book as pending until its
43
- `[A2A receipt] task <task_id>` message correlates — overdue rows auto-dead-letter past the stale TTL (a revived target can still
44
- settle with a late receipt), every settlement lands in the bounded archive instead of being silently evicted, and each row keeps
45
- its follow-up `context_id`, persisted across
46
- restarts. `a2a_probe` measures the tracked fleet's reachability and round-trip latency (one verified-card
47
- fetch per peer, optionally narrowed to a single url) for pre-dispatch health checks, naming each miss's
48
- stage — `unreachable` (transport/HTTP) vs. `rejected` (distrusted card).
49
- - **Sidebar control** — a footer action in the web sidebar listing this host's sessions as joinable network
50
- nodes: title, recent-activity excerpt, and team per row, join/leave in place, and a wake action for cold rows
51
- (persisted join intent whose session is not loaded yet — opening the session remounts the node). Session teams
52
- are `<team>/<id8>`; web sessions use their id's first 8 chars, imported sessions (`import-<uuid>`) keep the
53
- `import-` prefix plus the uuid's first 8 hex chars so imported ids cannot collapse into a handful of teams.
54
- The panel's activity section also lists owed receipts — async tasks delivered but not yet correlated by their
55
- `[A2A receipt]` message — beside the in-flight waits, so a cross-turn wait is visible without asking the model.
56
- - **Archive leaves the network** — archiving a session (workspace registry state) prunes its join
57
- intent and unmounts its node: at boot settlement before any wake, on every state read (a mid-session
58
- archive disappears within one panel poll), and as a route-time guard that never wakes an archived
59
- target.
60
- - **Honest waits** — a synchronous route whose target never answers releases the caller at a 180s
61
- reply-wait deadline with the delivered shape and the receipt contract (the target answers on its own
62
- cadence); `async: true` skips the wait entirely. The network panel dims an in-flight row past 120s as
63
- a stale wait instead of implying live progress, and its title carries the package version badge.
64
- - **Announce** — `announce: true` publishes this node's card (team, capabilities, referrals, joined session
65
- teams) so peers find it without any directory.
66
-
67
- ## Model tools at a glance
68
-
69
- Ten tools. With `teamScopeInfo` on (the default), `a2a_teams`, `a2a_status` and `a2a_probe` answer from
70
- this host's joined-team scope: `[this host]` rows plus peers serving a team this host's nodes declared.
71
- `a2a_tasks` (this host's own dispatch ledger) stays unscoped.
72
-
73
- | Tool | Answers | Notes |
74
- | --- | --- | --- |
75
- | `a2a_whoami` | Who am I on this network? | The calling session's node id, joined/live state, routable handle, roster + canvas memberships, the owner's `teamJoinAllowlist`, the `teamScopeRouting` switch, and recent refused join/leave attempts. |
76
- | `a2a_teams` | Which teams can I route to? | `[this host]` rows plus in-scope peer teams, with activity excerpts. |
77
- | `a2a_route` | Send a message to a team. | `context_id` continues a conversation; `async: true` delivers without waiting; fails over across candidates. |
78
- | `a2a_tasks` | Which async tasks still owe a receipt? | Three tiers: owed rows keep the follow-up `context_id`; past-TTL rows auto-dead-letter (still settleable); outcomes land in the bounded archive; persisted across restarts. |
79
- | `a2a_probe` | Is the fleet healthy? | One verified-card fetch per in-scope peer (or one given url); `N reachable, M down`; misses classified `unreachable` vs `rejected`. |
80
- | `a2a_team_join` | Declare one joined node a member of a team. | Allowlist-gated: only an **exact** name in `teamJoinAllowlist` authorizes a join (empty list denies every join). A wildcard/prefix entry is shown by `a2a_whoami` but authorizes nothing (`[PATTERN_NO_MINT]`); canvas teams are assigned in the workbench, never here (`[CANVAS_TEAM]`). Attempts land in the state face's `registry.teamAudit`. |
81
- | `a2a_team_leave` | Retract a team membership. | Always allowed — leaving only shrinks exposure. |
82
- | `a2a_exposure` | How visible is this session? | Sets the session's own tier: `linked` (the default floor) or `fleet` (a voluntary opt-up); a group's visibility follows its strictest member. Requires a joined session. |
83
- | `a2a_report` | Tell this host's supervision something while off the network. | The one ingress exempt from the join gate: one-way, lands in the state face's reports ring, grants no presence or routing. One report per 30s per session, ≤4000 chars, identical text 3× in 10 min is refused as a loop. |
84
- | `a2a_status` | What is this node doing right now? | In-scope tracked peers with quality scores, in-flight routes, recent routing activity, and the idempotency window aggregate (occupancy + claim/replay/conflict counters). |
85
- | `a2a_file_send` | Hand files to a team without putting them in a message. | Stages each file (≤16 per call, ≤64 MiB each) on this host for that team and routes ONE message of `[A2A file]` reference lines; the bytes never ride the message (#91). |
86
- | `a2a_file_fetch` | Pull a file someone referenced. | Pulls one `[A2A file]` reference from the host that staged it, signed as this session; checks size and sha256 before saving it under `<cwd>/a2a-inbox/`. A grant names one session (or a whole host, or a canvas team's members); the fetch always marks the session as having read peer content. |
87
-
88
- ## a2a-net CLI (callers outside a session)
89
-
90
- The package also ships a command, `a2a-net` (`bin` → `cli/a2a.mjs`, 0.6.27+), for scripts, terminals
91
- and other agent harnesses that are not a DSH session. It creates its own Ed25519 identity
92
- (`<name>/<16 hex>`), deposits a signed card on one home host for up to 4h (`publish`; an expired deposit
93
- stays listed as an offline row until re-published), and receives into a mailbox on that host (64
94
- entries kept) instead of being woken:
95
-
96
- ```sh
97
- a2a-net init review-seat --home http://127.0.0.1:3080 # identity + home host
98
- A2A_KEY=... a2a-net publish # deposit (key-gated host)
99
- a2a-net route dsh/1a2b3c4d "status?" # async by default; --sync waits for the reply
100
- a2a-net inbox --new # unread mail; --wait blocks, --hook feeds a Claude Code hook
101
- ```
102
-
103
- By default it delivers only to teams on its home host. With `forwardDirect: true`, the home can forward an authenticated online CLI one hop to a linked peer target. Both operators must link: A links the receiving B member; B links the CLI handle and explicitly pins A as its home. Canvas targets require A to directly link every advertised receiving member, not merely rely on the peer canvas org. With scope and settlement enforcement on, the member-host auto-receipt settles A’s forwarded row and lands in the CLI inbox; `--sync` returns B’s reply. Commands, flags, environment
104
- variables and exit codes: [`src/skill/references/cli.md`](src/skill/references/cli.md).
105
-
106
- ## Canvas teams (arbitrary grouping, v0.5.25)
107
-
108
- Peer/member badges are host-specific display facts: bilateral URL seeds or an exact, nonlocal canvas-member handle uniquely advertised by that host card. Shared zone names (such as `dsh`) never establish a badge link. A minimal card without session rows remains unconfirmed/stranger unless bilateral seeds establish the host; this conservative display rule does not change caller admission or CLI home validation.
109
-
110
- A canvas team is a user-composed, named, multi-member routing group: **one atomic session node may sit in many teams**, and a route to `<team>/canvas/<name>` resolves the **first live member** (member order is the routing priority) or wakes the **first cold joined member** (wake-on-route). The `canvas/` path segment can never collide with a node alias (`<team>/<id8>`), so both namespaces coexist untouched.
111
-
112
- - Storage: `<dsh-home>/a2a/canvas.json` (ordered team entries; caps: 64 teams × 32 members)
113
- - Control API: `POST /__dsh_a2a/canvas` with `action: create | remove | add-member | remove-member`; `cli-home-options` is a read-only owner action for an existing CLI member.
114
- - For a CLI member marked “home 未确定”, open its canvas team inspector and choose “指定 home”. The host freshly reads cards, lists only linked advertisers, and reports unreadable-card counts. No home is selected automatically; confirming calls the existing local `add-member` with explicit `home`, which freshly validates again. A locally held household (live or offline tombstone) instead shows “本机持有,无需指定”, returns `held: true` with no choices, and rejects explicit home writes until that household is revoked. Online local households receive receipts first. A successful local publish removes a conflicting older pin and leaves a durable inspector notice; expiry during local receipt delivery fails closed instead of resolving another advertiser. This does not weaken automatic pinning or admission.
115
- - Membership requires a joined session (live node or remembered intent) — no routing backdoor over unjoined sessions; leaving the network (leave/archive) drops every canvas membership
116
- - A member may also be a **remote** zone-qualified team handle (`zone/a1b2c3d4`) the operator adds by hand — the link that admits that remote caller here and entitles it to the full card
117
- - `a2a_teams` lists canvas teams as local rows with member/live counts; `/__dsh_a2a/state` serves per-member `joined`/`live` flags for local members and `{ id, team, remote: true }` for remote ones
118
-
119
- ## Native-teams bridge (node unification, P1) — deprecated
120
-
121
- > **Deprecated.** `@nelsonlongxiang/dsh-native-teams` is deprecated. The bridge code still ships
122
- > (`src/teams-bridge.ts`; `nativeTeamsInbound` defaults to `false`), kept for existing deployments;
123
- > new deployments should not rely on it. The section below describes it as it stands.
124
-
125
- Two half-bridges to `@nelsonlongxiang/dsh-native-teams` (structural contract mirror: `src/teams-bridge.ts`; the frozen contract lives in that package's `src/a2a-face.ts`):
126
-
127
- - **Outbound transport face** — this plugin mounts the `nativeTeamsA2a` service: `resolve` answers through the one directory walk `submit` uses (no drift-prone double matcher), `submit` rides the direct-route dispatcher (per-candidate async gate mirroring `a2a_route`; a peer's idempotency 409 replay is terminal per delivery (async → accepted, sync → throws with the `-32003` literal), never a failover duplicate; accepted submissions track in the owed ledger; when the submitting session is a joined node, its node team rides the wire as the receipt `callback`, so a peer's receipt wakes the session that dispatched the round — wake-on-route covers a cold one; without a joined parent no callback rides and receipts fall back to the caller label), `cancel` drives the cooperative `[A2A cancel]` stop-notice through the wire to the owning team (peers do not track inbound task ids, so a ledger route would always answer unknown) and clears the owed row. It exposes nothing new — only teams the peer network already publishes resolve.
128
- - **Inbound dispatch** (config `nativeTeamsInbound`, default `false`) — a routed team name the sibling registry classifies as an unambiguous local claim starts a routed round through its authoritative seam (`describeTarget`/`startRound`), parented by this node's live initiator, with the A2A envelope riding the round message. Registry presence alone is never exposure: the operator opts in. Dispatcher-level only — inbound callers address the team, never its members (members stay visible-not-addressable). Rounds are bounded like the steer path: a 180s reply-wait deadline (`nativeRoundWaitMs`) answers the honest delivered-unsettled shape while the round keeps going, and the caller's abort (where one exists) cancels the round through its own signal. `wait: false` fires the round detached after prepare-first checks (claim, seam, initiator — a phantom dispatch never answers success); native-teams rounds do not emit A2A receipts in this slice, so their results carry the `bridge` marker and are never booked as receipt-owed (the receipt backflow to `callbackTarget` is the P2 slice).
129
-
130
- See `docs/native-teams-bridge.md` for the full mapping and scope notes.
131
-
132
- ## Configuration
133
-
134
- Row config overlays in the profile's patch layers; every key has a schema default.
135
-
136
- | Key | Default | Purpose |
137
- | --- | --- | --- |
138
- | `announce` | `false` | Publish this node's agent card for peer discovery. |
139
- | `isolated` | `false` | Send nothing to any peer: card reads, directory sweeps, routes, remote log reads, canvas pings and fleet canvas-op relays are all refused (gh#193). `a2a/peers.json` is left untouched and refused fetches do not score peers; inbound faces keep answering; the state face shows `isolated: true`. For test and acceptance hosts. |
140
- | `forwardDirect` | `false` | Opt in to one-hop forwarding for an authenticated online local CLI after a local target miss; sender scope and the hop guard remain enforced. Link B receiving members at A, and link/pin the CLI home A at B; no second forwarding hop. |
141
- | `peers` | `[]` | Seed base URLs; referred peers are learned from their cards. |
142
- | `delegates` | `[]` | Zone delegations: `{ name, url, publicKey }` published on the card. |
143
- | `team` | `'dsh'` | Team name this node exposes for direct routing. |
144
- | `session` | `''` | Caller label; `''` derives a stable `dsh-host-<8hex>` per home. |
145
- | `agentName` | `'DeepSeek Harness A2A node'` | Human-facing name on the card. |
146
- | `apiKey` | `''` | `X-API-Key` sent on peer requests; non-empty also gates the control routes. |
147
- | `sessionNodes` | `true` | Expose main sessions as joinable network nodes. |
148
- | `nativeTeamsInbound` | `false` | Dispatch inbound direct routes (and the outbound A2A tools' local candidates) to native-teams registry teams through its routing seam; needs the sibling plugin composed. |
149
- | `nativeRoundWaitMs` | `180000` | Reply-wait budget for one native-teams round (the bridge's deadline, mirroring the steer path's 180s). A round still running past it answers the honest delivered-unsettled shape and keeps going. |
150
- | `wakeJoinedOnBoot` | `false` | Prewarm cold joined sessions' agents after mount (needs the session controller wake face; wake-on-route and the sidebar wake button stay available without it). The prewarm is deferred, foreground-yielding, and cancellable — it never blocks the boot window (see the two knobs below). |
151
- | `wakePrewarmDelayMs` | `10000` | Idle delay between loader settlement and the first prewarm wake; `0` restores fire-at-settle. |
152
- | `wakePrewarmQuietMs` | `5000` | Foreground quiet window: a wake/route demand (or any outbound route in flight) inside this window postpones the next prewarm step; `0` disables the yield. |
153
- | `wakeBootStaggerMs` | `3000` | Pause between serial boot-prewarm wakes (caps log-decode saturation after a restart; wake-on-route never waits on it). |
154
- | `wakeReconcile` | `true` | Continuous desired-state reconciliation: each tick re-derives the cold joined intents due and wakes one per pass (post-boot drift, failed wakes, late gateways). Tuned by `wakeReconcileIntervalMs` (`60000`), `wakeReconcileBackoffBaseMs` (`5000`) and `wakeReconcileMaxBackoffMs` (`600000`). |
155
- | `seatNames` | `{}` | Human-owned stable display labels keyed by full local session id; controls/format characters (Cc/Cf), line/paragraph separators, names without visible content, and address-shaped names (`dsh/66abc2cf`, `zone/canvas/x`) are rejected. Invalid boot entries are dropped individually with warnings; owner writes remain all-or-nothing. Set/clear through the workbench owner panel; labels hot-apply to cards, whoami, CLI teams and the board, without changing titles, handles or admission. A2A model-tool naming arguments are refused. |
156
- | `teamJoinAllowlist` | `[]` | Teams `a2a_team_join` may declare: **exact names only** (a `*`/prefix entry authorizes nothing). Empty denies every join. The owner can also edit it from the workbench owner panel (`/__dsh_a2a/owner-settings`). |
157
- | `teamScopeRouting` | `true` | S3 admission: routing checks caller↔target shared team membership; a session with no declared team has no network. `false` restores the open outbound seam. |
158
- | `teamScopeInfo` | `true` | S5 information-plane scope for `a2a_teams` / `a2a_status` / `a2a_probe` (see the tools section). `false` restores fleet-wide disclosure. |
159
- | `peerAllowlist` | `[]` | Non-empty: a referral is admitted only when its host matches an entry (exact `host[:port]`, bare domain suffix, or full URL). Seeds are never filtered. |
160
- | `referralLearning` | `true` | `false` freezes the tracked set to the configured seeds: referrals are neither learned nor offered. |
161
- | `referralTeamResolution` | `false` | gh#8: team names resolve over seeds + delegates only; `true` restores the legacy whole-store basis (referral-learned peers included). |
162
- | `exposureExcerpt` | `'full'` | How much of a session's activity excerpt the card publishes: `full`, `masked` (shape-only prefix) or `off`. Titles and handles stay. |
163
- | `originClaimWindow` | `true` | OriginClaim transition window: deliveries without an envelope still walk the caller gate (counted as `legacyInbound`); `false` rejects them like failed signatures. |
164
- | `originClaimUrl` | `''` | Override for the envelope's `callerUrl` (pocket-tunneled hosts); `''` derives it. |
165
- | `originClaimUnboundWindow` | `true` | OriginClaim audience-binding window: also sign and accept the legacy envelope that names no audience (accepted ones counted as `unboundInbound`; those whose bound form missed here also as `boundMissInbound`); `false` signs and accepts the audience-bound form only. |
166
- | `originClaimAudiences` | `[]` | Extra origins this host answers to as an envelope audience, for port-mapped or proxied hosts whose dialed URL is neither the card URL nor the arrival address. |
167
- | `gateAgentLessCalls` | `'audit'` | Host-side (agent-less) routing calls: audit them, or `refuse`. |
168
- | `settlementAuth` | `'observe'` | Settlement authorization (#114): `off`, `observe` (record what would be refused, settle as before), or `enforce`. |
169
- | `asyncNudgeDelayMs` | `120000` | Delay before a delivered-but-unconsumed async route re-wakes its target with a one-line nudge; `0` disables. |
170
- | `targetQueueCap` | `3` | At most this many unanswered, receipt-owing tasks queue for one target on this host; the next delivery is refused visibly (`-32009 target-queue-full`, nothing delivered). A receipt for a task this host booked and CLI mailboxes are never refused; a canvas team counts as one target. A refused task id is recorded as failed — retry with a new id. A target-attributed final or its own receipt attempt records `answeredAt` and releases queue capacity even if receipt delivery fails; the row remains owed, with unchanged expiry, late-receipt and conservation semantics. State/board queue counts use this same rule. `0` disables (#224/#253). |
171
- | `taskStaleTtlMs` | `86400000` | Owed-task stale TTL (24h): past it a pending row auto-dead-letters (a late receipt can still settle it). |
172
- | `dshHome` | `''` | DSH home directory override for the plugin's state (`<home>/a2a/…`); `''` resolves the default home. |
173
- | `cardTtlMs` / `flushTimeoutMs` / `routeTimeoutMs` | see schema | Card lifetime (48h) and route timing budgets. |
174
- | `cardCacheTtlMs` / `cardCacheNegativeTtlMs` / `remoteRowsTtlMs` / `stateColdRowsTtlMs` | `60000` / `30000` / `15000` / `5000` | Peer card cache (hit / miss), remote session rows refresh window, and cold-row refresh window of the state face. |
175
-
176
- Example — one announcing node with a seed:
177
-
178
- ```yaml
179
- - id: a2a
180
- name: '@nelsonlongxiang/dsh-open-a2a-net'
181
- config:
182
- announce: true
183
- peers: ['http://127.0.0.1:41243']
184
- ```
185
-
186
- ## Leave semantics (bounded staleness)
187
-
188
- `leave` is a per-session, per-host gesture: it removes the join intent locally and unmounts the node. There is **no network-wide revocation broadcast** — peers keep listing the team until their cached copy of this node's card refreshes (`cardCacheTtlMs`, 60s by default — `sessionTeams` is an unsigned serve-fresh field, so the card's own 48h TTL does not hold it), and inbound deliveries during that window fail honestly (the host no longer resolves the team). Remote targets that already implemented the receipt contract are unaffected. A formal revocation-broadcast protocol is tracked in upstream note `2026-08-27-a2a-leave-no-revocation-broadcast.md` (proposed).
189
-
190
- ## Trust model
191
-
192
- Cards are Ed25519-signed and expire; `peers` referrals, `sessionTeams` listings, `lanIp`, and `version` on a card
193
- are unsigned and read fresh — the last two exist so operators can audit which build every fleet node actually runs
194
- without visiting each host. A referral URL whose own fetch just proved it serves this node's card is remembered,
195
- and re-offers of it within a suppression window are ignored instead of flickering in and out of the peer store.
196
- Every `/__dsh_a2a/*` control route — reads included — requires the configured `apiKey` (`X-API-Key`,
197
- constant-time compare) when one is set. With an empty key only a loopback socket is trusted; a
198
- `Sec-Fetch-Site` header can only refuse (a cross-site page is rejected even on loopback), never grant.
199
- Set an `apiKey` before exposing the listener beyond loopback.
200
-
201
- | Control route | Purpose |
202
- | --- | --- |
203
- | `GET /__dsh_a2a/state` | The state face the panel and workbench poll (sessions, canvas, peers, remote rows, activity, ledger). |
204
- | `POST /__dsh_a2a/join` · `/leave` | Join or leave one session node. |
205
- | `/__dsh_a2a/canvas` · `/canvas-layout` | Canvas teams (create/remove/add-member/remove-member) and the board layout. |
206
- | `POST /__dsh_a2a/roster-op` | Owner-controlled roster team creation, member add/remove, dissolution, and exact allowlist maintenance. |
207
- | `/__dsh_a2a/groups` | Manual session grouping — deprecated since 0.6.6 (canvas teams supersede it), kept for old panels. |
208
- | `POST /__dsh_a2a/dispatch` | Workbench quick dispatch into a team. |
209
- | `POST /__dsh_a2a/tasks/abandon` · `/tasks/cancel` | Settle an owed task from the caller side (cancel also steers a stop notice to a live local target); the row lands in `archived[]`. |
210
- | `/__dsh_a2a/session-log` · `/remote-session-log` | Session log reads for the workbench. |
211
- | `/__dsh_a2a/owner-settings` | The owner panel: read (GET) or write (POST) `teamJoinAllowlist`, `teamScopeInfo`, `seatNames` and `announce` into the home patch layer (all but `announce` apply immediately). `seatNames` is a validated full map; omit a session id to clear its label. This owner-authenticated control face is not a model naming tool; local shell/config writers remain inside the existing operator trust boundary. |
212
- | `/__dsh_a2a/fleet-canvas-op` | Relay a canvas operation (`create`, `add-member`, `remove-member`, `remove`) to a peer's `/a2a/canvas-op`, stamped with this node's OriginClaim; the peer's link gate decides. |
213
- | `/__dsh_a2a/cli-deposit` | Deposit, list, revoke and mailbox reads for `a2a-net` CLI identities. |
214
- | `GET /__dsh_a2a/nodes` · `POST` | Node identities (docs/design/node-identity.md): GET reads the roster or one node (`?name=`); POST is an identity write and needs the apiKey even on loopback. |
215
- | `POST /__dsh_a2a/blob` | Stage a file for a recipient (`?to=&name=`) — the file channel's write side; peers pull through `GET /a2a/blob`. |
216
-
217
- The state face's ledger: `tasks` (pending, each with `settleable`), `tasksStaleCount`, `tasksDead` (with
218
- `direction` when known), and `archived` — the newest 64 settled rows, newest first, each with `outcome`,
219
- `endedBy` (`receipt` | `echo` | `abandoned` | `cancelled`), `late: true` when a receipt arrived after the
220
- caller ended it, and `direction` when known — plus `archivedCount` and `archivedSplit` (count per outcome).
221
-
222
- ## Stages (visualization surfaces)
223
-
224
- Two full-page stages mount beside the model tools: `/__dsh_a2a_nexus` (the
225
- A2A workbench — since 0.6.2 the 2D spatial planning board IS the stage and
226
- the directory/ledger/activity/plan panels ride a collapsible side rail in
227
- one merged, responsive shell; while the canvas face is down the panels fall
228
- back to the main stack; the Three.js topology viewer was retired in 0.6.0;
229
- `?mode=plan` still converges on the board) and `/__dsh_a2a_canvas`. A bare
230
- mount path
231
- redirects (301) onto its trailing-slash form, so both spellings render and
232
- the shell's relative assets always resolve inside the served tree. For
233
- offline development, `nexus-stage/scripts/mockhost.py [port]` serves the
234
- built stage with canned state; open `/?fault=500` on it to exercise the
235
- on-page fault badge. `pnpm verify` needs no manual bootstrap on a fresh
236
- checkout: its `verify:nexus` step installs the subpackage dependencies
237
- itself before building.
238
-
239
- ## Collaboration SOP (network practice, v0.1)
240
-
241
- > Current rules: the A2A collaboration norm ([`src/skill/references/collaboration.md`](src/skill/references/collaboration.md), in Chinese, shipped with the plugin to every node). The v0.1 SOP below is kept as history; where they conflict, the norm wins.
242
-
243
- Distilled from the 0.1.x–0.5.x delivery cycles by the network's research node; adopted by the maintainer.
244
-
245
- - **Roles** — one session, one office, no crossing workspace boundaries: a research node (protocol/design
246
- review, verdict criteria, source-line references with must-fix/recommend labels), a maintainer node
247
- (version rulings, releases, adoption receipts), a test node (the single execution point for all live-host
248
- verification; production hosts are never used for ad-hoc testing), and an install/ops node (production
249
- profile deployment, single-owner, waits for the maintenance window).
250
- - **Dispatch discipline** — test tasks go to the test node with version + commit + decidable criteria +
251
- receipt target; review tasks go to the research node with source pointers, not paraphrases.
252
- - **Receipt contract** — tasks longer than minutes route with `async: true` (wait:false); receipts read
253
- `[A2A receipt] task <task_id> <outcome summary>`; the task id is caller-born, request-carried,
254
- peer-echoed, and steered-header-transmitted — four-way agreement before a receipt correlates.
255
- - **Upgrade chain** — test node green → maintainer receipt → ops node requests the window → production
256
- upgrade → maintainer's browser-face final check → network-wide notice.
257
- - **Reuse red lines** — shared pure logic extracts to a library; shared runtime capability extracts to a
258
- Service; plugins never value-import each other; production hosts carry no verification duty (no
259
- dev-who-self-tests).
260
-
261
- ## Verify
262
-
263
- Tests are layered by depth (contract: `docs/testing-layers.md`): each layer
264
- has its own directory and script, so iteration can target one depth.
265
-
266
- ```sh
267
- pnpm install # auto-install-peers pulls the DSH peer train from npm
268
- pnpm verify # typecheck + build + ALL vitest layers below + layout gate + nexus-stage suite
269
- pnpm test:unit # L0 — pure logic, no fiber, no sockets (stores, codecs, jsdom components)
270
- pnpm test:wire # L1 — ONE real plugin instance over real HTTP against faked peers
271
- pnpm test:mesh # L2 — >=2 REAL nodes (own homes/keys/peers.json) over real HTTP: the
272
- # cross-machine equivalent, incl. fault injection (clock-skew cards,
273
- # tampered cards, partition/restart, alias collapse, zone delegation,
274
- # referral freeze) and receipt replay — see the fault matrix in
275
- # docs/testing-layers.md
276
- pnpm test:live # L4 — REAL remote machines, env-gated (DSH_A2A_LIVE_MESH=...); skipped
277
- # by default, never in CI — runbook in docs/testing-layers.md
278
- pnpm build # host lib (including lib/a2a-client.js), client types, and browser bundle (lib/client.js)
279
- ```
280
-
281
- ## Non-DSH interop (protocol modules)
282
-
283
- A node that is NOT a DeepSeek Harness host can still speak the protocol. The
284
- four dependency-free protocol modules are exported subpaths (gh#13) — import
285
- them without pulling the DSH peer train:
286
-
287
- ```js
288
- import { signCard, verifyCard } from '@nelsonlongxiang/dsh-open-a2a-net/card'
289
- import { formatReceipt, parseReceipt } from '@nelsonlongxiang/dsh-open-a2a-net/receipt'
290
- import { withinRouteBodyCap } from '@nelsonlongxiang/dsh-open-a2a-net/transport-caps'
291
- import { A2aClient } from '@nelsonlongxiang/dsh-open-a2a-net/a2a-client'
292
- ```
293
-
294
- Implement your own signed card, `GET /.well-known/agent-card.json`, and
295
- `POST /a2a/direct`. The wire shapes these modules encode are the contract.
296
- Anonymous card fetches get the minimal discovery face. The full card needs a
297
- verified OriginClaim envelope (`X-A2A-Caller` / `-Url` / `-Ts` / `-Sig`,
298
- signed with your own card key) **and** a link to the serving host: your card
299
- URL shares its origin, your handle is one of its joined session handles, its
300
- operator added your exact handle as a canvas remote member, or both operators
301
- seeded each other (its `peers` names your origin and your card's `peers` names
302
- its origin). A verified but unlinked caller still gets the minimal card. See
303
- `docs/protocol/delivery-origin-auth.md` and the rest of `docs/protocol/` for
304
- the envelope details.
305
-
306
- **Which artifact to build against.** npm direct install already includes the
307
- four subpaths: releases from 0.6.25 (badge `latest`) carry them, so
308
- `npm install @nelsonlongxiang/dsh-open-a2a-net` works out of the box —
309
- see `examples/external-node/` for a runnable external-node example that
310
- exercises all four subpaths against an embedded mock peer. The subpath
311
- exports landed in commit `9cb3852`; versions before that (public 0.5.15
312
- included) predate the admission rules and the OriginClaim envelope above,
313
- and importing the subpaths from them fails with
314
- `ERR_PACKAGE_PATH_NOT_EXPORTED` — the historical recipe (pin a commit,
315
- `pnpm build`, `npm pack`, depend on the tarball) survives below for
316
- pre-0.6.25 deployments only:
317
-
318
- ```bash
319
- # HISTORICAL (0.5.15 and earlier): build the package from a pinned commit
320
- git clone https://github.com/NelsonLongxiang/dsh-open-a2a-net && cd dsh-open-a2a-net
321
- git checkout <commit> # pin one; any commit at or after 9cb3852 exports the subpaths
322
- pnpm install --frozen-lockfile # pnpm version from package.json "packageManager"
323
- pnpm build
324
- npm pack # → nelsonlongxiang-dsh-open-a2a-net-<version>.tgz
325
- # in your project:
326
- npm install /path/to/nelsonlongxiang-dsh-open-a2a-net-<version>.tgz
327
- ```
328
-
329
- Installing the package (npm or the historical tarball) pulls no
330
- `@deepseek-ai/*` package: the DSH peers are optional and the four
331
- subpaths import only each other and `node:crypto`.
332
-
333
- ## License
334
-
335
- MIT
1
+ # @nelsonlongxiang/dsh-open-a2a-net
2
+
3
+ English | [中文](README_zh.md)
4
+
5
+ [![npm](https://img.shields.io/npm/v/@nelsonlongxiang/dsh-open-a2a-net?label=npm)](https://www.npmjs.com/package/@nelsonlongxiang/dsh-open-a2a-net)
6
+
7
+ Open A2A network plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): turns a DSH
8
+ deployment into one node of a decentralized agent network — no central server, no registry.
9
+
10
+ Each node publishes a **signed, expiring agent card** at `/.well-known/agent-card.json`, discovers peers through
11
+ **seed URLs and referrals** (each card lists the peers it knows), resolves names through **zone delegations**
12
+ (GNUnet GNS-style: a card may delegate a name to another zone), and routes **directly** to a peer's team with
13
+ failover across the reachable half of candidates. Every main session can additionally be exposed as a **session
14
+ node** other hosts can discover and join from the web sidebar.
15
+
16
+ | The sidebar network panel | Context message display |
17
+ | --- | --- |
18
+ | ![Sidebar network panel](images/image-01.png) | ![Context message display](images/image-02.png) |
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ dsh plugin --profile web add @nelsonlongxiang/dsh-open-a2a-net
24
+ ```
25
+
26
+ Restart `dsh web`, then open the sidebar network panel (footer action).
27
+
28
+ (As a package specifier, a local path, or a Git URL; the package runs `pnpm build` in `prepare`, so a source
29
+ install builds on install. Note: a standalone Git install currently fails to build outside a DSH profile —
30
+ the official `@deepseek-ai/*` npm packages lag the harness source this plugin compiles against, so install
31
+ from the registry or inside a profile whose harness provides current `@deepseek-ai` peers.) The plugin composes
32
+ beside the stock
33
+ webserver row: it registers its card, state, and control routes on the shared listener and its model tools on the
34
+ shared tool runtime.
35
+
36
+ ## What you get
37
+
38
+ - **Model tools** (see the table below) — `a2a_teams` lists the teams this node can see (its own rows
39
+ plus peer teams inside the joined-team scope); `a2a_route`
40
+ sends one message to a team, reuses `context_id` to continue a remote conversation, and fails over across
41
+ candidates when a peer is unreachable. `a2a_tasks` reconciles the receipt contract across three tiers: every route that leaves a
42
+ task owed a reply (async delivery, a released wait) stays in the owed book as pending until its
43
+ `[A2A receipt] task <task_id>` message correlates — overdue rows auto-dead-letter past the stale TTL (a revived target can still
44
+ settle with a late receipt), every settlement lands in the bounded archive instead of being silently evicted, and each row keeps
45
+ its follow-up `context_id`, persisted across
46
+ restarts. `a2a_probe` measures the tracked fleet's reachability and round-trip latency (one verified-card
47
+ fetch per peer, optionally narrowed to a single url) for pre-dispatch health checks, naming each miss's
48
+ stage — `unreachable` (transport/HTTP) vs. `rejected` (distrusted card).
49
+ - **Sidebar control** — a footer action in the web sidebar listing this host's sessions as joinable network
50
+ nodes: title, recent-activity excerpt, and team per row, join/leave in place, and a wake action for cold rows
51
+ (persisted join intent whose session is not loaded yet — opening the session remounts the node). Session teams
52
+ are `<team>/<id8>`; web sessions use their id's first 8 chars, imported sessions (`import-<uuid>`) keep the
53
+ `import-` prefix plus the uuid's first 8 hex chars so imported ids cannot collapse into a handful of teams.
54
+ The panel's activity section also lists owed receipts — async tasks delivered but not yet correlated by their
55
+ `[A2A receipt]` message — beside the in-flight waits, so a cross-turn wait is visible without asking the model.
56
+ - **Archive leaves the network** — archiving a session (workspace registry state) prunes its join
57
+ intent and unmounts its node: at boot settlement before any wake, on every state read (a mid-session
58
+ archive disappears within one panel poll), and as a route-time guard that never wakes an archived
59
+ target.
60
+ - **Honest waits** — a synchronous route whose target never answers releases the caller at a 180s
61
+ reply-wait deadline with the delivered shape and the receipt contract (the target answers on its own
62
+ cadence); `async: true` skips the wait entirely. The network panel dims an in-flight row past 120s as
63
+ a stale wait instead of implying live progress, and its title carries the package version badge.
64
+ - **Announce** — `announce: true` publishes this node's card (team, capabilities, referrals, joined session
65
+ teams) so peers find it without any directory.
66
+
67
+ ## Model tools at a glance
68
+
69
+ Ten tools. With `teamScopeInfo` on (the default), `a2a_teams`, `a2a_status` and `a2a_probe` answer from
70
+ this host's joined-team scope: `[this host]` rows plus peers serving a team this host's nodes declared.
71
+ `a2a_tasks` (this host's own dispatch ledger) stays unscoped.
72
+
73
+ | Tool | Answers | Notes |
74
+ | --- | --- | --- |
75
+ | `a2a_whoami` | Who am I on this network? | The calling session's node id, joined/live state, routable handle, roster + canvas memberships, the owner's `teamJoinAllowlist`, the `teamScopeRouting` switch, and recent refused join/leave attempts. |
76
+ | `a2a_teams` | Which teams can I route to? | `[this host]` rows plus in-scope peer teams, with activity excerpts. |
77
+ | `a2a_route` | Send a message to a team. | `context_id` continues a conversation; `async: true` delivers without waiting; fails over across candidates. |
78
+ | `a2a_tasks` | Which async tasks still owe a receipt? | Three tiers: owed rows keep the follow-up `context_id`; past-TTL rows auto-dead-letter (still settleable); outcomes land in the bounded archive; persisted across restarts. |
79
+ | `a2a_probe` | Is the fleet healthy? | One verified-card fetch per in-scope peer (or one given url); `N reachable, M down`; misses classified `unreachable` vs `rejected`. |
80
+ | `a2a_team_join` | Declare one joined node a member of a team. | Allowlist-gated: only an **exact** name in `teamJoinAllowlist` authorizes a join (empty list denies every join). A wildcard/prefix entry is shown by `a2a_whoami` but authorizes nothing (`[PATTERN_NO_MINT]`); canvas teams are assigned in the workbench, never here (`[CANVAS_TEAM]`). Attempts land in the state face's `registry.teamAudit`. |
81
+ | `a2a_team_leave` | Retract a team membership. | Always allowed — leaving only shrinks exposure. |
82
+ | `a2a_exposure` | How visible is this session? | Sets the session's own tier: `linked` (the default floor) or `fleet` (a voluntary opt-up); a group's visibility follows its strictest member. Requires a joined session. |
83
+ | `a2a_report` | Tell this host's supervision something while off the network. | The one ingress exempt from the join gate: one-way, lands in the state face's reports ring, grants no presence or routing. One report per 30s per session, ≤4000 chars, identical text 3× in 10 min is refused as a loop. |
84
+ | `a2a_status` | What is this node doing right now? | In-scope tracked peers with quality scores, in-flight routes, recent routing activity, and the idempotency window aggregate (occupancy + claim/replay/conflict counters). |
85
+ | `a2a_file_send` | Hand files to a team without putting them in a message. | Stages each file (≤16 per call, ≤64 MiB each) on this host for that team and routes ONE message of `[A2A file]` reference lines; the bytes never ride the message (#91). |
86
+ | `a2a_file_fetch` | Pull a file someone referenced. | Pulls one `[A2A file]` reference from the host that staged it, signed as this session; checks size and sha256 before saving it under `<cwd>/a2a-inbox/`. A grant names one session (or a whole host, or a canvas team's members); the fetch always marks the session as having read peer content. |
87
+
88
+ ## a2a-net CLI (callers outside a session)
89
+
90
+ The package also ships a command, `a2a-net` (`bin` → `cli/a2a.mjs`, 0.6.27+), for scripts, terminals
91
+ and other agent harnesses that are not a DSH session. It creates its own Ed25519 identity
92
+ (`<name>/<16 hex>`), deposits a signed card on one home host for up to 4h (`publish`; an expired deposit
93
+ stays listed as an offline row until re-published), and receives into a mailbox on that host (64
94
+ entries kept) instead of being woken:
95
+
96
+ ```sh
97
+ a2a-net init review-seat --home http://127.0.0.1:3080 # identity + home host
98
+ A2A_KEY=... a2a-net publish # deposit (key-gated host)
99
+ a2a-net route dsh/1a2b3c4d "status?" # async by default; --sync waits for the reply
100
+ a2a-net inbox --new # unread mail; --wait blocks, --hook feeds a Claude Code hook
101
+ ```
102
+
103
+ From 0.6.38, `forwardDirect` defaults to `true`: after a local miss the home can forward an authenticated online CLI one hop to a linked peer target. Both operators must link: A links the receiving B member; B links the CLI handle and explicitly pins A as its home. For canvas targets A links only the owner host (#282 S3): receipts are admitted and settle only from receivers frozen from the owner's signed card, on that owner's key. With scope and settlement enforcement on, the member-host auto-receipt settles A’s forwarded row and lands in the CLI inbox; `--sync` returns B’s reply. Commands, flags, environment
104
+ variables and exit codes: [`src/skill/references/cli.md`](src/skill/references/cli.md).
105
+
106
+
107
+ ### Standalone network node (no DSH instance)
108
+
109
+ `a2a-net serve --config node.json` runs the network layer in its own Node process: cards and signatures, the peer directory, CLI deposits and mailboxes, one-hop forwarding, canvas ownership, the ledger, the state face and the workbench. It hosts no AI sessions; messages for a session are forwarded to a DSH host as usual. Run it on a stable machine as the CLIs' home so a DSH restart does not take their deposits, mailboxes and canvas directory down (they also survive the node's own restart). Install and config: the `a2a-net serve` section of [`src/skill/references/cli.md`](src/skill/references/cli.md).
110
+
111
+ ## Canvas teams (arbitrary grouping, v0.5.25)
112
+
113
+ Peer/member badges are host-specific display facts: bilateral URL seeds or an exact, nonlocal canvas-member handle uniquely advertised by that host card. Shared zone names (such as `dsh`) never establish a badge link. A minimal card without session rows remains unconfirmed/stranger unless bilateral seeds establish the host; this conservative display rule does not change caller admission or CLI home validation.
114
+
115
+ A canvas team is a user-composed, named, multi-member routing group: **one atomic session node may sit in many teams**, and a route to `<team>/canvas/<name>` resolves the **first live member** (member order is the routing priority) or wakes the **first cold joined member** (wake-on-route). The `canvas/` path segment can never collide with a node alias (`<team>/<id8>`), so both namespaces coexist untouched.
116
+
117
+ - Storage: `<dsh-home>/a2a/canvas.json` (ordered team entries; caps: 64 teams × 32 members)
118
+ - Control API: `POST /__dsh_a2a/canvas` with `action: create | remove | add-member | remove-member`; `cli-home-options` is a read-only owner action for an existing CLI member.
119
+ - For a CLI member marked “home 未确定”, open its canvas team inspector and choose “指定 home”. The host freshly reads cards, lists only linked advertisers, and reports unreadable-card counts. No home is selected automatically; confirming calls the existing local `add-member` with explicit `home`, which freshly validates again. A locally held household (live or offline tombstone) instead shows “本机持有,无需指定”, returns `held: true` with no choices, and rejects explicit home writes until that household is revoked. Online local households receive receipts first. A successful local publish removes a conflicting older pin and leaves a durable inspector notice; expiry during local receipt delivery fails closed instead of resolving another advertiser. This does not weaken automatic pinning or admission.
120
+ - Membership requires a joined session (live node or remembered intent) — no routing backdoor over unjoined sessions; leaving the network (leave/archive) drops every canvas membership
121
+ - A member may also be a **remote** zone-qualified team handle (`zone/a1b2c3d4`) the operator adds by hand — the link that admits that remote caller here and entitles it to the full card
122
+ - `a2a_teams` lists local and in-scope remote canvas teams with owner URL and member count; CLI `teams` also includes local canvas and peerOrg, retaining same-name owners as separate rows. `a2a_whoami.teams.canvasDetails` and CLI `whoami --json` list local/remote memberships of this identity (legacy `teams.canvas` stays local-name-only). Owner URLs are discovery metadata, not a requirement to override `--home`. Failure guidance distinguishes visible-directory absence, known-owner missing prerequisites, and multiple owners (candidates, never guessed). Information scope, candidate selection and admission do not change; `/__dsh_a2a/state` serves per-member `joined`/`live` flags for local members and `{ id, team, remote: true }` for remote ones
123
+
124
+ ## Native-teams bridge (node unification, P1) — deprecated
125
+
126
+ > **Deprecated.** `@nelsonlongxiang/dsh-native-teams` is deprecated. The bridge code still ships
127
+ > (`src/teams-bridge.ts`; `nativeTeamsInbound` defaults to `false`), kept for existing deployments;
128
+ > new deployments should not rely on it. The section below describes it as it stands.
129
+
130
+ Two half-bridges to `@nelsonlongxiang/dsh-native-teams` (structural contract mirror: `src/teams-bridge.ts`; the frozen contract lives in that package's `src/a2a-face.ts`):
131
+
132
+ - **Outbound transport face** — this plugin mounts the `nativeTeamsA2a` service: `resolve` answers through the one directory walk `submit` uses (no drift-prone double matcher), `submit` rides the direct-route dispatcher (per-candidate async gate mirroring `a2a_route`; a peer's idempotency 409 replay is terminal per delivery (async → accepted, sync → throws with the `-32003` literal), never a failover duplicate; accepted submissions track in the owed ledger; when the submitting session is a joined node, its node team rides the wire as the receipt `callback`, so a peer's receipt wakes the session that dispatched the round — wake-on-route covers a cold one; without a joined parent no callback rides and receipts fall back to the caller label), `cancel` drives the cooperative `[A2A cancel]` stop-notice through the wire to the owning team (peers do not track inbound task ids, so a ledger route would always answer unknown) and clears the owed row. It exposes nothing new — only teams the peer network already publishes resolve.
133
+ - **Inbound dispatch** (config `nativeTeamsInbound`, default `false`) — a routed team name the sibling registry classifies as an unambiguous local claim starts a routed round through its authoritative seam (`describeTarget`/`startRound`), parented by this node's live initiator, with the A2A envelope riding the round message. Registry presence alone is never exposure: the operator opts in. Dispatcher-level only — inbound callers address the team, never its members (members stay visible-not-addressable). Rounds are bounded like the steer path: a 180s reply-wait deadline (`nativeRoundWaitMs`) answers the honest delivered-unsettled shape while the round keeps going, and the caller's abort (where one exists) cancels the round through its own signal. `wait: false` fires the round detached after prepare-first checks (claim, seam, initiator — a phantom dispatch never answers success); native-teams rounds do not emit A2A receipts in this slice, so their results carry the `bridge` marker and are never booked as receipt-owed (the receipt backflow to `callbackTarget` is the P2 slice).
134
+
135
+ See `docs/native-teams-bridge.md` for the full mapping and scope notes.
136
+
137
+ ## Configuration
138
+
139
+ Row config overlays in the profile's patch layers; every key has a schema default.
140
+
141
+ | Key | Default | Purpose |
142
+ | --- | --- | --- |
143
+ | `announce` | `false` | Publish this node's agent card for peer discovery. |
144
+ | `isolated` | `false` | Send nothing to any peer: card reads, directory sweeps, routes, remote log reads, canvas pings and fleet canvas-op relays are all refused (gh#193). `a2a/peers.json` is left untouched and refused fetches do not score peers; inbound faces keep answering; the state face shows `isolated: true`. For test and acceptance hosts. |
145
+ | `forwardDirect` | `true` | From 0.6.38, one-hop forwarding is enabled by default for an authenticated online local CLI after a local target miss; sender scope and the hop guard remain enforced. Link B receiving members at A, and link/pin the CLI home A at B; no second forwarding hop. Set `forwardDirect: false` to restore local-only CLI delivery. |
146
+ | `peers` | `[]` | Seed base URLs; referred peers are learned from their cards. |
147
+ | `delegates` | `[]` | Zone delegations: `{ name, url, publicKey }` published on the card. |
148
+ | `team` | `'dsh'` | Team name this node exposes for direct routing. |
149
+ | `session` | `''` | Caller label; `''` derives a stable `dsh-host-<8hex>` per home. |
150
+ | `agentName` | `'DeepSeek Harness A2A node'` | Human-facing name on the card. |
151
+ | `apiKey` | `''` | `X-API-Key` sent on peer requests; non-empty also gates the control routes. |
152
+ | `sessionNodes` | `true` | Expose main sessions as joinable network nodes. |
153
+ | `nativeTeamsInbound` | `false` | Dispatch inbound direct routes (and the outbound A2A tools' local candidates) to native-teams registry teams through its routing seam; needs the sibling plugin composed. |
154
+ | `nativeRoundWaitMs` | `180000` | Reply-wait budget for one native-teams round (the bridge's deadline, mirroring the steer path's 180s). A round still running past it answers the honest delivered-unsettled shape and keeps going. |
155
+ | `wakeJoinedOnBoot` | `false` | Prewarm cold joined sessions' agents after mount (needs the session controller wake face; wake-on-route and the sidebar wake button stay available without it). The prewarm is deferred, foreground-yielding, and cancellable — it never blocks the boot window (see the two knobs below). |
156
+ | `wakePrewarmDelayMs` | `10000` | Idle delay between loader settlement and the first prewarm wake; `0` restores fire-at-settle. |
157
+ | `wakePrewarmQuietMs` | `5000` | Foreground quiet window: a wake/route demand (or any outbound route in flight) inside this window postpones the next prewarm step; `0` disables the yield. |
158
+ | `wakeBootStaggerMs` | `3000` | Pause between serial boot-prewarm wakes (caps log-decode saturation after a restart; wake-on-route never waits on it). |
159
+ | `wakeReconcile` | `true` | Continuous desired-state reconciliation: each tick re-derives the cold joined intents due and wakes one per pass (post-boot drift, failed wakes, late gateways). Tuned by `wakeReconcileIntervalMs` (`60000`), `wakeReconcileBackoffBaseMs` (`5000`) and `wakeReconcileMaxBackoffMs` (`600000`). |
160
+ | `seatNames` | `{}` | Human-owned stable display labels keyed by full local session id; controls/format characters (Cc/Cf), line/paragraph separators, names without visible content, and address-shaped names (`dsh/66abc2cf`, `zone/canvas/x`) are rejected. Invalid boot entries are dropped individually with warnings; owner writes remain all-or-nothing. Set/clear through the workbench owner panel; labels hot-apply to cards, whoami, CLI teams and the board, without changing titles, handles or admission. A2A model-tool naming arguments are refused. |
161
+ | `teamJoinAllowlist` | `[]` | Teams `a2a_team_join` may declare: **exact names only** (a `*`/prefix entry authorizes nothing). Empty denies every join. The owner can also edit it from the workbench owner panel (`/__dsh_a2a/owner-settings`). |
162
+ | `teamScopeRouting` | `true` | S3 admission: routing checks caller↔target shared team membership; a session with no declared team has no network. `false` restores the open outbound seam. |
163
+ | `teamScopeInfo` | `true` | S5 information-plane scope for `a2a_teams` / `a2a_status` / `a2a_probe` (see the tools section). `false` restores fleet-wide disclosure. |
164
+ | `peerAllowlist` | `[]` | Non-empty: a referral is admitted only when its host matches an entry (exact `host[:port]`, bare domain suffix, or full URL). Seeds are never filtered. |
165
+ | `referralLearning` | `true` | `false` freezes the tracked set to the configured seeds: referrals are neither learned nor offered. |
166
+ | `referralTeamResolution` | `false` | gh#8: team names resolve over seeds + delegates only; `true` restores the legacy whole-store basis (referral-learned peers included). |
167
+ | `exposureExcerpt` | `'full'` | How much of a session's activity excerpt the card publishes: `full`, `masked` (shape-only prefix) or `off`. Titles and handles stay. |
168
+ | `originClaimWindow` | `true` | OriginClaim transition window: deliveries without an envelope still walk the caller gate (counted as `legacyInbound`); `false` rejects them like failed signatures. |
169
+ | `originClaimUrl` | `''` | Override for the envelope's `callerUrl` (pocket-tunneled hosts); `''` derives it. |
170
+ | `originClaimUnboundWindow` | `true` | OriginClaim audience-binding window: also sign and accept the legacy envelope that names no audience (accepted ones counted as `unboundInbound`; those whose bound form missed here also as `boundMissInbound`); `false` signs and accepts the audience-bound form only. |
171
+ | `originClaimAudiences` | `[]` | Extra origins this host answers to as an envelope audience, for port-mapped or proxied hosts whose dialed URL is neither the card URL nor the arrival address. |
172
+ | `gateAgentLessCalls` | `'audit'` | Host-side (agent-less) routing calls: audit them, or `refuse`. |
173
+ | `settlementAuth` | `'observe'` | Settlement authorization (#114): `off`, `observe` (record what would be refused, settle as before), or `enforce`. |
174
+ | `asyncNudgeDelayMs` | `120000` | Delay before a delivered-but-unconsumed async route re-wakes its target with a one-line nudge; `0` disables. |
175
+ | `targetQueueCap` | `3` | At most this many unanswered, receipt-owing tasks queue for one target on this host; the next delivery is refused visibly (`-32009 target-queue-full`, nothing delivered). A receipt for a task this host booked and CLI mailboxes are never refused; a canvas team counts as one target. A refused task id is recorded as failed — retry with a new id. A target-attributed final or its own receipt attempt records `answeredAt` and releases queue capacity even if receipt delivery fails; the row remains owed, with unchanged expiry, late-receipt and conservation semantics. State/board queue counts use this same rule. `0` disables (#224/#253). |
176
+ | `taskStaleTtlMs` | `86400000` | Owed-task stale TTL (24h): past it a pending row auto-dead-letters (a late receipt can still settle it). |
177
+ | `dshHome` | `''` | DSH home directory override for the plugin's state (`<home>/a2a/…`); `''` resolves the default home. |
178
+ | `cardTtlMs` / `flushTimeoutMs` / `routeTimeoutMs` | see schema | Card lifetime (48h) and route timing budgets. |
179
+ | `cardCacheTtlMs` / `cardCacheNegativeTtlMs` / `remoteRowsTtlMs` / `stateColdRowsTtlMs` | `60000` / `30000` / `15000` / `5000` | Peer card cache (hit / miss), remote session rows refresh window, and cold-row refresh window of the state face. |
180
+
181
+ Example — one announcing node with a seed:
182
+
183
+ ```yaml
184
+ - id: a2a
185
+ name: '@nelsonlongxiang/dsh-open-a2a-net'
186
+ config:
187
+ announce: true
188
+ peers: ['http://127.0.0.1:41243']
189
+ ```
190
+
191
+ ## Leave semantics (bounded staleness)
192
+
193
+ `leave` is a per-session, per-host gesture: it removes the join intent locally and unmounts the node. There is **no network-wide revocation broadcast** — peers keep listing the team until their cached copy of this node's card refreshes (`cardCacheTtlMs`, 60s by default — `sessionTeams` is an unsigned serve-fresh field, so the card's own 48h TTL does not hold it), and inbound deliveries during that window fail honestly (the host no longer resolves the team). Remote targets that already implemented the receipt contract are unaffected. A formal revocation-broadcast protocol is tracked in upstream note `2026-08-27-a2a-leave-no-revocation-broadcast.md` (proposed).
194
+
195
+ ## Trust model
196
+
197
+ Cards are Ed25519-signed and expire; `peers` referrals, `sessionTeams` listings, `lanIp`, and `version` on a card
198
+ are unsigned and read fresh — the last two exist so operators can audit which build every fleet node actually runs
199
+ without visiting each host. A referral URL whose own fetch just proved it serves this node's card is remembered,
200
+ and re-offers of it within a suppression window are ignored instead of flickering in and out of the peer store.
201
+ Every `/__dsh_a2a/*` control route — reads included — requires the configured `apiKey` (`X-API-Key`,
202
+ constant-time compare) when one is set. With an empty key only a loopback socket is trusted; a
203
+ `Sec-Fetch-Site` header can only refuse (a cross-site page is rejected even on loopback), never grant.
204
+ Set an `apiKey` before exposing the listener beyond loopback.
205
+
206
+ | Control route | Purpose |
207
+ | --- | --- |
208
+ | `GET /__dsh_a2a/state` | The state face the panel and workbench poll (sessions, canvas, peers, remote rows, activity, ledger). |
209
+ | `POST /__dsh_a2a/join` · `/leave` | Join or leave one session node. |
210
+ | `/__dsh_a2a/canvas` · `/canvas-layout` | Canvas teams (create/remove/add-member/remove-member) and the board layout. |
211
+ | `POST /__dsh_a2a/roster-op` | Owner-controlled roster team creation, member add/remove, dissolution, and exact allowlist maintenance. |
212
+ | `/__dsh_a2a/groups` | Manual session grouping — deprecated since 0.6.6 (canvas teams supersede it), kept for old panels. |
213
+ | `POST /__dsh_a2a/dispatch` | Workbench quick dispatch into a team. |
214
+ | `POST /__dsh_a2a/tasks/abandon` · `/tasks/cancel` | Settle an owed task from the caller side (cancel also steers a stop notice to a live local target); the row lands in `archived[]`. |
215
+ | `/__dsh_a2a/session-log` · `/remote-session-log` | Session log reads for the workbench. |
216
+ | `/__dsh_a2a/owner-settings` | The owner panel: read (GET) or write (POST) `teamJoinAllowlist`, `teamScopeInfo`, `seatNames` and `announce` into the home patch layer (all but `announce` apply immediately). `seatNames` is a validated full map; omit a session id to clear its label. This owner-authenticated control face is not a model naming tool; local shell/config writers remain inside the existing operator trust boundary. |
217
+ | `/__dsh_a2a/fleet-canvas-op` | Relay a canvas operation (`create`, `add-member`, `remove-member`, `remove`) to a peer's `/a2a/canvas-op`, stamped with this node's OriginClaim; the peer's link gate decides. |
218
+ | `/__dsh_a2a/cli-deposit` | Deposit, list, revoke and mailbox reads for `a2a-net` CLI identities. |
219
+ | `GET /__dsh_a2a/nodes` · `POST` | Node identities (docs/design/node-identity.md): GET reads the roster or one node (`?name=`); POST is an identity write and needs the apiKey even on loopback. |
220
+ | `POST /__dsh_a2a/blob` | Stage a file for a recipient (`?to=&name=`) — the file channel's write side; peers pull through `GET /a2a/blob`. |
221
+
222
+ The state face's ledger: `tasks` (pending, each with `settleable`), `tasksStaleCount`, `tasksDead` (with
223
+ `direction` when known), and `archived` — the newest 64 settled rows, newest first, each with `outcome`,
224
+ `endedBy` (`receipt` | `echo` | `abandoned` | `cancelled`), `late: true` when a receipt arrived after the
225
+ caller ended it, and `direction` when known — plus `archivedCount` and `archivedSplit` (count per outcome).
226
+
227
+ ## Stages (visualization surfaces)
228
+
229
+ Two full-page stages mount beside the model tools: `/__dsh_a2a_nexus` (the
230
+ A2A workbench — since 0.6.2 the 2D spatial planning board IS the stage and
231
+ the directory/ledger/activity/plan panels ride a collapsible side rail in
232
+ one merged, responsive shell; while the canvas face is down the panels fall
233
+ back to the main stack; the Three.js topology viewer was retired in 0.6.0;
234
+ `?mode=plan` still converges on the board) and `/__dsh_a2a_canvas`. A bare
235
+ mount path
236
+ redirects (301) onto its trailing-slash form, so both spellings render and
237
+ the shell's relative assets always resolve inside the served tree. For
238
+ offline development, `nexus-stage/scripts/mockhost.py [port]` serves the
239
+ built stage with canned state; open `/?fault=500` on it to exercise the
240
+ on-page fault badge. `pnpm verify` needs no manual bootstrap on a fresh
241
+ checkout: its `verify:nexus` step installs the subpackage dependencies
242
+ itself before building.
243
+
244
+ ## Collaboration SOP (network practice, v0.1)
245
+
246
+ > Current rules: the A2A collaboration norm ([`src/skill/references/collaboration.md`](src/skill/references/collaboration.md), in Chinese, shipped with the plugin to every node). The v0.1 SOP below is kept as history; where they conflict, the norm wins.
247
+
248
+ Distilled from the 0.1.x–0.5.x delivery cycles by the network's research node; adopted by the maintainer.
249
+
250
+ - **Roles** — one session, one office, no crossing workspace boundaries: a research node (protocol/design
251
+ review, verdict criteria, source-line references with must-fix/recommend labels), a maintainer node
252
+ (version rulings, releases, adoption receipts), a test node (the single execution point for all live-host
253
+ verification; production hosts are never used for ad-hoc testing), and an install/ops node (production
254
+ profile deployment, single-owner, waits for the maintenance window).
255
+ - **Dispatch discipline** — test tasks go to the test node with version + commit + decidable criteria +
256
+ receipt target; review tasks go to the research node with source pointers, not paraphrases.
257
+ - **Receipt contract** — tasks longer than minutes route with `async: true` (wait:false); receipts read
258
+ `[A2A receipt] task <task_id> <outcome summary>`; the task id is caller-born, request-carried,
259
+ peer-echoed, and steered-header-transmitted — four-way agreement before a receipt correlates.
260
+ - **Upgrade chain** — test node green → maintainer receipt → ops node requests the window → production
261
+ upgrade → maintainer's browser-face final check → network-wide notice.
262
+ - **Reuse red lines** — shared pure logic extracts to a library; shared runtime capability extracts to a
263
+ Service; plugins never value-import each other; production hosts carry no verification duty (no
264
+ dev-who-self-tests).
265
+
266
+ ## Verify
267
+
268
+ Tests are layered by depth (contract: `docs/testing-layers.md`): each layer
269
+ has its own directory and script, so iteration can target one depth.
270
+
271
+ ```sh
272
+ pnpm install # auto-install-peers pulls the DSH peer train from npm
273
+ pnpm verify # typecheck + build + ALL vitest layers below + layout gate + nexus-stage suite
274
+ pnpm test:unit # L0 — pure logic, no fiber, no sockets (stores, codecs, jsdom components)
275
+ pnpm test:wire # L1 — ONE real plugin instance over real HTTP against faked peers
276
+ pnpm test:mesh # L2 — >=2 REAL nodes (own homes/keys/peers.json) over real HTTP: the
277
+ # cross-machine equivalent, incl. fault injection (clock-skew cards,
278
+ # tampered cards, partition/restart, alias collapse, zone delegation,
279
+ # referral freeze) and receipt replay — see the fault matrix in
280
+ # docs/testing-layers.md
281
+ pnpm test:live # L4 — REAL remote machines, env-gated (DSH_A2A_LIVE_MESH=...); skipped
282
+ # by default, never in CI — runbook in docs/testing-layers.md
283
+ pnpm build # host lib (including lib/a2a-client.js), client types, and browser bundle (lib/client.js)
284
+ ```
285
+
286
+ ## Non-DSH interop (protocol modules)
287
+
288
+ A node that is NOT a DeepSeek Harness host can still speak the protocol. The
289
+ four dependency-free protocol modules are exported subpaths (gh#13) — import
290
+ them without pulling the DSH peer train:
291
+
292
+ ```js
293
+ import { signCard, verifyCard } from '@nelsonlongxiang/dsh-open-a2a-net/card'
294
+ import { formatReceipt, parseReceipt } from '@nelsonlongxiang/dsh-open-a2a-net/receipt'
295
+ import { withinRouteBodyCap } from '@nelsonlongxiang/dsh-open-a2a-net/transport-caps'
296
+ import { A2aClient } from '@nelsonlongxiang/dsh-open-a2a-net/a2a-client'
297
+ ```
298
+
299
+ Implement your own signed card, `GET /.well-known/agent-card.json`, and
300
+ `POST /a2a/direct`. The wire shapes these modules encode are the contract.
301
+ Anonymous card fetches get the minimal discovery face. The full card needs a
302
+ verified OriginClaim envelope (`X-A2A-Caller` / `-Url` / `-Ts` / `-Sig`,
303
+ signed with your own card key) **and** a link to the serving host: your card
304
+ URL shares its origin, your handle is one of its joined session handles, its
305
+ operator added your exact handle as a canvas remote member, or both operators
306
+ seeded each other (its `peers` names your origin and your card's `peers` names
307
+ its origin). A verified but unlinked caller still gets the minimal card. See
308
+ `docs/protocol/delivery-origin-auth.md` and the rest of `docs/protocol/` for
309
+ the envelope details.
310
+
311
+ **Which artifact to build against.** npm direct install already includes the
312
+ four subpaths: releases from 0.6.25 (badge `latest`) carry them, so
313
+ `npm install @nelsonlongxiang/dsh-open-a2a-net` works out of the box —
314
+ see `examples/external-node/` for a runnable external-node example that
315
+ exercises all four subpaths against an embedded mock peer. The subpath
316
+ exports landed in commit `9cb3852`; versions before that (public 0.5.15
317
+ included) predate the admission rules and the OriginClaim envelope above,
318
+ and importing the subpaths from them fails with
319
+ `ERR_PACKAGE_PATH_NOT_EXPORTED` — the historical recipe (pin a commit,
320
+ `pnpm build`, `npm pack`, depend on the tarball) survives below for
321
+ pre-0.6.25 deployments only:
322
+
323
+ ```bash
324
+ # HISTORICAL (0.5.15 and earlier): build the package from a pinned commit
325
+ git clone https://github.com/NelsonLongxiang/dsh-open-a2a-net && cd dsh-open-a2a-net
326
+ git checkout <commit> # pin one; any commit at or after 9cb3852 exports the subpaths
327
+ pnpm install --frozen-lockfile # pnpm version from package.json "packageManager"
328
+ pnpm build
329
+ npm pack # → nelsonlongxiang-dsh-open-a2a-net-<version>.tgz
330
+ # in your project:
331
+ npm install /path/to/nelsonlongxiang-dsh-open-a2a-net-<version>.tgz
332
+ ```
333
+
334
+ Installing the package (npm or the historical tarball) pulls no
335
+ `@deepseek-ai/*` package: the DSH peers are optional and the four
336
+ subpaths import only each other and `node:crypto`.
337
+
338
+ ## License
339
+
340
+ MIT