@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
@@ -1,4 +1,5 @@
1
- import { type TaskLedger } from './task-ledger.ts';
1
+ import { type TaskLedger, type TaskRecord } from './task-ledger.ts';
2
+ import { type TaskAuthorization } from './task-auth.ts';
2
3
  import type { A2aPeerCard, A2aRouteResult } from './types.ts';
3
4
  export interface ForwardRequest {
4
5
  team: string;
@@ -9,12 +10,27 @@ export interface ForwardRequest {
9
10
  noWait: boolean;
10
11
  localCliProof: boolean;
11
12
  guarded: boolean;
13
+ auth?: TaskAuthorization;
14
+ }
15
+ /** An early receipt waits at most this long for its forward row to be born (#298). */
16
+ export declare const BIRTH_WAIT_CAP_MS = 60000;
17
+ /** Birth barrier exists only for eligible, actively dispatched tasks; receipts never reserve a slot. */
18
+ export declare class ForwardBirths {
19
+ private readonly waitCapMs;
20
+ private readonly pending;
21
+ constructor(waitCapMs?: number);
22
+ /**
23
+ * Resolves when the birth is released or after the cap, whichever is first. After the cap the receipt
24
+ * is judged by the normal rules against whatever row exists then — never accepted in advance (#298).
25
+ */
26
+ wait(taskId: string): Promise<void> | undefined;
27
+ begin(taskId: string): (() => void) | undefined;
12
28
  }
13
29
  export interface DirectForwardDeps {
30
+ births: ForwardBirths;
14
31
  enabled(): boolean;
15
32
  isolated(): boolean;
16
33
  deposited(handle: string): boolean;
17
- memberLinked(handle: string): boolean;
18
34
  scope(handle: string, team: string): Promise<{
19
35
  ok: true;
20
36
  } | {
@@ -30,3 +46,18 @@ export interface DirectForwardDeps {
30
46
  activity(team: string, peer: string, ok: boolean): void;
31
47
  }
32
48
  export declare function createDirectForward(d: DirectForwardDeps): (request: ForwardRequest) => Promise<A2aRouteResult>;
49
+ /**
50
+ * #282 S3 (same team authorizes): the admission basis for a receipt delivered to a local CLI mailbox
51
+ * from a member the home never linked — only for that CLI's own pending forward, only from a receiver
52
+ * frozen at birth, only when the body-bound (Sig3) claim's card key is the frozen peer key. Anything else:
53
+ * undefined. Trust assumption (design D2): the selected owner host is honest about its own canvas — it may
54
+ * list any label as a receiver, including one of this host's session labels in the same zone.
55
+ */
56
+ export declare function forwardReceiptBasis(row: TaskRecord | undefined, caller: string, cliHandle: string, claim: {
57
+ readonly caller: string;
58
+ readonly bound: boolean;
59
+ readonly bodyBound?: true;
60
+ readonly card: {
61
+ readonly publicKey: string;
62
+ };
63
+ }): string | undefined;
@@ -70,6 +70,10 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
70
70
  }>;
71
71
  readonly layoutStore: LayoutStore;
72
72
  readonly taskLedger: TaskLedger;
73
+ readonly enforceRefused?: {
74
+ reason: string;
75
+ blockers: readonly string[];
76
+ };
73
77
  /** #114 S6: observe (lazily) and return the /state `authorization` block. */
74
78
  readonly authorizationView: () => unknown;
75
79
  readonly idempotencyStore: IdempotencyStore;
@@ -140,7 +144,15 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
140
144
  readonly legacyInboundCount: number;
141
145
  readonly unboundInboundCount: number;
142
146
  readonly boundMissInboundCount: number;
147
+ readonly boundMissRecent: ReadonlyArray<{
148
+ caller: string;
149
+ audience: string;
150
+ count: number;
151
+ lastAt: number;
152
+ }>;
143
153
  readonly ownCardUrl: string;
154
+ /** #282 S2: this host's canvas owner fingerprint. */
155
+ readonly ownCanvasFingerprint: string;
144
156
  ownerSeatNames: Record<string, string>;
145
157
  ownerAllowlist: string[];
146
158
  ownerTeamScopeInfo: boolean;
@@ -111,6 +111,7 @@ export type OriginClaimResult = {
111
111
  card: A2aPeerCard;
112
112
  bound: boolean;
113
113
  cli?: true;
114
+ bodyBound?: true;
114
115
  } | {
115
116
  ok: false;
116
117
  reason: string;
@@ -131,9 +132,16 @@ export interface WireDeps {
131
132
  readonly rawBody?: Uint8Array;
132
133
  }) => Promise<OriginClaimResult>;
133
134
  readonly forwardDirect: (request: ForwardRequest) => Promise<A2aRouteResult>;
135
+ /** #282 S2: this host's canvas owner fingerprint. */
136
+ readonly ownCanvasFingerprint: string;
137
+ readonly diagnoseCanvas: (team: string, reason: string, caller?: string) => Promise<string>;
134
138
  readonly remoteCliProof: (handle: string, claim: Extract<OriginClaimResult, {
135
139
  ok: true;
136
140
  }>) => Promise<RemoteCliProof | undefined>;
141
+ /** #282 S3: the basis for a receipt to a local CLI from a member frozen on this host's own forward, else undefined. */
142
+ readonly forwardReceiptBasis: (taskId: string, caller: string, cliHandle: string, claim: Extract<OriginClaimResult, {
143
+ ok: true;
144
+ }>) => string | undefined;
137
145
  readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
138
146
  /** This host's own card URL (read when a request lands; set once the web server settles). */
139
147
  readonly ownCardUrl: string;
@@ -169,6 +177,7 @@ export interface WireDeps {
169
177
  /** gh#224: refuses a new delivery once the target already owes `targetQueueCap` receipts. */
170
178
  readonly refuseIfQueueFull: (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
171
179
  /** Settles a receipt; returns the enforce refusal of the row its header names (#139), if any. */
180
+ readonly waitForForwardBirth: (message: string) => Promise<void>;
172
181
  readonly settleAndAnnounce: (message: string, settler?: Settler) => SettlementRefusal | undefined;
173
182
  readonly touchWakeDemand: () => void;
174
183
  readonly session: string;
@@ -199,7 +208,7 @@ export interface WireDeps {
199
208
  readonly nativeTeamsRound: (prepared: Extract<NativePrepared, {
200
209
  ok: true;
201
210
  }>, team: string, caller: string, message: string, taskId: string | undefined, proofClause: string, external?: AbortSignal, proofHeader?: (team: string, taskId: string, from: string, proofClause: string) => string) => Promise<NativeRoundOutcome>;
202
- readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string, proofClause?: string, directHeader?: (from: string) => string) => Promise<RouteOutcome>;
211
+ readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string, proofClause?: string, directHeader?: (from: string) => string, localFrom?: string, onSteered?: (target: Agent, floor: number) => void) => Promise<RouteOutcome>;
203
212
  }
204
213
  /**
205
214
  * #187: what a verified claim may vouch for at settlement. The signing key always (the card was
@@ -20,9 +20,7 @@ export declare function resolveCallerLabel(input: {
20
20
  }): string | undefined;
21
21
  import { type ReceiptResolvedInfo } from './task-ledger.ts';
22
22
  import { type AffectedEntry } from './auth-observer.ts';
23
- import { type SettlementMode } from './task-auth.ts';
24
23
  import type { Context } from '@deepseek-ai/cordis';
25
- import s from '@deepseek-ai/schemastery';
26
24
  declare module '@deepseek-ai/dsh-llm' {
27
25
  interface MessageSourceMap {
28
26
  'a2a-relay': {
@@ -50,281 +48,9 @@ declare module '@deepseek-ai/cordis' {
50
48
  }
51
49
  export declare const name = "a2a";
52
50
  export declare const inject: string[];
53
- /** One signed zone delegation published on this node's agent card. */
54
- export interface DelegateConfig {
55
- /** The delegated name; resolves zone-relatively at route time. */
56
- readonly name: string;
57
- /** Base URL of the zone the name resolves in. */
58
- readonly url: string;
59
- /** Base64 SPKI DER binding to the target zone's card key; `''` leaves the delegation unbound. */
60
- readonly publicKey: string;
61
- }
62
- /** Deployment-varying A2A node facts; every field has a declared default. */
63
- export interface Config {
64
- /** `X-API-Key` value sent on peer requests and required by the control routes; empty disables the header. */
65
- readonly apiKey: string;
66
- /**
67
- * This node's caller label. `''` (the default) derives `dsh-host-<8 hex>`
68
- * from a per-home node id created on first use, so two deployments mounting
69
- * defaults do not collide while a persisted home keeps the label stable
70
- * across restarts.
71
- */
72
- readonly session: string;
73
- /** Team name this node exposes for direct routing. */
74
- readonly team: string;
75
- /** S4 fix 3: audit (default) or refuse host-side (agent-less) routing calls. */
76
- readonly gateAgentLessCalls: 'audit' | 'refuse';
77
- /** #114 S4: settlement authorization — off, observe (record would-refuse, settle as before), enforce. */
78
- readonly settlementAuth: SettlementMode;
79
- /**
80
- * Publish this node's `/.well-known/agent-card.json` on the shared web
81
- * server (peer discovery). Requires the web server in the composition;
82
- * the card carries this node's team, capabilities, referral list, and
83
- * joined session teams so peers find it without a central directory.
84
- */
85
- readonly announce: boolean;
86
- /**
87
- * gh#193: send nothing to any peer. Every outbound peer request — card
88
- * reads, directory sweeps, routes, log reads, canvas pings and canvas-op
89
- * relays — goes through one fetch that refuses while this is on, so a
90
- * test host stays dark even with learned peers in `a2a/peers.json`
91
- * (which is left untouched). Inbound faces keep answering. Default false.
92
- */
93
- readonly isolated: boolean;
94
- /** Forward online local CLI deliveries one hop after a local miss. Default off. */
95
- readonly forwardDirect: boolean;
96
- /** Human-facing name published on the agent card. */
97
- readonly agentName: string;
98
- /**
99
- * Base URLs of seed peer nodes. `a2a_teams` lists teams discovered from
100
- * the peers' agent cards and `a2a_route` connects to the owning peer's
101
- * `/a2a/direct` endpoint. The list is a seed set: referral URLs learned
102
- * from fetched cards join a bounded, quality-scored peer store persisted
103
- * at `<dsh-home>/a2a/peers.json`.
104
- */
105
- readonly peers: string[];
106
- /**
107
- * Zone delegation records published (signed) on this node's agent card:
108
- * each entry publishes `name` as resolvable in the zone at `url`, so a
109
- * route for `name` walks the delegation chain (at most 5 hops, cycles
110
- * fail closed).
111
- */
112
- readonly delegates: DelegateConfig[];
113
- /**
114
- * gh#9+gh#10 OriginClaim window (docs/protocol/delivery-origin-auth.md §4):
115
- * while true (default), a delivery/log request with NO envelope still
116
- * walks N3 admission and increments the legacyInbound warn metric; when
117
- * the operator closes it (post fleet flip), an absent envelope rejects
118
- * `untrusted-origin` — same code as a failed signature (M1: a
119
- * self-asserted already-linked name must not pass on shape alone).
120
- */
121
- readonly originClaimWindow: boolean;
122
- /**
123
- * OriginClaim unbound window (docs/protocol/delivery-origin-auth.md §2.1):
124
- * while true (default), this host also signs the legacy unbound form and
125
- * accepts a peer's unbound-only envelope (counted as unboundInbound);
126
- * closed, it signs and accepts the audience-bound form only.
127
- */
128
- readonly originClaimUnboundWindow: boolean;
129
- /**
130
- * Extra origins this host answers to as an OriginClaim audience, beyond
131
- * its card URL and the address a request arrives on (port-mapped or
132
- * proxied hosts name the URL peers dial).
133
- */
134
- readonly originClaimAudiences: string[];
135
- /**
136
- * #19 D2: card-face sessionTeams excerpt exposure — 'full' | 'masked'
137
- * | 'off' (owner-level; default full). See the schema comment.
138
- */
139
- readonly exposureExcerpt: 'full' | 'masked' | 'off';
140
- /**
141
- * Override for the OriginClaim callerUrl anchor. Loopback-bound hosts
142
- * behind a pocket tunnel must name the card URL peers can actually
143
- * fetch (e.g. http://192.168.3.157:13080); empty uses the web server's
144
- * own address.
145
- */
146
- readonly originClaimUrl: string;
147
- /**
148
- * gh#8 (N9) escape hatch: when true, team-name resolution also considers
149
- * the referral-learned peer tail (the legacy whole-store basis) instead
150
- * of the seeded/delegate subset only. Default false — a team name binds
151
- * to hosts the operator linked; fleets relying on referral discovery for
152
- * routing migrate by adding explicit seeds, then leave this off.
153
- */
154
- readonly referralTeamResolution: boolean;
155
- /**
156
- * Harness home slice for the node keypair and peer store; `''` (the
157
- * default) uses the process DSH home. The Ed25519 key lives at
158
- * `<home>/a2a/node-key.pem` and is generated on first announce; a
159
- * persisted key keeps the node's card identity stable across restarts.
160
- */
161
- readonly dshHome: string;
162
- /**
163
- * Agent-card validity. Announce re-signs at TTL/4 so three lost refreshes
164
- * still leave a live card; a node silent for one TTL disappears from every
165
- * peer's discovery without manual eviction.
166
- */
167
- readonly cardTtlMs: number;
168
- /**
169
- * Enable per-session A2A nodes: each live top-level session can join the
170
- * network explicitly — label `<session>-<agentId8>`, team
171
- * `<team>/<agentId8>`, node facts carrying the session title and a
172
- * recent-activity excerpt. Joining is opt-in through the sidebar control's
173
- * host routes; joined teams ride this node's card and dispatch through
174
- * its `/a2a/direct` endpoint.
175
- */
176
- readonly sessionNodes: boolean;
177
- /**
178
- * Prewarm every cold joined session's agent after the plugin mounts (dev
179
- * boxes and always-on collaboration hosts that restart often: the network
180
- * state returns without opening each session). Each wake materializes a
181
- * full agent — a main-thread log replay — so the prewarm is deferred,
182
- * foreground-yielding, and cancellable (see `wakePrewarmDelayMs` /
183
- * `wakePrewarmQuietMs`); the default stays off for deployments that would
184
- * rather pay on demand (wake-on-route and the sidebar's wake button remain
185
- * available either way).
186
- */
187
- readonly wakeJoinedOnBoot: boolean;
188
- /**
189
- * Idle delay between loader settlement and the first boot prewarm wake:
190
- * boot traffic gets a clear window before any log replay starts. `0`
191
- * restores the old fire-at-settle behavior.
192
- */
193
- readonly wakePrewarmDelayMs: number;
194
- /**
195
- * Foreground quiet window: a wake/route demand inside this window (or any
196
- * outbound route still in flight) postpones the next prewarm step. Panel
197
- * polls never count. `0` disables the yield.
198
- */
199
- readonly wakePrewarmQuietMs: number;
200
- /**
201
- * Pause between consecutive boot prewarm wakes. Each wake replays a full
202
- * session log — the zstd decode yields the event loop only every 500ms —
203
- * so unbounded concurrent wakes of several huge logs starve every request
204
- * for tens of seconds after a restart. The serial queue with this pause
205
- * keeps the preheat while capping decode saturation, and wake-on-route
206
- * never waits on this queue.
207
- */
208
- readonly wakeBootStaggerMs: number;
209
- /**
210
- * F8: continuous desired-state reconciliation. The boot prewarm is a
211
- * one-shot queue; the reconciler re-derives the due set every tick and
212
- * wakes one cold intent per pass — covering post-boot drift, failed
213
- * wakes, and gateways that mounted late. Default on.
214
- */
215
- readonly wakeReconcile: boolean;
216
- /** Cadence between reconciliation ticks (floored at 50ms). */
217
- readonly wakeReconcileIntervalMs: number;
218
- /** First-retry delay after a failed reconcile wake; doubles per attempt. */
219
- readonly wakeReconcileBackoffBaseMs: number;
220
- /** Backoff ceiling; corrupt-log rows park as needs-repair and never retry. */
221
- readonly wakeReconcileMaxBackoffMs: number;
222
- /**
223
- * S4 (gh#11 N7, operator directive: team creation is a human act): the
224
- * joinable-team curation list for a2a_team_join. ONLY an EXACT entry
225
- * authorizes a join; trailing-`*` pattern entries stay readable here
226
- * (whoami shows them) but carry no mint rights — a pattern that
227
- * pre-authorized every future name under it would let a model mint teams
228
- * nobody created. Empty list denies every join (default).
229
- */
230
- readonly teamJoinAllowlist: string[];
231
- /** Human-owned stable display labels keyed by full local session id; never a routing identity. */
232
- readonly seatNames: Record<string, unknown>;
233
- /**
234
- * S3 (phase-2 default-ON, owner ruling 2026-09-01): route dispatch checks
235
- * caller↔target shared team membership before addressing; set false to
236
- * restore the open outbound seam. (Kept in sync with the schema comment.)
237
- */
238
- readonly teamScopeRouting: boolean;
239
- /**
240
- * S5 (owner directive 2026-10-20): information-plane scoping — the
241
- * read-only fleet tools (a2a_teams / a2a_status / a2a_probe) disclose
242
- * only teams joined by this host's nodes (the union of local roster
243
- * declarations). [this host] rows stay visible; a2a_tasks (this host's
244
- * own ledger) stays unscoped. Set false for the open plane.
245
- */
246
- readonly teamScopeInfo: boolean;
247
- /**
248
- * Peer boundary model (docs/design/peer-boundary-model.md) — connection
249
- * layer: an empty list keeps the historical open-referral posture; a
250
- * non-empty list admits a referral only when its host matches an entry
251
- * (exact host[:port], bare domain suffix, or full URL). Seeds from
252
- * `peers` are owner-configured and unaffected.
253
- */
254
- readonly peerAllowlist: string[];
255
- /**
256
- * Peer boundary model — connection layer: when false, referral URLs are
257
- * neither learned nor offered; the tracked set stays the configured seed
258
- * set for the lifetime of the config that named them.
259
- */
260
- readonly referralLearning: boolean;
261
- /**
262
- * v0.5.23 (async-stall): see the schema comment on asyncNudgeDelayMs.
263
- */
264
- readonly asyncNudgeDelayMs: number;
265
- /** gh#224: receipt-owing tasks one target may queue on this host before new deliveries are refused (0 = off). */
266
- readonly targetQueueCap: number;
267
- /**
268
- * How long the state route serves the cold-row id set from cache before
269
- * re-enumerating the persistence layer. The panel polls every 2s; the
270
- * default keeps one full enumeration per 5s instead of one per poll.
271
- */
272
- readonly stateColdRowsTtlMs: number;
273
- /**
274
- * Serve window for a verified peer card from the shared cache. Cards are
275
- * signed for days and re-signed at TTL/4, so 60s is semantically safe; a
276
- * peer that restarts with a fresh card is picked up on the next window.
277
- */
278
- readonly cardCacheTtlMs: number;
279
- /** Shorter window for unreachable/invalid cards, so a revived peer reappears quickly. */
280
- readonly cardCacheNegativeTtlMs: number;
281
- /**
282
- * Serve window for the panel\x27s remote rows. The refresh itself rides the
283
- * shared card cache, so this is pure sweep cadence: how often real
284
- * network activity happens while the panel polls every 2s.
285
- */
286
- readonly remoteRowsTtlMs: number;
287
- /**
288
- * `final` reply budget: how long an inbound direct route waits for the
289
- * session's next assistant message before answering with a timeout
290
- * notice. Without it a session that never idles (or never produces text)
291
- * leaves the caller hanging until `routeTimeoutMs` expires.
292
- */
293
- readonly flushTimeoutMs: number;
294
- /**
295
- * `a2a_route` execution budget: how long a call may plausibly wait for a
296
- * remote session's reply (default 30 minutes).
297
- */
298
- readonly routeTimeoutMs: number;
299
- /**
300
- * Age at which an unsettled async task auto-dead-letters (exits the owed
301
- * view, disarms the nudge, still settles on a late receipt). Default
302
- * 24h; the mesh anomaly suite injects sub-second windows to exercise the
303
- * dead-letter → revival lifecycle without wall-clock waits.
304
- */
305
- readonly taskStaleTtlMs: number;
306
- /**
307
- * Opt-in native-teams inbound bridge: when composed with
308
- * `@nelsonlongxiang/dsh-native-teams`, a routed team name that its
309
- * registry classifies as an unambiguous local claim dispatches through
310
- * its authoritative routing seam (`/a2a/direct`, the outbound A2A tools,
311
- * and the directory listing). Off by default — exposing every registered
312
- * team to the network is an operator decision (exposure-grants
313
- * governance: grants are deliberate, never ambient). Dispatcher-level
314
- * only: inbound callers address the team, never its individual members
315
- * (members stay visible-not-addressable).
316
- */
317
- readonly nativeTeamsInbound?: boolean;
318
- /**
319
- * Reply-wait budget for one native-teams round (the bridge's answer
320
- * deadline, mirroring the steer path's 180s deadline). A round still
321
- * running past it answers the honest delivered-unsettled shape instead
322
- * of parking the caller; the round itself keeps going.
323
- */
324
- readonly nativeRoundWaitMs?: number;
325
- }
326
- /** Schemastery configuration for the A2A client plugin row. */
327
- export declare const Config: s<Config>;
51
+ import { Config } from './config.ts';
52
+ export { Config };
53
+ export type { DelegateConfig } from './config.ts';
328
54
  /**
329
55
  * Register the outbound peer tools, the direct-route endpoint, and (when
330
56
  * composed) the announced card and session-node control surface.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * #291: live top-level agents across harness generations.
3
+ *
4
+ * Hosts up to 0.2.1 expose `AgentRegistry.roots()` — "all live top-level
5
+ * agents (created without an owning agent context)". 0.2.2-rc removed it:
6
+ * live lineage moved to the subagent tree and the registry keeps only
7
+ * `list()` (all live agents). Top-level is then the delegation-depth
8
+ * predicate of `@deepseek-ai/dsh-subagent/depth` `delegationDepthOf` —
9
+ * depth 0 is top-level, absence is top-level. That package is not a
10
+ * plugin-provided dependency, so its read is mirrored here.
11
+ *
12
+ * Nothing in this module throws. The `agent/created` listener runs at
13
+ * session publication and a throwing listener vetoes the publication — the
14
+ * 2026-11-05 session-resume incident (history sessions vanished from the
15
+ * list until refresh). An unreadable or throwing registry surface degrades
16
+ * to an empty join surface, never to a broken session list. Unlike the
17
+ * harness' own `delegationDepthOf`, invalid depth values read as absent
18
+ * instead of throwing, for the same reason.
19
+ */
20
+ import type { Agent, AgentRegistry } from '@deepseek-ai/dsh-agent';
21
+ /**
22
+ * Delegation depth of a live agent: 0 = top-level. Mirrors the harness'
23
+ * authoritative max(persisted session header, runtime options); invalid
24
+ * values read as absent (never throwing) — see the module comment.
25
+ */
26
+ export declare function delegationDepthOf(agent: Agent): number;
27
+ /** Whether a live agent is top-level (not inside any subagent tree). */
28
+ export declare function isTopLevelAgent(agent: Agent): boolean;
29
+ /**
30
+ * Live top-level agents on this host: `roots()` where the host still has
31
+ * it (exact pre-0.2.2 semantics, so old hosts change nothing), else
32
+ * `list()` filtered by delegation depth. A missing registry, a surface
33
+ * with neither method, or a throwing method all degrade to `[]`.
34
+ */
35
+ export declare function liveRootAgents(registry: AgentRegistry | undefined | null): Agent[];
@@ -56,6 +56,7 @@ export type OriginClaimVerdict = {
56
56
  card: A2aPeerCard;
57
57
  bound: boolean;
58
58
  cli?: true;
59
+ bodyBound?: true;
59
60
  } | {
60
61
  ok: false;
61
62
  reason: OriginClaimRefusal;
@@ -117,7 +118,10 @@ export declare function verifyOriginClaim(req: IncomingMessage, d: {
117
118
  /** Count one accepted unbound envelope (the drain gauge). */
118
119
  readonly onUnbound: () => void;
119
120
  /** Count one bound form that missed and fell back to the unbound one. */
120
- readonly onBoundMiss?: () => void;
121
+ readonly onBoundMiss?: (miss: {
122
+ readonly caller: string;
123
+ readonly audience: string;
124
+ }) => void;
121
125
  /** A loopback arrival is an audience of its own (default); off when originClaimUrl declares a tunnel or proxy. */
122
126
  readonly loopbackArrival?: boolean;
123
127
  readonly now: number;
@@ -29,5 +29,7 @@ export interface QueueRow {
29
29
  export declare function queuedFor(rows: readonly QueueRow[], team: string): number;
30
30
  /** The refusal text when `team`'s queue is full, else undefined. `cap <= 0` disables the guard. */
31
31
  export declare function queueFullRefusal(rows: readonly QueueRow[], team: string, message: string, cap: number, mailbox: boolean, ownTask: (taskId: string) => boolean): string | undefined;
32
+ /** #282 S1: the one next step for a full target queue. */
33
+ export declare const QUEUE_FULL_NEXT = "wait for a receipt from the target (a2a-net inbox / a2a_tasks), then send again with a new task id; do not resend in a loop";
32
34
  /** Wire-face adapter in the flood guard's shape: a refused fresh key is FAILED, not stranded pending. */
33
35
  export declare function createQueueGuard(rows: () => readonly QueueRow[], cap: () => number, ownTask: (taskId: string) => boolean, outcomes: Pick<IdempotencyStore, 'recordOutcome'>, recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void): (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
@@ -17,9 +17,12 @@ export type Admission = {
17
17
  } | {
18
18
  readonly ok: false;
19
19
  readonly refusal: string;
20
+ readonly next?: string;
20
21
  };
21
22
  /** The refusal text of a decision, or undefined when admitted — the shape the faces have always read. */
22
23
  export declare const refusalOf: (admission: Admission) => string | undefined;
24
+ /** The next step of a refused decision, or undefined. */
25
+ export declare const nextOf: (a: Admission) => string | undefined;
23
26
  /** One cached peer card's published canvas org (gh#71). */
24
27
  export interface PeerOrgEntry {
25
28
  readonly team?: unknown;
@@ -232,6 +235,8 @@ export interface SettlementRow {
232
235
  readonly peerHost?: string;
233
236
  /** #139: the member that actually received the task (canvas and bare-zone deliveries). */
234
237
  readonly deliveredTo?: string;
238
+ readonly forwarded?: true;
239
+ readonly receiverMembers?: readonly string[];
235
240
  }
236
241
  /** Facts from outside the row. */
237
242
  export interface SettlementFacts {
@@ -96,31 +96,36 @@ export declare function withAuth(raw: unknown, entry: {
96
96
  direction?: unknown;
97
97
  team?: unknown;
98
98
  peer?: unknown;
99
+ forwarded?: unknown;
100
+ sender?: unknown;
99
101
  }, now: number): {
100
102
  auth?: TaskAuthorization;
101
103
  };
102
104
  /** Routing and answer facts copied unchanged between a row and its archive entry. */
103
- export declare function routingOf(row: Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt'>): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt'>;
105
+ export declare function routingOf(row: Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>;
104
106
  /** #139: what an outbound row is born with besides its sender. */
105
107
  export interface OutboundBirth {
106
108
  readonly forwarded?: true;
107
- /** The local-candidate admission (ingress local); kept only on a row whose peer is 'local'. */
109
+ /** Local-candidate admission, or a forwarded online CLI's verified wire admission. */
108
110
  readonly auth?: TaskAuthorization;
109
111
  readonly peerKey?: string;
110
112
  readonly peerHost?: string;
111
113
  readonly deliveredTo?: string;
114
+ /** Verified selected peer canvas membership at dispatch; actual deliveredTo remains stricter when available. */
115
+ readonly receiverMembers?: readonly string[];
112
116
  }
113
117
  /** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
114
- export declare function birthFacts(peer: string, birth: OutboundBirth | undefined): Pick<TaskRecord, 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded'>;
118
+ export declare function birthFacts(peer: string, birth: OutboundBirth | undefined, team?: string, sender?: Sender, now?: number, warn?: (message: string) => void): Pick<TaskRecord, 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'receiverMembers'>;
115
119
  /** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
116
120
  export declare const ROUTING_FACT_CAP = 256;
121
+ export declare function parseReceiverMembers(value: unknown, warn?: (message: string) => void): readonly string[] | undefined;
117
122
  /**
118
123
  * #139: restore-side allowlist for the routing facts. Each is kept only in
119
124
  * its shape — a non-empty string within the cap — and the peer facts only on
120
125
  * a remote outbound row; anything else is dropped (the row survives as one
121
126
  * born before the fields, judged by current membership and origin).
122
127
  */
123
- export declare function withRouting(entry: Record<string, unknown>): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt'>;
128
+ export declare function withRouting(entry: Record<string, unknown>, warn?: (message: string) => void): Pick<TaskRecord, 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'answeredAt' | 'receiverMembers'>;
124
129
  /** One tracked outbound task still in the owed book (unsettled). */
125
130
  export interface TaskRecord {
126
131
  readonly taskId: string;
@@ -164,6 +169,8 @@ export interface TaskRecord {
164
169
  readonly peerHost?: string;
165
170
  /** #139: the member that actually received the task (canvas / bare-zone deliveries). */
166
171
  readonly deliveredTo?: string;
172
+ /** Verified selected peer canvas membership at dispatch; actual deliveredTo remains stricter when available. */
173
+ readonly receiverMembers?: readonly string[];
167
174
  /** Local target has produced an answer; still owed until receipt delivery/settlement, but no longer queued. */
168
175
  answeredAt?: number;
169
176
  status: TaskStatus;
@@ -221,6 +228,8 @@ export interface ArchivedRecord {
221
228
  readonly peerKey?: string;
222
229
  readonly peerHost?: string;
223
230
  readonly deliveredTo?: string;
231
+ /** Verified selected peer canvas membership at dispatch; actual deliveredTo remains stricter when available. */
232
+ readonly receiverMembers?: readonly string[];
224
233
  /** Local answer time retained as audit metadata, independent of resolution time. */
225
234
  readonly answeredAt?: number;
226
235
  /** #114 S4: who settled the row (receipt and echo ends), and their chain role. */
@@ -252,6 +261,8 @@ export interface CancelledResult extends AbandonResult {
252
261
  }
253
262
  /** Per-instance knobs; defaults come from the exported constants. */
254
263
  export interface TaskLedgerOptions {
264
+ /** Invalid persisted member entries are dropped individually and reported without their content. */
265
+ warn?: (message: string) => void;
255
266
  /** Stale-TTL override (tests inject short windows); defaults to {@link TASK_STALE_TTL_MS}. */
256
267
  staleTtlMs?: number;
257
268
  /** Archive capacity override; defaults to {@link ARCHIVE_CAP}. */
@@ -331,7 +342,7 @@ export declare const SETTLEMENT_REFUSAL_CAP = 64;
331
342
  export interface SettlementJudge {
332
343
  readonly enforce: boolean;
333
344
  readonly settler: SettlementRefusalRecord['settler'];
334
- check(row: Pick<TaskRecord, 'team' | 'peer' | 'direction' | 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo'>, echo: boolean): SettlementVerdict;
345
+ check(row: Pick<TaskRecord, 'team' | 'peer' | 'direction' | 'auth' | 'peerKey' | 'peerHost' | 'deliveredTo' | 'forwarded' | 'receiverMembers'>, echo: boolean): SettlementVerdict;
335
346
  }
336
347
  /** Restore-side allowlist for settledBy: whole or nothing. */
337
348
  export declare function withSettledBy(raw: unknown): {
@@ -44,6 +44,7 @@ export declare class TaskLedger {
44
44
  private readonly archiveCap;
45
45
  /** Injectable clock (option `now`). */
46
46
  private readonly now;
47
+ private readonly warn;
47
48
  /**
48
49
  * @param path - persistence file (`<dsh-home>/a2a/tasks.json`); empty = no persistence.
49
50
  * @param options - TTL/capacity overrides; defaults come from the exported constants.
@@ -55,6 +56,8 @@ export declare class TaskLedger {
55
56
  * @returns the owed-book records.
56
57
  */
57
58
  list(): readonly TaskRecord[];
59
+ /** #275: old forwarded canvas debts cannot be safely upgraded by inventing receiver authority. */
60
+ settlementEnforceBlockers(): string[];
58
61
  /**
59
62
  * The settled archive, most recently resolved first. Rows land here the
60
63
  * moment their receipt correlates and stay until the archive cap evicts
@@ -18,6 +18,9 @@ export type DirectoryTeamRow = {
18
18
  session: string;
19
19
  name: string;
20
20
  seatName?: string;
21
+ kind?: 'canvas';
22
+ owner?: string;
23
+ memberCount?: number;
21
24
  description: string;
22
25
  local?: boolean;
23
26
  origin?: string;
@@ -160,6 +160,8 @@ export interface A2aRouteError {
160
160
  /** A2A JSON-RPC-style code when the peer supplied one. */
161
161
  readonly code?: number;
162
162
  readonly task_id?: string;
163
+ /** One actionable next step for the caller (#282 S1); absent when nothing the caller can do helps. */
164
+ readonly next?: string;
163
165
  }
164
166
  /** Canonical `a2a_route` value: success or explicit failure. */
165
167
  export type A2aRouteResult = A2aRouteOk | A2aRouteError;