@nelsonlongxiang/dsh-open-a2a-net 0.6.31 → 0.6.33
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.
- package/README.md +11 -2
- package/README_zh.md +10 -1
- package/assets/nexusDist/assets/index-BACVZe0j.css +1 -0
- package/assets/nexusDist/assets/index-Bbxnpf2p.js +9 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +450 -59
- package/lib/a2a-client.js +9 -4
- package/lib/auth-observer.js +254 -0
- package/lib/blob-store.js +210 -0
- package/lib/canvas-store.js +6 -0
- package/lib/card.js +4 -2
- package/lib/cli-deposit.js +122 -7
- package/lib/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/faces/admin.js +176 -47
- package/lib/faces/canvas-notify.js +12 -3
- package/lib/faces/files.js +155 -0
- package/lib/faces/nodes.js +171 -0
- package/lib/faces/session-log.js +162 -0
- package/lib/faces/wire.js +212 -43
- package/lib/index.js +819 -1725
- package/lib/joined-store.js +23 -0
- package/lib/layout-store.js +3 -2
- package/lib/ledger-conservation.js +97 -0
- package/lib/legacy/teams-bridge-inbound.js +156 -0
- package/lib/node-bind.js +534 -0
- package/lib/node-github.js +72 -0
- package/lib/node-self.js +453 -0
- package/lib/node-store.js +484 -0
- package/lib/node-tasks.js +342 -0
- package/lib/origin-claim.js +246 -0
- package/lib/peer-directory.js +134 -0
- package/lib/receipt.js +26 -1
- package/lib/roster-op.js +104 -0
- package/lib/skill/SKILL.md +3 -1
- package/lib/skill/references/cli.md +83 -4
- package/lib/skill/references/collaboration.md +126 -74
- package/lib/skill/references/discovery.md +29 -0
- package/lib/skill/references/intake.md +107 -0
- package/lib/skill/references/router.md +96 -0
- package/lib/skill/references/setup.md +2 -1
- package/lib/task-auth.js +483 -0
- package/lib/task-ledger.js +324 -12
- package/lib/team-store.js +73 -2
- package/lib/tools/act.js +383 -0
- package/lib/tools/files.js +201 -0
- package/lib/tools/info.js +684 -0
- package/lib/types/a2a-client.d.ts +10 -4
- package/lib/types/auth-observer.d.ts +110 -0
- package/lib/types/blob-store.d.ts +83 -0
- package/lib/types/canvas-store.d.ts +4 -0
- package/lib/types/cli-deposit.d.ts +38 -0
- package/lib/types/faces/admin.d.ts +15 -2
- package/lib/types/faces/canvas-notify.d.ts +4 -1
- package/lib/types/faces/files.d.ts +53 -0
- package/lib/types/faces/nodes.d.ts +69 -0
- package/lib/types/faces/session-log.d.ts +44 -0
- package/lib/types/faces/wire.d.ts +51 -5
- package/lib/types/index.d.ts +27 -0
- package/lib/types/joined-store.d.ts +9 -0
- package/lib/types/ledger-conservation.d.ts +77 -0
- package/lib/types/legacy/teams-bridge-inbound.d.ts +94 -0
- package/lib/types/node-bind.d.ts +220 -0
- package/lib/types/node-github.d.ts +35 -0
- package/lib/types/node-self.d.ts +178 -0
- package/lib/types/node-store.d.ts +180 -0
- package/lib/types/node-tasks.d.ts +121 -0
- package/lib/types/origin-claim.d.ts +119 -0
- package/lib/types/peer-directory.d.ts +39 -0
- package/lib/types/receipt.d.ts +12 -0
- package/lib/types/roster-op.d.ts +21 -0
- package/lib/types/task-auth.d.ts +271 -0
- package/lib/types/task-ledger.d.ts +143 -6
- package/lib/types/team-store.d.ts +16 -0
- package/lib/types/tools/act.d.ts +52 -0
- package/lib/types/tools/files.d.ts +38 -0
- package/lib/types/tools/info.d.ts +102 -0
- package/lib/types/types.d.ts +6 -0
- package/nexus-stage/src/canvas-ops.ts +3 -2
- package/nexus-stage/src/federation.ts +1 -0
- package/nexus-stage/src/layout-doc.ts +6 -4
- package/nexus-stage/src/main.ts +68 -20
- package/nexus-stage/src/planning-frame-hue.ts +13 -0
- package/nexus-stage/src/planning-legend.ts +1 -0
- package/nexus-stage/src/planning-member-handles.ts +34 -0
- package/nexus-stage/src/planning-menus.ts +28 -12
- package/nexus-stage/src/planning-mirror.ts +47 -3
- package/nexus-stage/src/planning-reachability.ts +28 -0
- package/nexus-stage/src/planning-roster.ts +15 -0
- package/nexus-stage/src/planning-types.ts +2 -2
- package/nexus-stage/src/planning-view.ts +57 -52
- package/nexus-stage/src/planning.css +6 -0
- package/nexus-stage/src/wb-directory.ts +13 -0
- package/nexus-stage/src/workbench.css +3 -0
- package/nexus-stage/src/workbench.ts +7 -1
- package/package.json +3 -2
- package/assets/nexusDist/assets/index-Dz8N80tI.css +0 -1
- package/assets/nexusDist/assets/index-jm-cFAhB.js +0 -9
package/lib/faces/wire.js
CHANGED
|
@@ -3,14 +3,78 @@ import { TRANSCRIPT_DEFAULT_LIMIT, TRANSCRIPT_MAX_LIMIT } from "../session-trans
|
|
|
3
3
|
import { refuseIfBudgetExhausted } from "../recursion-guard.js";
|
|
4
4
|
import { peerPayloadFingerprint, WIRE_ERROR_IDEMPOTENCY_CONFLICT, WIRE_ERROR_REPLAY_REJECTED } from "../idempotency-store.js";
|
|
5
5
|
import { briefOf } from "../task-ledger.js";
|
|
6
|
+
import { parseReceipt, taskIdRefusal } from "../receipt.js";
|
|
7
|
+
import { buildAuthorization, claimCovers, refusalOf } from "../task-auth.js";
|
|
8
|
+
import { isRootOriginUrl } from "../origin-claim.js";
|
|
6
9
|
import { WIRE_ERROR_PAYLOAD_TOO_LARGE, withinRouteBodyCap } from "../transport-caps.js";
|
|
10
|
+
/**
|
|
11
|
+
* #139 (design §4.4): add `settlement: {status:'refused', reason, task_id}`
|
|
12
|
+
* to whatever JSON object this response ends with. The delivery face has
|
|
13
|
+
* many exits (mailbox, no-wait, waiting, refusals); deferring the head until
|
|
14
|
+
* the body is known keeps one rule for all of them. A non-JSON or non-object
|
|
15
|
+
* body is sent unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export function withSettlementField(res, settlement) {
|
|
18
|
+
const writeHead = res.writeHead.bind(res);
|
|
19
|
+
const end = res.end.bind(res);
|
|
20
|
+
let head;
|
|
21
|
+
res.writeHead = (status, headers) => {
|
|
22
|
+
head = { status, headers: { ...(headers ?? {}) } };
|
|
23
|
+
return res;
|
|
24
|
+
};
|
|
25
|
+
res.end = (body) => {
|
|
26
|
+
let out = body;
|
|
27
|
+
if (typeof body === 'string') {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(body);
|
|
30
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
31
|
+
out = JSON.stringify({ ...parsed, settlement });
|
|
32
|
+
}
|
|
33
|
+
catch { /* not JSON: unchanged */ }
|
|
34
|
+
}
|
|
35
|
+
if (head !== undefined) {
|
|
36
|
+
if (out !== undefined && 'Content-Length' in head.headers)
|
|
37
|
+
head.headers['Content-Length'] = Buffer.byteLength(out);
|
|
38
|
+
writeHead(head.status, head.headers);
|
|
39
|
+
}
|
|
40
|
+
return end(out);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** The origin of a card URL for the authorization record; the raw text, bounded, when it does not parse. */
|
|
44
|
+
const originOf = (url) => {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(url).origin;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return url.slice(0, 256);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** Same scheme/host/port; false when either side is empty or malformed. */
|
|
53
|
+
function sameOrigin(a, b) {
|
|
54
|
+
try {
|
|
55
|
+
return a !== '' && b !== '' && new URL(a).origin === new URL(b).origin;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* #187: what a verified claim may vouch for at settlement. The signing key always (the card was
|
|
63
|
+
* fetched and its key verified); the ORIGIN only when the claim URL is that origin's root — a card
|
|
64
|
+
* fetched from a path under an origin proves the path's publisher, not the origin's host.
|
|
65
|
+
*/
|
|
66
|
+
export function settlerClaimFields(claim, originOf) {
|
|
67
|
+
if (!claim.ok || claim.cli === true || claim.card === undefined || claim.callerUrl === undefined)
|
|
68
|
+
return {};
|
|
69
|
+
return { ...(isRootOriginUrl(claim.callerUrl) ? { claimOrigin: originOf(claim.callerUrl) } : {}), claimKey: claim.card.publicKey };
|
|
70
|
+
}
|
|
7
71
|
/**
|
|
8
72
|
* Register the small wire faces on a settled web server.
|
|
9
73
|
* @param webServer - the settled host web server.
|
|
10
74
|
* @param d - the captures listed on {@link WireDeps}.
|
|
11
75
|
*/
|
|
12
76
|
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;
|
|
77
|
+
const { ctx, logger, config, cliDeposits, canvasStore, canvasNameReserved, sessionNodes, joinedSessions, idempotencyStore, verifyOriginClaim, callerLinkedToThisHost, recordActivity, secretEqual, a2aCallerRefusal, a2aCallerAdmission, allowlistVersionNow, observeAuthorization, scanSessionLogList, readSessionLogTail, taskLedger, refuseIfFlooded, settleAndAnnounce, session, resolveAgentForTeam, parseCanvasTeamName, canvasLiveAgent, wakeColdTeam, probeConsumption, sessionTeamOf, localSessionOfHandle, steerRelay, fromClause, sanitizeFromClause, armReceiptAutosend, eventCountOf, receiptExpectationOf, armAsyncNudge, liveAgent, callerIsAgent, selfRouteText, noWakeFaceText, archivedColdMatchText, noColdMatchText, nativeTeamsPrepare, nativeTeamsRound, routeIntoAgent, } = d;
|
|
14
78
|
// gh#45 CLI 户口卡面 (peer face, unsigned card serve): the deposited
|
|
15
79
|
// CLI's SIGNED card is served verbatim under its own base URL — an
|
|
16
80
|
// OriginClaim verification of a CLI envelope fetches exactly here
|
|
@@ -41,7 +105,7 @@ export function registerWireFaces(webServer, d) {
|
|
|
41
105
|
},
|
|
42
106
|
}), 'a2a: cli deposit card face');
|
|
43
107
|
// 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
|
|
108
|
+
// host's canvas teams (create / add-member / remove-member / remove) with a
|
|
45
109
|
// VERIFIED OriginClaim envelope. The owner's ruling: any peer composes
|
|
46
110
|
// and regulates the whole fleet — the bilateral/operator link IS the
|
|
47
111
|
// authorization, the envelope proves the key, and every op lands in the
|
|
@@ -75,6 +139,16 @@ export function registerWireFaces(webServer, d) {
|
|
|
75
139
|
send(200, JSON.stringify({ ok: false, error: `untrusted-origin: OriginClaim verification failed (${claim.reason}) — canvas regulation requires a signed envelope`, code: -32000 }));
|
|
76
140
|
return;
|
|
77
141
|
}
|
|
142
|
+
// This host edits its own canvas through the local control, never
|
|
143
|
+
// through this peer face. A claim naming OUR origin is an envelope
|
|
144
|
+
// we stamped on some outbound request (a card fetch to any tracked
|
|
145
|
+
// peer, linked or not) being replayed at us: the claim is stateless
|
|
146
|
+
// and binds no target, so the same-host basis must not admit it here.
|
|
147
|
+
if (sameOrigin(claim.callerUrl, d.ownCardUrl)) {
|
|
148
|
+
recordActivity('in', 'fleet-canvas', claim.caller, false);
|
|
149
|
+
send(200, JSON.stringify({ ok: false, error: 'a host does not regulate its own canvas through the peer face (replayed self envelope refused)' }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
78
152
|
if (!callerLinkedToThisHost(claim.caller, claim.callerUrl, claim.card)) {
|
|
79
153
|
recordActivity('in', 'fleet-canvas', claim.caller, false);
|
|
80
154
|
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)` }));
|
|
@@ -82,23 +156,37 @@ export function registerWireFaces(webServer, d) {
|
|
|
82
156
|
}
|
|
83
157
|
const op = body.op;
|
|
84
158
|
const name = typeof body.name === 'string' ? body.name : '';
|
|
85
|
-
const
|
|
159
|
+
const rawId = typeof body.id === 'string' ? body.id : '';
|
|
160
|
+
// gh#131: a peer names our sessions by handle — the session it names, for removal.
|
|
161
|
+
const id = (isRemoteMemberRef(rawId) ? localSessionOfHandle(rawId) : undefined) ?? rawId;
|
|
86
162
|
let payload;
|
|
87
163
|
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) };
|
|
164
|
+
const stored = canvasNameReserved(name) ? undefined : canvasStore.create(name);
|
|
165
|
+
payload = stored === undefined ? { ok: false, error: 'invalid, roster-reserved name or team cap reached' } : { ok: true, members: canvasStore.membersOf(stored) };
|
|
90
166
|
}
|
|
91
167
|
else if (op === 'add-member') {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
168
|
+
// gh#131: a handle is stored as sent, even one of this host's own — the
|
|
169
|
+
// local tiers read it as the session it names at route time.
|
|
170
|
+
const memberValid = isRemoteMemberRef(rawId) ? validRemoteMemberRef(rawId) : rawId !== '' && (sessionNodes.has(rawId) || joinedSessions.has(rawId));
|
|
171
|
+
payload = !canvasStore.hasTeam(name) && canvasNameReserved(name)
|
|
172
|
+
? { ok: false, error: 'roster reserves this canvas name' }
|
|
173
|
+
: name === '' || !memberValid
|
|
174
|
+
? { ok: false, error: 'name and a member (joined session id, or a zone-qualified remote team handle) are required' }
|
|
175
|
+
: (canvasStore.create(name), { ok: canvasStore.addMember(name, rawId), members: canvasStore.membersOf(name) });
|
|
96
176
|
}
|
|
97
177
|
else if (op === 'remove-member') {
|
|
98
|
-
|
|
178
|
+
// Either shape may be stored (an older edit kept the handle): remove both (review of PR #151 P2-1).
|
|
179
|
+
const removed = name !== '' && [...new Set([id, rawId])].map(each => canvasStore.removeMember(name, each)).some(Boolean);
|
|
180
|
+
payload = { ok: removed, members: name !== '' ? canvasStore.membersOf(name) : undefined };
|
|
181
|
+
}
|
|
182
|
+
else if (op === 'remove') {
|
|
183
|
+
// Dissolve. Idempotent: a team that is not here (another peer got
|
|
184
|
+
// there first) is the outcome the caller asked for — `removed`
|
|
185
|
+
// says which it was.
|
|
186
|
+
payload = name === '' ? { ok: false, error: 'name is required' } : { ok: true, removed: canvasStore.remove(name) };
|
|
99
187
|
}
|
|
100
188
|
else {
|
|
101
|
-
payload = { ok: false, error: 'op must be create | add-member | remove-member' };
|
|
189
|
+
payload = { ok: false, error: 'op must be create | add-member | remove-member | remove' };
|
|
102
190
|
}
|
|
103
191
|
recordActivity('in', 'fleet-canvas', `${claim.caller}:${String(op)}`, payload.ok);
|
|
104
192
|
logger.info(`a2a: fleet canvas op from ${claim.caller} (${String(op)} ${name}${id !== '' ? ` ${id}` : ''}) -> ${payload.ok ? 'applied' : `refused: ${payload.error ?? '?'}`}`);
|
|
@@ -242,6 +330,8 @@ export function registerWireFaces(webServer, d) {
|
|
|
242
330
|
let body;
|
|
243
331
|
try {
|
|
244
332
|
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
333
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
334
|
+
throw new TypeError('query body must be a JSON object'); // #168: `null` read body.task_id in this 'end' callback → uncaught → host exit, no auth needed
|
|
245
335
|
}
|
|
246
336
|
catch {
|
|
247
337
|
const payload = JSON.stringify({ error: 'malformed body', code: -32000 });
|
|
@@ -328,6 +418,8 @@ export function registerWireFaces(webServer, d) {
|
|
|
328
418
|
let body;
|
|
329
419
|
try {
|
|
330
420
|
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
421
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
422
|
+
throw new TypeError('direct body must be a JSON object'); // #168: `null` read body.team in this async 'end' callback → unhandled rejection → host exit
|
|
331
423
|
}
|
|
332
424
|
catch {
|
|
333
425
|
const payload = JSON.stringify({ error: 'malformed body', code: -32000 });
|
|
@@ -370,6 +462,15 @@ export function registerWireFaces(webServer, d) {
|
|
|
370
462
|
res.end(payload);
|
|
371
463
|
return;
|
|
372
464
|
}
|
|
465
|
+
// #114 S5: a caller-born id must be header-safe — refused before
|
|
466
|
+
// admission, claim or steer, so nothing is booked under it.
|
|
467
|
+
const idRefusal = taskIdRefusal(taskId);
|
|
468
|
+
if (idRefusal !== undefined) {
|
|
469
|
+
const payload = JSON.stringify({ error: idRefusal, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
|
|
470
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
471
|
+
res.end(payload);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
373
474
|
// Phase-3 admission (owner ruling 2026-09-01, decision-seat
|
|
374
475
|
// filed): an inbound direct delivery must show a SHARED team.
|
|
375
476
|
// Local-zone callers are judged synchronously and exactly — the
|
|
@@ -383,21 +484,27 @@ export function registerWireFaces(webServer, d) {
|
|
|
383
484
|
// (0.6.15: the rule now lives in a2aCallerRefusal so the
|
|
384
485
|
// read-only /a2a/log face admits callers by exactly the same
|
|
385
486
|
// standard as message delivery.)
|
|
386
|
-
const
|
|
387
|
-
|
|
487
|
+
const admission = a2aCallerAdmission(caller, team);
|
|
488
|
+
const callerRefusal = refusalOf(admission);
|
|
489
|
+
if (callerRefusal !== undefined || !admission.ok) {
|
|
388
490
|
recordActivity('in', team, caller === '' ? 'anonymous' : caller, false);
|
|
389
491
|
const payload = JSON.stringify({ error: callerRefusal, code: -32000, team, task_status: 'TASK_STATE_FAILED' });
|
|
390
492
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
391
493
|
res.end(payload);
|
|
392
494
|
return;
|
|
393
495
|
}
|
|
496
|
+
// #114 S6: a version an inbound record is born under is always in the history.
|
|
497
|
+
observeAuthorization('lazy');
|
|
498
|
+
// #114 S2: the version is read at admission time, beside the decision it explains.
|
|
499
|
+
const admittedAt = { version: allowlistVersionNow(), at: Date.now() };
|
|
394
500
|
// gh#9+gh#10 OriginClaim on the delivery face (M1): an envelope
|
|
395
501
|
// that is present but unverifiable rejects untrusted-origin; an
|
|
396
502
|
// ABSENT envelope during the window walks on with a counted
|
|
397
503
|
// legacyInbound warn (the operator watches the tail drain), and
|
|
398
504
|
// after the window closes it rejects like a failed signature —
|
|
399
505
|
// N3 proves a NAME's linkability, never key possession.
|
|
400
|
-
|
|
506
|
+
// gh#133: the delivery face is where a deposited CLI identity signs its routes.
|
|
507
|
+
const claim = await verifyOriginClaim(req, { cli: true });
|
|
401
508
|
if (!claim.ok) {
|
|
402
509
|
if (claim.reason !== 'absent' || config.originClaimWindow === false) {
|
|
403
510
|
recordActivity('in', team, caller, false);
|
|
@@ -408,6 +515,17 @@ export function registerWireFaces(webServer, d) {
|
|
|
408
515
|
}
|
|
409
516
|
d.bumpLegacyInbound();
|
|
410
517
|
}
|
|
518
|
+
// A verified claim proves the caller only when its signed zone covers it (review of PR #134 P1),
|
|
519
|
+
// and the host face only when it was signed from this very host (round 2: a zone name is not an origin).
|
|
520
|
+
const callerProof = !claim.ok ? 'legacy-window'
|
|
521
|
+
: claim.cli === true ? (claimCovers(caller, claim.caller) ? 'cli-identity' : 'claim-mismatch')
|
|
522
|
+
: claim.bound !== true ? 'unbound'
|
|
523
|
+
: !isRootOriginUrl(claim.callerUrl) ? 'claim-mismatch'
|
|
524
|
+
: admission.rule === 'host-face' && originOf(claim.callerUrl) !== originOf(d.ownCardUrl) ? 'claim-mismatch'
|
|
525
|
+
: claimCovers(caller, claim.caller) ? 'origin-claim' : 'claim-mismatch';
|
|
526
|
+
const proofClause = callerProof === 'origin-claim' && claim.ok ? `已验证来源 ${originOf(claim.callerUrl)}`
|
|
527
|
+
: callerProof === 'cli-identity' ? '未验证(cli-identity)' : `未验证(${callerProof})`;
|
|
528
|
+
const directHeader = (from) => `[A2A direct] (task ${taskId}) ${proofClause} ${sanitizeFromClause(from)} (routed to ${team}) sent:`;
|
|
411
529
|
if (refuseIfBudgetExhausted(req, res, team, caller, taskId, taskLedger))
|
|
412
530
|
return;
|
|
413
531
|
// Server-side idempotency (P3/B3): one key, one execution inside
|
|
@@ -437,17 +555,29 @@ export function registerWireFaces(webServer, d) {
|
|
|
437
555
|
// receipt resolves the task it echoes, whichever wait semantics
|
|
438
556
|
// carried it here. Correlation is bookkeeping only — the message
|
|
439
557
|
// steers on exactly as before.
|
|
440
|
-
|
|
558
|
+
// #114 S4: the settler is this delivery's caller, as proven here. A CLI identity's claim URL
|
|
559
|
+
// is not a host's origin (its origin is unconstrained), so it never vouches for one (review of PR #140 P1-2).
|
|
560
|
+
// #139: the claim's card key names the host whatever address spelling it signed with.
|
|
561
|
+
const refusedSettlement = settleAndAnnounce(message, { caller, proof: callerProof, ...settlerClaimFields(claim, originOf), ingress: 'wire' });
|
|
562
|
+
// #139 (design §4.4): every answer this delivery gets carries the refusal.
|
|
563
|
+
if (refusedSettlement !== undefined)
|
|
564
|
+
withSettlementField(res, refusedSettlement);
|
|
441
565
|
// gh#45 CLI mailbox (wire face): a delivery targeting a DEPOSITED
|
|
442
566
|
// CLI handle never resolves a session and never wakes — it lands
|
|
443
567
|
// in the deposit's mailbox and answers delivered. Placement here
|
|
444
568
|
// keeps the idempotency claim (above) exactly as for sessions.
|
|
445
569
|
const cliRow = cliDeposits.get(team);
|
|
446
570
|
if (cliRow !== undefined) {
|
|
447
|
-
cliDeposits.mail(team, caller, message);
|
|
571
|
+
cliDeposits.mail(team, caller, `${directHeader(caller)}\n\n${message}`);
|
|
448
572
|
recordActivity('in', team, caller, true);
|
|
449
573
|
logger.info(`a2a: cli mailbox delivery for ${team} (from ${caller})`);
|
|
450
|
-
const
|
|
574
|
+
const note = `delivered to the mailbox of ${team} — the CLI polls while online`;
|
|
575
|
+
// gh#112 review: a mailbox delivery is final from this host's
|
|
576
|
+
// side, so a resend of this id reads `completed`. A receipt the
|
|
577
|
+
// CLI routes back later still reaches its caller, but does not
|
|
578
|
+
// replace this outcome (recordOutcome is first-writer-wins).
|
|
579
|
+
idempotencyStore.recordOutcome(taskId, { status: 'completed', reply: note });
|
|
580
|
+
const payload = JSON.stringify({ routed: true, delivered: true, receipt: 'mailbox', team, task_id: taskId, task_status: 'TASK_STATE_COMPLETED', note });
|
|
451
581
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
452
582
|
res.end(payload);
|
|
453
583
|
return;
|
|
@@ -460,6 +590,18 @@ export function registerWireFaces(webServer, d) {
|
|
|
460
590
|
// lookup settles the message is already in — answer delivered and
|
|
461
591
|
// let the receipt contract carry the reply.
|
|
462
592
|
if (noWait) {
|
|
593
|
+
// gh#112 review P1: a no-wait refusal AFTER the idempotency
|
|
594
|
+
// claim records its failed outcome — otherwise a resend of the
|
|
595
|
+
// same id reads `pending` from /a2a/query forever and the
|
|
596
|
+
// caller is told an undelivered message already landed.
|
|
597
|
+
const refuseNoWait = (error, activity = true) => {
|
|
598
|
+
if (activity)
|
|
599
|
+
recordActivity('in', team, caller, false);
|
|
600
|
+
idempotencyStore.recordOutcome(taskId, { status: 'failed', error });
|
|
601
|
+
const payload = JSON.stringify({ error, code: -32000, team, task_id: taskId, task_status: 'TASK_STATE_FAILED' });
|
|
602
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
603
|
+
res.end(payload);
|
|
604
|
+
};
|
|
463
605
|
// Issue #41 companion fix: the no-wait inbound branch resolved
|
|
464
606
|
// only bare session teams — a canvas team delivery (wait:false)
|
|
465
607
|
// fell through to the cold tier and answered a bogus
|
|
@@ -475,6 +617,11 @@ export function registerWireFaces(webServer, d) {
|
|
|
475
617
|
// supplied one (a session node team — wake-on-route covers a
|
|
476
618
|
// cold one), else the caller label as before.
|
|
477
619
|
const receiptTarget = callback !== '' ? callback : from;
|
|
620
|
+
// gh#112 review: only a failed steer is a refusal. Once the
|
|
621
|
+
// message is in the session, a failure in the bookkeeping that
|
|
622
|
+
// follows must not record `failed` (a resend would read
|
|
623
|
+
// "already ran and failed" and invite a second execution).
|
|
624
|
+
let steered = false;
|
|
478
625
|
try {
|
|
479
626
|
// The receipt header carries the task id: the target echoes
|
|
480
627
|
// it verbatim in "[A2A receipt] task <id> ...", closing the
|
|
@@ -486,9 +633,16 @@ export function registerWireFaces(webServer, d) {
|
|
|
486
633
|
// latched, so the post-steer read alone would answer a false
|
|
487
634
|
// consumed:true and strand the message.
|
|
488
635
|
const runningBeforeSteer = probeConsumption(target, taskId);
|
|
489
|
-
|
|
636
|
+
// #114 S5: the receipt's consumption search starts here, so an
|
|
637
|
+
// older steer of a reused id in the history never anchors it.
|
|
638
|
+
const steerFloor = eventCountOf(target);
|
|
639
|
+
const deliveredReceipt = parseReceipt(message) !== null;
|
|
640
|
+
// gh#141: a receipt is answered by nobody — its steer asks for no receipt back.
|
|
641
|
+
const receiptRequest = deliveredReceipt ? '' : `\n\n(When done, route your outcome back with one call — a2a_route { team: "${receiptTarget}", message: "[A2A receipt] task ${taskId} <one-line outcome>" }.)`;
|
|
642
|
+
steerRelay(target, `${directHeader(from)}\n\n${message}${receiptRequest}`, from === 'an unknown node' ? undefined : from);
|
|
643
|
+
steered = true;
|
|
490
644
|
recordActivity('in', team, caller, true);
|
|
491
|
-
armReceiptAutosend(target, taskId, receiptTarget, message);
|
|
645
|
+
armReceiptAutosend(target, taskId, receiptTarget, message, steerFloor);
|
|
492
646
|
// v0.5.23 (async-stall): "delivered" only proves the steer call
|
|
493
647
|
// returned — not that a turn started. An idle-phase steer wakes
|
|
494
648
|
// the driver, but a maintenance/abort window latches the wake for
|
|
@@ -516,8 +670,28 @@ export function registerWireFaces(webServer, d) {
|
|
|
516
670
|
// settle. The row itself stays: stall visibility and the
|
|
517
671
|
// nudge still apply to the TARGET side.
|
|
518
672
|
const receiptExpectation = receiptExpectationOf(receiptTarget);
|
|
519
|
-
|
|
520
|
-
|
|
673
|
+
// #114 S2: the row is born with its immutable authorization record.
|
|
674
|
+
const auth = buildAuthorization(admission, {
|
|
675
|
+
caller,
|
|
676
|
+
proof: callerProof,
|
|
677
|
+
...(claim.ok ? { claim: { caller: claim.caller, origin: originOf(claim.callerUrl) } } : {}),
|
|
678
|
+
ingress: 'wire',
|
|
679
|
+
allowlistVersion: admittedAt.version,
|
|
680
|
+
target: team,
|
|
681
|
+
receiptTarget,
|
|
682
|
+
receivedAt: admittedAt.at,
|
|
683
|
+
// Audit only: a cold session's activation window also reads as running.
|
|
684
|
+
targetBusy: runningBeforeSteer,
|
|
685
|
+
});
|
|
686
|
+
// #139: the member that actually received it answers for a canvas or bare-zone team.
|
|
687
|
+
const deliveredTo = sessionTeamOf(target);
|
|
688
|
+
// gh#141: a receipt owes no receipt — it is settlement traffic,
|
|
689
|
+
// recorded on the row it settled. Booking it would leave a debt
|
|
690
|
+
// nothing can pay, and a nudge chasing an answer nobody owes.
|
|
691
|
+
if (!deliveredReceipt) {
|
|
692
|
+
taskLedger.trackInbound(taskId, team, receiptTarget, contextId === '' ? undefined : contextId, receiptExpectation !== 'process-label', briefOf(message), auth, deliveredTo === team ? undefined : deliveredTo);
|
|
693
|
+
armAsyncNudge(target, team, taskId, config.asyncNudgeDelayMs);
|
|
694
|
+
}
|
|
521
695
|
const payload = JSON.stringify({
|
|
522
696
|
routed: true,
|
|
523
697
|
delivered: true,
|
|
@@ -528,16 +702,22 @@ export function registerWireFaces(webServer, d) {
|
|
|
528
702
|
task_status: 'TASK_STATE_DELIVERED',
|
|
529
703
|
artifacts: [],
|
|
530
704
|
consumed: consumedProbe,
|
|
531
|
-
receipt: receiptExpectation === 'process-label' ? 'none' : 'routed',
|
|
705
|
+
receipt: receiptExpectation === 'process-label' || deliveredReceipt ? 'none' : 'routed',
|
|
532
706
|
});
|
|
533
707
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
534
708
|
res.end(payload);
|
|
535
709
|
}
|
|
536
710
|
catch (error) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
711
|
+
if (!steered) {
|
|
712
|
+
refuseNoWait(`The DSH session rejected the message: ${String(error)}`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
logger.warn(`a2a: task ${taskId.slice(0, 80)} was delivered to ${team}, but its bookkeeping failed: ${String(error)}`);
|
|
716
|
+
if (!res.headersSent) {
|
|
717
|
+
const payload = JSON.stringify({ routed: true, delivered: true, team, task_id: taskId, context_id: contextId, task_status: 'TASK_STATE_DELIVERED', artifacts: [], consumed: false, receipt: 'routed' });
|
|
718
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
719
|
+
res.end(payload);
|
|
720
|
+
}
|
|
541
721
|
}
|
|
542
722
|
};
|
|
543
723
|
if (agent !== undefined) {
|
|
@@ -546,10 +726,7 @@ export function registerWireFaces(webServer, d) {
|
|
|
546
726
|
}
|
|
547
727
|
if (wake?.kind === 'flight') {
|
|
548
728
|
void wake.flight.then(deliver, (error) => {
|
|
549
|
-
|
|
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);
|
|
729
|
+
refuseNoWait(`waking the session for team "${team}" failed: ${String(error)}`);
|
|
553
730
|
});
|
|
554
731
|
return;
|
|
555
732
|
}
|
|
@@ -571,34 +748,26 @@ export function registerWireFaces(webServer, d) {
|
|
|
571
748
|
// get an honest failure, never its own dispatch echoed
|
|
572
749
|
// home plus an auto-receipt for work nobody did.
|
|
573
750
|
if (callerIsAgent(live, caller)) {
|
|
574
|
-
|
|
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);
|
|
751
|
+
refuseNoWait(selfRouteText(team));
|
|
578
752
|
return;
|
|
579
753
|
}
|
|
580
754
|
deliver(live);
|
|
581
755
|
return;
|
|
582
756
|
}
|
|
583
757
|
}
|
|
584
|
-
|
|
585
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
586
|
-
res.end(payload);
|
|
758
|
+
refuseNoWait(wake?.kind === 'no-face' ? noWakeFaceText(team) : wake?.kind === 'archived' ? archivedColdMatchText(team) : noColdMatchText(team), false);
|
|
587
759
|
};
|
|
588
760
|
const prepared = await nativeTeamsPrepare(team);
|
|
589
761
|
if (!prepared.ok) {
|
|
590
762
|
if (prepared.reason === 'error') {
|
|
591
|
-
|
|
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);
|
|
763
|
+
refuseNoWait(prepared.error);
|
|
595
764
|
return;
|
|
596
765
|
}
|
|
597
766
|
standardMiss();
|
|
598
767
|
return;
|
|
599
768
|
}
|
|
600
769
|
recordActivity('in', team, caller, true);
|
|
601
|
-
void nativeTeamsRound(prepared, team, caller, message, taskId)
|
|
770
|
+
void nativeTeamsRound(prepared, team, caller, message, taskId, proofClause, undefined, (_team, _taskId, from) => directHeader(from))
|
|
602
771
|
.then(outcome => {
|
|
603
772
|
if (!outcome.ok) {
|
|
604
773
|
recordActivity('in', team, caller, false);
|
|
@@ -622,7 +791,7 @@ export function registerWireFaces(webServer, d) {
|
|
|
622
791
|
})();
|
|
623
792
|
return;
|
|
624
793
|
}
|
|
625
|
-
void routeIntoAgent(team, message, caller, taskId).then((outcome) => {
|
|
794
|
+
void routeIntoAgent(team, message, caller, taskId, proofClause, directHeader).then((outcome) => {
|
|
626
795
|
recordActivity('in', team, caller, outcome.ok);
|
|
627
796
|
// W7 slice-2 hook 1: record the settled outcome on the claim row.
|
|
628
797
|
// Ruling: a non-COMPLETED status (TASK_STATE_DELIVERED — the
|