@nelsonlongxiang/dsh-open-a2a-net 0.6.28 → 0.6.30

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 (69) hide show
  1. package/README.md +87 -14
  2. package/README_zh.md +83 -7
  3. package/assets/nexusDist/assets/index-C_x3iAth.js +9 -0
  4. package/assets/nexusDist/assets/index-Dz8N80tI.css +1 -0
  5. package/assets/nexusDist/index.html +2 -2
  6. package/cli/a2a.mjs +0 -0
  7. package/cordis.patch.yml +21 -21
  8. package/lib/canvas-store.js +21 -1
  9. package/lib/client.js +64 -64
  10. package/lib/client.js.map +1 -1
  11. package/lib/faces/admin.js +755 -0
  12. package/lib/faces/canvas-notify.js +138 -0
  13. package/lib/faces/wire.js +669 -0
  14. package/lib/flood-guard.js +106 -0
  15. package/lib/index.js +200 -1392
  16. package/lib/recursion-guard.js +78 -0
  17. package/lib/runtime.js +1 -0
  18. package/lib/skill/SKILL.md +35 -9
  19. package/lib/skill/references/cli.md +24 -7
  20. package/lib/skill/references/collaboration.md +355 -0
  21. package/lib/skill/references/discovery.md +5 -2
  22. package/lib/skill/references/pitfalls.md +2 -1
  23. package/lib/skill/references/routing.md +18 -8
  24. package/lib/skill/references/setup.md +152 -0
  25. package/lib/task-ledger.js +40 -4
  26. package/lib/types/canvas-store.d.ts +10 -1
  27. package/lib/types/faces/admin.d.ts +133 -0
  28. package/lib/types/faces/canvas-notify.d.ts +74 -0
  29. package/lib/types/faces/wire.d.ts +171 -0
  30. package/lib/types/flood-guard.d.ts +29 -0
  31. package/lib/types/index.d.ts +11 -3
  32. package/lib/types/recursion-guard.d.ts +56 -0
  33. package/lib/types/runtime.d.ts +35 -0
  34. package/lib/types/task-ledger.d.ts +15 -1
  35. package/nexus-stage/package-lock.json +2262 -2262
  36. package/nexus-stage/src/canvas-ops.ts +175 -175
  37. package/nexus-stage/src/canvas-wire.ts +156 -156
  38. package/nexus-stage/src/edges.ts +72 -72
  39. package/nexus-stage/src/federation-overlay.ts +103 -103
  40. package/nexus-stage/src/federation.ts +240 -240
  41. package/nexus-stage/src/layout-doc.ts +143 -143
  42. package/nexus-stage/src/layout-wire.ts +158 -158
  43. package/nexus-stage/src/main.ts +5 -1
  44. package/nexus-stage/src/planning-card-text.ts +73 -0
  45. package/nexus-stage/src/planning-edges.ts +26 -0
  46. package/nexus-stage/src/planning-fit.ts +45 -0
  47. package/nexus-stage/src/planning-legend.ts +38 -0
  48. package/nexus-stage/src/planning-mirror.ts +31 -0
  49. package/nexus-stage/src/planning-overlap.ts +159 -0
  50. package/nexus-stage/src/planning-relayout.ts +121 -0
  51. package/nexus-stage/src/planning-search.ts +168 -0
  52. package/nexus-stage/src/planning-types.ts +1 -0
  53. package/nexus-stage/src/planning-undo.ts +68 -0
  54. package/nexus-stage/src/planning-view.ts +120 -121
  55. package/nexus-stage/src/planning.css +41 -8
  56. package/nexus-stage/src/seat.ts +35 -35
  57. package/nexus-stage/src/stage-name.ts +13 -13
  58. package/nexus-stage/src/tokens.ts +31 -31
  59. package/nexus-stage/src/viewport.ts +24 -1
  60. package/nexus-stage/src/wb-chat.ts +20 -6
  61. package/nexus-stage/src/wb-ledger.ts +20 -7
  62. package/nexus-stage/src/wb-names.ts +43 -0
  63. package/nexus-stage/src/workbench.css +8 -2
  64. package/nexus-stage/src/workbench.ts +55 -17
  65. package/nexus-stage/src/world.ts +13 -21
  66. package/package.json +216 -214
  67. package/assets/nexusDist/assets/index-1niOBYxp.js +0 -5
  68. package/assets/nexusDist/assets/index-uQf7NwjF.css +0 -1
  69. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
@@ -0,0 +1,669 @@
1
+ import { isRemoteMemberRef, validRemoteMemberRef } from "../canvas-store.js";
2
+ import { TRANSCRIPT_DEFAULT_LIMIT, TRANSCRIPT_MAX_LIMIT } from "../session-transcript.js";
3
+ import { refuseIfBudgetExhausted } from "../recursion-guard.js";
4
+ import { peerPayloadFingerprint, WIRE_ERROR_IDEMPOTENCY_CONFLICT, WIRE_ERROR_REPLAY_REJECTED } from "../idempotency-store.js";
5
+ import { briefOf } from "../task-ledger.js";
6
+ import { WIRE_ERROR_PAYLOAD_TOO_LARGE, withinRouteBodyCap } from "../transport-caps.js";
7
+ /**
8
+ * Register the small wire faces on a settled web server.
9
+ * @param webServer - the settled host web server.
10
+ * @param d - the captures listed on {@link WireDeps}.
11
+ */
12
+ export function registerWireFaces(webServer, d) {
13
+ const { ctx, logger, config, cliDeposits, canvasStore, sessionNodes, joinedSessions, idempotencyStore, verifyOriginClaim, callerLinkedToThisHost, recordActivity, secretEqual, a2aCallerRefusal, scanSessionLogList, readSessionLogTail, taskLedger, refuseIfFlooded, settleAndAnnounce, session, resolveAgentForTeam, parseCanvasTeamName, canvasLiveAgent, wakeColdTeam, probeConsumption, steerRelay, fromClause, armReceiptAutosend, receiptExpectationOf, armAsyncNudge, liveAgent, callerIsAgent, selfRouteText, noWakeFaceText, archivedColdMatchText, noColdMatchText, nativeTeamsPrepare, nativeTeamsRound, routeIntoAgent, } = d;
14
+ // gh#45 CLI 户口卡面 (peer face, unsigned card serve): the deposited
15
+ // CLI's SIGNED card is served verbatim under its own base URL — an
16
+ // OriginClaim verification of a CLI envelope fetches exactly here
17
+ // (callerUrl + /.well-known/agent-card.json). Expired or unknown
18
+ // handles 404 (auto-deregistered — the identity is dead until
19
+ // re-publish).
20
+ ctx.effect(() => webServer.register({
21
+ kind: 'prefix',
22
+ path: '/a2a/cli',
23
+ handler: (req, res) => {
24
+ const MOUNT = '/a2a/cli';
25
+ const rest = (req.url ?? '').slice(MOUNT.length);
26
+ const m = /^\/([^/]+)\/\.well-known\/agent-card\.json$/.exec(rest);
27
+ if (req.method !== 'GET' || m === null) {
28
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
29
+ res.end();
30
+ return;
31
+ }
32
+ const handle = decodeURIComponent(m[1]);
33
+ const row = cliDeposits.get(handle);
34
+ if (row === undefined) {
35
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
36
+ res.end();
37
+ return;
38
+ }
39
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
40
+ res.end(row.cardJson);
41
+ },
42
+ }), 'a2a: cli deposit card face');
43
+ // gh#41 fleet planning, peer-write face: a LINKED peer may regulate this
44
+ // host's canvas teams (create / add-member / remove-member) with a
45
+ // VERIFIED OriginClaim envelope. The owner's ruling: any peer composes
46
+ // and regulates the whole fleet — the bilateral/operator link IS the
47
+ // authorization, the envelope proves the key, and every op lands in the
48
+ // activity ring + host log for audit. Unlinked or unsigned callers get
49
+ // a refusal, never a mutation.
50
+ ctx.effect(() => webServer.register({
51
+ kind: 'exact',
52
+ path: '/a2a/canvas-op',
53
+ handler: async (req, res) => {
54
+ const send = (status, payload) => {
55
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
56
+ res.end(payload);
57
+ };
58
+ if (req.method !== 'POST') {
59
+ send(200, JSON.stringify({ ok: false, error: 'POST required' }));
60
+ return;
61
+ }
62
+ const chunks = [];
63
+ let size = 0;
64
+ req.on('data', (chunk) => { size += chunk.length; if (size > 16 * 1024) {
65
+ chunks.length = 0;
66
+ return;
67
+ } chunks.push(chunk); });
68
+ req.on('end', () => {
69
+ void (async () => {
70
+ try {
71
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
72
+ const claim = await verifyOriginClaim(req);
73
+ if (!claim.ok) {
74
+ logger.warn(`a2a: canvas-op claim failed (${claim.reason}) — caller=${String(req.headers['x-a2a-caller'] ?? '')} url=${String(req.headers['x-a2a-caller-url'] ?? '')} ts=${String(req.headers['x-a2a-caller-ts'] ?? '')}`);
75
+ send(200, JSON.stringify({ ok: false, error: `untrusted-origin: OriginClaim verification failed (${claim.reason}) — canvas regulation requires a signed envelope`, code: -32000 }));
76
+ return;
77
+ }
78
+ if (!callerLinkedToThisHost(claim.caller, claim.callerUrl, claim.card)) {
79
+ recordActivity('in', 'fleet-canvas', claim.caller, false);
80
+ send(200, JSON.stringify({ ok: false, error: `caller "${claim.caller}" is not linked to this host — canvas regulation requires the operator link (bilateral seed, canvas ref, or same-host membership)` }));
81
+ return;
82
+ }
83
+ const op = body.op;
84
+ const name = typeof body.name === 'string' ? body.name : '';
85
+ const id = typeof body.id === 'string' ? body.id : '';
86
+ let payload;
87
+ if (op === 'create') {
88
+ const stored = canvasStore.create(name);
89
+ payload = stored === undefined ? { ok: false, error: 'invalid name or team cap reached' } : { ok: true, members: canvasStore.membersOf(stored) };
90
+ }
91
+ else if (op === 'add-member') {
92
+ const memberValid = isRemoteMemberRef(id) ? validRemoteMemberRef(id) : id !== '' && (sessionNodes.has(id) || joinedSessions.has(id));
93
+ payload = name === '' || !memberValid
94
+ ? { ok: false, error: 'name and a member (joined session id, or a zone-qualified remote team handle) are required' }
95
+ : (canvasStore.create(name), { ok: canvasStore.addMember(name, id), members: canvasStore.membersOf(name) });
96
+ }
97
+ else if (op === 'remove-member') {
98
+ payload = { ok: name !== '' && canvasStore.removeMember(name, id), members: name !== '' ? canvasStore.membersOf(name) : undefined };
99
+ }
100
+ else {
101
+ payload = { ok: false, error: 'op must be create | add-member | remove-member' };
102
+ }
103
+ recordActivity('in', 'fleet-canvas', `${claim.caller}:${String(op)}`, payload.ok);
104
+ logger.info(`a2a: fleet canvas op from ${claim.caller} (${String(op)} ${name}${id !== '' ? ` ${id}` : ''}) -> ${payload.ok ? 'applied' : `refused: ${payload.error ?? '?'}`}`);
105
+ send(200, JSON.stringify(payload));
106
+ }
107
+ catch (error) {
108
+ send(200, JSON.stringify({ ok: false, error: `canvas-op failed: ${error instanceof Error ? error.message : String(error)}` }));
109
+ }
110
+ })();
111
+ });
112
+ },
113
+ }), 'a2a: fleet canvas-op peer route');
114
+ // 0.6.15: the read-only peer log face — a peer asks for THIS node's
115
+ // session transcript tail (or, without id, the readable-session
116
+ // listing) so a remote workbench can show "what has this node been
117
+ // talking about". Admission is a2aCallerRefusal — exactly the rule
118
+ // /a2a/direct applies to message delivery: caller_session required,
119
+ // zone/id8 callers judged on shared teams. Read semantics are the
120
+ // local /__dsh_a2a/session-log face verbatim (artifact existence is
121
+ // the only gate; archived and never-joined sessions stay readable;
122
+ // caps mirror session-transcript.ts). Nothing here steers a session,
123
+ // claims a task, or writes state — the only side effects are the
124
+ // activity-ring record and the admission path's background
125
+ // directory learn.
126
+ ctx.effect(() => webServer.register({
127
+ kind: 'exact',
128
+ path: '/a2a/log',
129
+ handler: async (req, res) => {
130
+ const send = (status, body) => {
131
+ const payload = JSON.stringify(body);
132
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
133
+ res.end(payload);
134
+ };
135
+ if (req.method !== 'GET') {
136
+ send(405, { ok: false, error: 'GET is the only method on this face' });
137
+ return;
138
+ }
139
+ // V2 (gh#7, S1): the read-only log face rides the same apiKey gate
140
+ // as delivery — a configured key must be presented here too.
141
+ if (config.apiKey !== '') {
142
+ const presented = String(req.headers['x-api-key'] ?? '');
143
+ if (!secretEqual(presented, config.apiKey)) {
144
+ send(401, { ok: false, error: 'unauthorized: X-API-Key required (apiKey is configured)', code: -32000 });
145
+ return;
146
+ }
147
+ }
148
+ const url = new URL(req.url ?? '/', 'http://x');
149
+ const caller = url.searchParams.get('caller_session') ?? '';
150
+ const id = url.searchParams.get('id') ?? '';
151
+ const limitArg = Number(url.searchParams.get('limit') ?? TRANSCRIPT_DEFAULT_LIMIT);
152
+ const limit = Number.isFinite(limitArg) ? Math.max(1, Math.min(TRANSCRIPT_MAX_LIMIT, Math.floor(limitArg))) : TRANSCRIPT_DEFAULT_LIMIT;
153
+ const refusal = a2aCallerRefusal(caller);
154
+ if (refusal !== undefined) {
155
+ recordActivity('in', 'log', caller === '' ? 'anonymous' : caller, false);
156
+ send(200, { ok: false, error: refusal, code: -32000 });
157
+ return;
158
+ }
159
+ // gh#9+gh#10: the log face reuses the exact OriginClaim
160
+ // verification (§8.3) — same window semantics as delivery.
161
+ const logClaim = await verifyOriginClaim(req);
162
+ if (!logClaim.ok) {
163
+ if (logClaim.reason !== 'absent' || config.originClaimWindow === false) {
164
+ recordActivity('in', 'log', caller, false);
165
+ send(200, { ok: false, error: `untrusted-origin: OriginClaim verification failed (${logClaim.reason}) — present a signed envelope or upgrade your node`, code: -32000 });
166
+ return;
167
+ }
168
+ d.bumpLegacyInbound();
169
+ }
170
+ recordActivity('in', 'log', caller, true);
171
+ if (id.includes('/') || id.includes('\\') || id.includes('..')) {
172
+ send(200, { ok: false, error: 'id is required' });
173
+ return;
174
+ }
175
+ if (id === '') {
176
+ try {
177
+ const rows = scanSessionLogList();
178
+ send(200, { ok: true, count: rows.length, sessions: rows.slice(0, 50) });
179
+ }
180
+ catch (error) {
181
+ send(200, { ok: false, error: `transcript list failed: ${error instanceof Error ? error.message : String(error)}` });
182
+ }
183
+ return;
184
+ }
185
+ try {
186
+ const tail = readSessionLogTail(id, limit);
187
+ if (!tail.ok)
188
+ send(200, { ok: false, error: tail.error });
189
+ else
190
+ send(200, { ok: true, id: tail.id, count: tail.messages.length, messages: tail.messages });
191
+ }
192
+ catch (error) {
193
+ send(200, { ok: false, error: `transcript failed: ${error instanceof Error ? error.message : String(error)}` });
194
+ }
195
+ },
196
+ }), 'a2a: peer log face endpoint');
197
+ // W7 slice 2: the S1 outcome-retrieval surface — a READ-ONLY lookup of
198
+ // one claimed task's settled outcome. The body carries ONLY the task id
199
+ // and the submit payload's fingerprint (peerPayloadFingerprint, the
200
+ // gate's own shared implementation): the fingerprint match is the
201
+ // authorization, and the fan-out probe never leaks the original message
202
+ // to peers that never saw it. Constant-200 semantics — a query NEVER
203
+ // produces a wire error code (the frozen -32002/-32003 vocabulary stays
204
+ // submit-only), never claims, never steers, never enters a session: the
205
+ // gate's "verdict before steer" has its mirror in "a query executes
206
+ // nothing". Malformed fields degrade to unknown-task rather than
207
+ // guessing; an unparseable body answers 400 like /a2a/direct.
208
+ ctx.effect(() => webServer.register({
209
+ kind: 'exact',
210
+ path: '/a2a/query',
211
+ handler: (req, res) => {
212
+ const chunks = [];
213
+ let size = 0;
214
+ let overflowed = false;
215
+ req.on('data', (chunk) => {
216
+ if (overflowed)
217
+ return;
218
+ const next = size + chunk.length;
219
+ if (next > 65_536) {
220
+ overflowed = true;
221
+ chunks.length = 0;
222
+ return;
223
+ }
224
+ size = next;
225
+ chunks.push(chunk);
226
+ });
227
+ req.on('error', () => {
228
+ if (!res.headersSent) {
229
+ res.writeHead(400);
230
+ res.end();
231
+ }
232
+ });
233
+ req.on('end', () => {
234
+ if (overflowed) {
235
+ if (!res.writableEnded && !res.headersSent) {
236
+ const payload = JSON.stringify({ error: 'payload too large', code: WIRE_ERROR_PAYLOAD_TOO_LARGE });
237
+ res.writeHead(413, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
238
+ res.end(payload);
239
+ }
240
+ return;
241
+ }
242
+ let body;
243
+ try {
244
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
245
+ }
246
+ catch {
247
+ const payload = JSON.stringify({ error: 'malformed body', code: -32000 });
248
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
249
+ res.end(payload);
250
+ return;
251
+ }
252
+ const taskId = typeof body.task_id === 'string' ? body.task_id : '';
253
+ const fingerprint = typeof body.fingerprint === 'string' ? body.fingerprint : '';
254
+ const verdict = taskId === '' || fingerprint === ''
255
+ ? { found: false, reason: 'unknown-task' }
256
+ : idempotencyStore.query(taskId, fingerprint);
257
+ const payload = JSON.stringify(verdict.found
258
+ ? verdict.status === 'pending'
259
+ ? { found: true, status: 'pending', task_id: taskId }
260
+ : verdict.status === 'completed'
261
+ ? {
262
+ found: true,
263
+ status: 'completed',
264
+ reply: verdict.reply,
265
+ settled_at: new Date(verdict.settledAt).toISOString(),
266
+ task_id: taskId,
267
+ ...(verdict.truncated === true ? { truncated: true } : {}),
268
+ }
269
+ : {
270
+ found: true,
271
+ status: 'failed',
272
+ error: verdict.error,
273
+ settled_at: new Date(verdict.settledAt).toISOString(),
274
+ task_id: taskId,
275
+ ...(verdict.truncated === true ? { truncated: true } : {}),
276
+ }
277
+ : { found: false, reason: verdict.reason, task_id: taskId });
278
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
279
+ res.end(payload);
280
+ });
281
+ },
282
+ }), 'a2a: query endpoint');
283
+ ctx.effect(() => webServer.register({
284
+ kind: 'exact',
285
+ path: '/a2a/direct',
286
+ handler: async (req, res) => {
287
+ const chunks = [];
288
+ let size = 0;
289
+ // B5 enforcement: 512 KiB hard cap. An oversized body is rejected,
290
+ // never truncated and never connection-killed mid-read — buffering
291
+ // stops at the crossing chunk, the stream drains to `end`, and the
292
+ // peer receives one structured 413 with a wire error code (the old
293
+ // behavior tore the socket down with no diagnosis at all).
294
+ let overflowed = false;
295
+ const declared = Number.parseInt(String(req.headers['content-length'] ?? ''), 10);
296
+ if (Number.isFinite(declared) && !withinRouteBodyCap(declared))
297
+ overflowed = true;
298
+ req.on('data', (chunk) => {
299
+ if (overflowed)
300
+ return;
301
+ const next = size + chunk.length;
302
+ if (!withinRouteBodyCap(next)) {
303
+ overflowed = true;
304
+ chunks.length = 0;
305
+ return;
306
+ }
307
+ size = next;
308
+ chunks.push(chunk);
309
+ });
310
+ /* v8 ignore next 6 -- client aborts surface as 'close' via the shared
311
+ webServer's per-request guard; a raw req 'error' needs a malformed
312
+ HTTP stream the webServer rejects before this handler exists */
313
+ req.on('error', () => {
314
+ if (!res.headersSent) {
315
+ res.writeHead(400);
316
+ res.end();
317
+ }
318
+ });
319
+ req.on('end', async () => {
320
+ if (overflowed) {
321
+ if (!res.writableEnded && !res.headersSent) {
322
+ const payload = JSON.stringify({ error: 'payload too large', code: WIRE_ERROR_PAYLOAD_TOO_LARGE });
323
+ res.writeHead(413, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
324
+ res.end(payload);
325
+ }
326
+ return;
327
+ }
328
+ let body;
329
+ try {
330
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
331
+ }
332
+ catch {
333
+ const payload = JSON.stringify({ error: 'malformed body', code: -32000 });
334
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
335
+ res.end(payload);
336
+ return;
337
+ }
338
+ // V2 (gh#7, S1): the delivery face honors the apiKey gate exactly
339
+ // like the control face — a configured key without this header
340
+ // used to let anyone reachable steer sessions while the control
341
+ // routes demanded the key, making it decorative for delivery.
342
+ // Empty-key deployments keep the documented open posture.
343
+ if (config.apiKey !== '') {
344
+ const presented = String(req.headers['x-api-key'] ?? '');
345
+ if (!secretEqual(presented, config.apiKey)) {
346
+ const payload = JSON.stringify({ error: 'unauthorized: X-API-Key required on the delivery face (apiKey is configured)', code: -32000 });
347
+ res.writeHead(401, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
348
+ res.end(payload);
349
+ return;
350
+ }
351
+ }
352
+ const team = typeof body.team === 'string' ? body.team : '';
353
+ const message = typeof body.message === 'string' ? body.message : '';
354
+ const caller = typeof body.caller_session === 'string' ? body.caller_session : '';
355
+ // P2 receipt-callback: where THIS node's receipt should route on
356
+ // the caller's side — the caller knows its routable address best
357
+ // (a joined session's node team, e.g. via the transport face's
358
+ // callbackTarget mapping). Bounded; absent → the caller label
359
+ // plays its usual role.
360
+ const callback = typeof body.callback === 'string' && body.callback !== '' && body.callback.length <= 128 ? body.callback : '';
361
+ const noWait = body.wait === false;
362
+ const contextId = typeof body.context_id === 'string' && body.context_id !== '' ? body.context_id : `ctx-${Math.random().toString(16).slice(2, 10)}`;
363
+ // The caller-born task id (idempotency key): echo it when present
364
+ // so the receipt header correlates with the caller's own result;
365
+ // pre-0.5.3 callers carry none and this node mints one.
366
+ const taskId = typeof body.task_id === 'string' && body.task_id !== '' ? body.task_id : `direct-${Math.random().toString(16).slice(2, 10)}`;
367
+ if (team === '' || message === '') {
368
+ const payload = JSON.stringify({ error: 'team and message are required', code: -32000 });
369
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
370
+ res.end(payload);
371
+ return;
372
+ }
373
+ // Phase-3 admission (owner ruling 2026-09-01, decision-seat
374
+ // filed): an inbound direct delivery must show a SHARED team.
375
+ // Local-zone callers are judged synchronously and exactly — the
376
+ // declaration lives in this host's own roster store, so the
377
+ // Anonymous-delivery refusal (S3 phase-3b, test-seat finding
378
+ // direct-001649ab): a delivery with NO caller_session field
379
+ // skips the admission gate entirely (nothing to judge) — that
380
+ // anonymous hole let any local process or reachable peer inject
381
+ // into a live session. The protocol contract is caller_session;
382
+ // its absence is now refused instead of silently waved through.
383
+ // (0.6.15: the rule now lives in a2aCallerRefusal so the
384
+ // read-only /a2a/log face admits callers by exactly the same
385
+ // standard as message delivery.)
386
+ const callerRefusal = a2aCallerRefusal(caller, team);
387
+ if (callerRefusal !== undefined) {
388
+ recordActivity('in', team, caller === '' ? 'anonymous' : caller, false);
389
+ const payload = JSON.stringify({ error: callerRefusal, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
390
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
391
+ res.end(payload);
392
+ return;
393
+ }
394
+ // gh#9+gh#10 OriginClaim on the delivery face (M1): an envelope
395
+ // that is present but unverifiable rejects untrusted-origin; an
396
+ // ABSENT envelope during the window walks on with a counted
397
+ // legacyInbound warn (the operator watches the tail drain), and
398
+ // after the window closes it rejects like a failed signature —
399
+ // N3 proves a NAME's linkability, never key possession.
400
+ const claim = await verifyOriginClaim(req);
401
+ if (!claim.ok) {
402
+ if (claim.reason !== 'absent' || config.originClaimWindow === false) {
403
+ recordActivity('in', team, caller, false);
404
+ const payload = JSON.stringify({ error: `untrusted-origin: OriginClaim verification failed (${claim.reason}) — present a signed envelope (X-A2A-Caller/-Url/-Ts/-Sig with your card key) or upgrade your node`, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
405
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
406
+ res.end(payload);
407
+ return;
408
+ }
409
+ d.bumpLegacyInbound();
410
+ }
411
+ if (refuseIfBudgetExhausted(req, res, team, caller, taskId, taskLedger))
412
+ return;
413
+ // Server-side idempotency (P3/B3): one key, one execution inside
414
+ // the window. Same key + same payload ⇒ replay; different payload
415
+ // ⇒ hard conflict (both 409). A replay must never re-enter the
416
+ // target session, no matter how it settles.
417
+ const idemFingerprint = peerPayloadFingerprint({ caller, message, noWait, team });
418
+ const idemVerdict = idempotencyStore.claim(taskId, idemFingerprint);
419
+ if (idemVerdict !== 'fresh') {
420
+ const replay = idemVerdict === 'replay';
421
+ // Conflicts warn (caller key reuse bugs); harmless replays stay silent.
422
+ if (!replay)
423
+ logger.warn(`a2a: idempotency CONFLICT on task ${taskId.slice(0, 80)} — same key reused with a different payload (caller bug; never auto-retry)`);
424
+ const payload = JSON.stringify({
425
+ error: replay ? 'duplicate task id within the idempotency window' : 'task id reused with a different payload',
426
+ code: replay ? WIRE_ERROR_REPLAY_REJECTED : WIRE_ERROR_IDEMPOTENCY_CONFLICT,
427
+ task_id: taskId,
428
+ replay,
429
+ });
430
+ res.writeHead(409, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
431
+ res.end(payload);
432
+ return;
433
+ }
434
+ if (refuseIfFlooded(caller, message, team, taskId, res))
435
+ return;
436
+ // The caller-side receipt correlation: a message shaped as a
437
+ // receipt resolves the task it echoes, whichever wait semantics
438
+ // carried it here. Correlation is bookkeeping only — the message
439
+ // steers on exactly as before.
440
+ settleAndAnnounce(message);
441
+ // gh#45 CLI mailbox (wire face): a delivery targeting a DEPOSITED
442
+ // CLI handle never resolves a session and never wakes — it lands
443
+ // in the deposit's mailbox and answers delivered. Placement here
444
+ // keeps the idempotency claim (above) exactly as for sessions.
445
+ const cliRow = cliDeposits.get(team);
446
+ if (cliRow !== undefined) {
447
+ cliDeposits.mail(team, caller, message);
448
+ recordActivity('in', team, caller, true);
449
+ logger.info(`a2a: cli mailbox delivery for ${team} (from ${caller})`);
450
+ const payload = JSON.stringify({ routed: true, delivered: true, receipt: 'mailbox', team, task_id: taskId, task_status: 'TASK_STATE_COMPLETED', note: `delivered to the mailbox of ${team} — the CLI polls while online` });
451
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
452
+ res.end(payload);
453
+ return;
454
+ }
455
+ // Inbound routing is foreground demand: boot prewarm yields for a
456
+ // quiet window around it.
457
+ d.touchWakeDemand();
458
+ // wait:false delivers without the final-reply hold: steer resolves
459
+ // synchronously inside routeIntoAgentFor, so by the time the agent
460
+ // lookup settles the message is already in — answer delivered and
461
+ // let the receipt contract carry the reply.
462
+ if (noWait) {
463
+ // Issue #41 companion fix: the no-wait inbound branch resolved
464
+ // only bare session teams — a canvas team delivery (wait:false)
465
+ // fell through to the cold tier and answered a bogus
466
+ // no-match although a live member existed. Same resolution as
467
+ // the waiting face (routeIntoAgent), and likewise NO initiator
468
+ // skip: the wire face delivers faithfully to the first live
469
+ // member, whoever the caller is.
470
+ const agent = resolveAgentForTeam(team) ?? canvasLiveAgent(parseCanvasTeamName(team));
471
+ const wake = agent !== undefined ? undefined : wakeColdTeam(team);
472
+ const deliver = (target) => {
473
+ const from = caller === '' ? 'an unknown node' : caller;
474
+ // The receipt target: the caller's callback address when it
475
+ // supplied one (a session node team — wake-on-route covers a
476
+ // cold one), else the caller label as before.
477
+ const receiptTarget = callback !== '' ? callback : from;
478
+ try {
479
+ // The receipt header carries the task id: the target echoes
480
+ // it verbatim in "[A2A receipt] task <id> ...", closing the
481
+ // correlation loop with the caller's own route result.
482
+ // F1' (consumed-probe prior-running): capture the pre-steer
483
+ // status first — a target that was already running (its own
484
+ // turn / a brand-new session's activation window) reads
485
+ // `running` after the steer whether or not our wake was
486
+ // latched, so the post-steer read alone would answer a false
487
+ // consumed:true and strand the message.
488
+ const runningBeforeSteer = probeConsumption(target, taskId);
489
+ steerRelay(target, `[A2A direct] (task ${taskId}) ${fromClause(from)} (routed to ${team}) sent:\n\n${message}\n\n(When done, route your outcome back with one call — a2a_route { team: "${receiptTarget}", message: "[A2A receipt] task ${taskId} <one-line outcome>" }.)`, from === 'an unknown node' ? undefined : from);
490
+ recordActivity('in', team, caller, true);
491
+ armReceiptAutosend(target, taskId, receiptTarget, message);
492
+ // v0.5.23 (async-stall): "delivered" only proves the steer call
493
+ // returned — not that a turn started. An idle-phase steer wakes
494
+ // the driver, but a maintenance/abort window latches the wake for
495
+ // replay that may never fire (defect t-mt6nd0sq-hxuhj6: seven
496
+ // delivered:true routes with zero log growth). Probe the driver
497
+ // shortly after: running means the message is being consumed;
498
+ // still-idle means the wake was latched or dropped — surface that
499
+ // honestly and arm a delayed nudge retry below.
500
+ // F1': a prior-running target never trusts the single
501
+ // post-steer read — answer consumed:false conservatively and
502
+ // arm the nudge (a false negative costs one harmless nudge; a
503
+ // false positive strands the delivered message).
504
+ // F2' (receipt-settlement): the delivery is born into the
505
+ // ledger — an inbound row owes a receipt only this node can
506
+ // produce, and before this row existed the nudge's isPending
507
+ // gate no-op'd for exactly the deliveries that stall (08-30:
508
+ // zero ledger records for latched activations). Arming is
509
+ // unconditional now; settlement evidence disarms in the
510
+ // callback.
511
+ const consumedProbe = runningBeforeSteer ? false : probeConsumption(target, taskId);
512
+ // F5 (settlement integrity): the debt gate — a process-label
513
+ // caller cannot receive receipts, so this delivery is born
514
+ // fire-and-forget (receiptExpected:false, answered as
515
+ // receipt:"none") instead of manufacturing a debt nobody can
516
+ // settle. The row itself stays: stall visibility and the
517
+ // nudge still apply to the TARGET side.
518
+ const receiptExpectation = receiptExpectationOf(receiptTarget);
519
+ taskLedger.trackInbound(taskId, team, receiptTarget, contextId === '' ? undefined : contextId, receiptExpectation !== 'process-label', briefOf(message));
520
+ armAsyncNudge(target, team, taskId, config.asyncNudgeDelayMs);
521
+ const payload = JSON.stringify({
522
+ routed: true,
523
+ delivered: true,
524
+ team,
525
+ session,
526
+ task_id: taskId,
527
+ context_id: contextId,
528
+ task_status: 'TASK_STATE_DELIVERED',
529
+ artifacts: [],
530
+ consumed: consumedProbe,
531
+ receipt: receiptExpectation === 'process-label' ? 'none' : 'routed',
532
+ });
533
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
534
+ res.end(payload);
535
+ }
536
+ catch (error) {
537
+ recordActivity('in', team, caller, false);
538
+ const payload = JSON.stringify({ error: `The DSH session rejected the message: ${String(error)}`, code: -32000, team, task_id: taskId, task_status: 'TASK_STATE_FAILED' });
539
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
540
+ res.end(payload);
541
+ }
542
+ };
543
+ if (agent !== undefined) {
544
+ deliver(agent);
545
+ return;
546
+ }
547
+ if (wake?.kind === 'flight') {
548
+ void wake.flight.then(deliver, (error) => {
549
+ recordActivity('in', team, caller, false);
550
+ const payload = JSON.stringify({ error: `waking the session for team "${team}" failed: ${String(error)}`, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
551
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
552
+ res.end(payload);
553
+ });
554
+ return;
555
+ }
556
+ // Remaining candidates need one async classification (the
557
+ // native-teams bridge), so the tail answers inside an IIFE.
558
+ void (async () => {
559
+ // Native-teams inbound bridge (opt-in): prepare BEFORE answering
560
+ // delivered — claim, seam, and parent initiator are everything
561
+ // that can fail fast, and a phantom dispatch must never answer
562
+ // success. Past prepare, the round fires detached (noWait means
563
+ // delivery, not settlement); its late outcome corrects the
564
+ // activity ring and the log.
565
+ const standardMiss = () => {
566
+ if (team === config.team) {
567
+ const live = liveAgent();
568
+ if (live !== undefined) {
569
+ // Self-route refusal (mirrors the sync face): a caller
570
+ // naming the very agent the fallback resolves to must
571
+ // get an honest failure, never its own dispatch echoed
572
+ // home plus an auto-receipt for work nobody did.
573
+ if (callerIsAgent(live, caller)) {
574
+ recordActivity('in', team, caller, false);
575
+ const payload = JSON.stringify({ error: selfRouteText(team), code: -32000, team, task_status: 'TASK_STATE_FAILED' });
576
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
577
+ res.end(payload);
578
+ return;
579
+ }
580
+ deliver(live);
581
+ return;
582
+ }
583
+ }
584
+ const payload = JSON.stringify({ error: wake?.kind === 'no-face' ? noWakeFaceText(team) : wake?.kind === 'archived' ? archivedColdMatchText(team) : noColdMatchText(team), code: -32000, team, task_status: 'TASK_STATE_FAILED' });
585
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
586
+ res.end(payload);
587
+ };
588
+ const prepared = await nativeTeamsPrepare(team);
589
+ if (!prepared.ok) {
590
+ if (prepared.reason === 'error') {
591
+ recordActivity('in', team, caller, false);
592
+ const payload = JSON.stringify({ error: prepared.error, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
593
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
594
+ res.end(payload);
595
+ return;
596
+ }
597
+ standardMiss();
598
+ return;
599
+ }
600
+ recordActivity('in', team, caller, true);
601
+ void nativeTeamsRound(prepared, team, caller, message, taskId)
602
+ .then(outcome => {
603
+ if (!outcome.ok) {
604
+ recordActivity('in', team, caller, false);
605
+ logger.warn(`a2a: detached native-teams round for "${team}" failed: ${outcome.error}`);
606
+ }
607
+ // W7 slice-2 hook 2: only a genuinely settled detached round
608
+ // records an outcome. The deadline's settled:false
609
+ // projection is a placeholder, not a product — the same
610
+ // ruling as hook 1's DELIVERED skip; placeholder prose never
611
+ // enters the ledger a replay could adopt from.
612
+ if (taskId !== '') {
613
+ if (!outcome.ok)
614
+ idempotencyStore.recordOutcome(taskId, { status: 'failed', error: outcome.error });
615
+ else if (outcome.settled)
616
+ idempotencyStore.recordOutcome(taskId, { status: 'completed', reply: outcome.reply });
617
+ }
618
+ });
619
+ const payload = JSON.stringify({ routed: true, delivered: true, team, session, task_id: taskId, context_id: contextId, task_status: 'TASK_STATE_DELIVERED', artifacts: [], consumed: false, bridge: 'native-teams' });
620
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
621
+ res.end(payload);
622
+ })();
623
+ return;
624
+ }
625
+ void routeIntoAgent(team, message, caller, taskId).then((outcome) => {
626
+ recordActivity('in', team, caller, outcome.ok);
627
+ // W7 slice-2 hook 1: record the settled outcome on the claim row.
628
+ // Ruling: a non-COMPLETED status (TASK_STATE_DELIVERED — the
629
+ // bridge-round deadline placeholder, ABORTED_WAIT likewise) is
630
+ // NOT a settled outcome, and neither is a host-authored
631
+ // placeholder on the reply channel (flush timeout, dead session)
632
+ // — the row stays pending, because an adopted replay must never
633
+ // settle a node on placeholder text (the exact hazard W7 gap B
634
+ // closed, barred from resurrecting through the outcome ledger).
635
+ if (taskId !== '') {
636
+ if (!outcome.ok) {
637
+ idempotencyStore.recordOutcome(taskId, { status: 'failed', error: outcome.error });
638
+ }
639
+ else if (outcome.placeholder === true) {
640
+ // placeholder prose never enters the ledger
641
+ }
642
+ else if (outcome.status === undefined || outcome.status === 'TASK_STATE_COMPLETED') {
643
+ idempotencyStore.recordOutcome(taskId, { status: 'completed', reply: outcome.reply });
644
+ }
645
+ }
646
+ const payload = outcome.ok
647
+ ? JSON.stringify({
648
+ routed: true,
649
+ team,
650
+ session,
651
+ result: { text: outcome.reply },
652
+ task_id: taskId,
653
+ context_id: contextId,
654
+ task_status: outcome.status ?? 'TASK_STATE_COMPLETED',
655
+ artifacts: [],
656
+ // The bridge marker rides the wire so the caller's
657
+ // trackOwedTask skips a receipt-less native round — without
658
+ // it, a cross-node unsettled round books an unpayable owed
659
+ // row and re-creates the TASK_CAP backlog.
660
+ ...(outcome.bridge !== undefined ? { bridge: outcome.bridge } : {}),
661
+ })
662
+ : JSON.stringify({ error: outcome.error, code: -32000, team, task_id: taskId, task_status: 'TASK_STATE_FAILED' });
663
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
664
+ res.end(payload);
665
+ });
666
+ });
667
+ },
668
+ }), 'a2a: direct route endpoint');
669
+ }