@nelsonlongxiang/dsh-open-a2a-net 0.6.36 → 0.6.38
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 +7 -4
- package/README_zh.md +7 -4
- package/assets/nexusDist/assets/index-C7HagaGy.css +1 -0
- package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +35 -5
- package/lib/a2a-client.js +3 -3
- package/lib/canvas-discovery.js +29 -0
- package/lib/client.js +63 -63
- package/lib/direct-forward.js +49 -27
- package/lib/faces/admin.js +30 -4
- package/lib/faces/canvas-notify.js +8 -6
- package/lib/faces/wire.js +29 -10
- package/lib/index.js +107 -24
- package/lib/origin-claim.js +31 -8
- package/lib/owner-settings.js +38 -1
- package/lib/peer-directory.js +1 -1
- package/lib/peer-link-display.js +15 -0
- package/lib/skill/references/cli.md +11 -3
- package/lib/skill/references/pitfalls.md +1 -1
- package/lib/skill/references/setup.md +9 -9
- package/lib/task-auth.js +1 -1
- package/lib/task-ledger-rows.js +220 -0
- package/lib/task-ledger.js +13 -196
- package/lib/tools/info.js +14 -5
- package/lib/types/a2a-client.d.ts +1 -0
- package/lib/types/canvas-discovery.d.ts +20 -0
- package/lib/types/direct-forward.d.ts +9 -0
- package/lib/types/faces/admin.d.ts +11 -0
- package/lib/types/faces/canvas-notify.d.ts +3 -1
- package/lib/types/faces/wire.d.ts +4 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/origin-claim.d.ts +16 -4
- package/lib/types/owner-settings.d.ts +5 -0
- package/lib/types/peer-link-display.d.ts +13 -0
- package/lib/types/task-auth.d.ts +2 -0
- package/lib/types/task-ledger-rows.d.ts +352 -0
- package/lib/types/task-ledger.d.ts +7 -288
- package/lib/types/tools/info.d.ts +5 -0
- package/lib/types/types.d.ts +2 -1
- package/nexus-stage/src/planning-member-handles.ts +28 -1
- package/nexus-stage/src/planning-menus.ts +12 -5
- package/nexus-stage/src/planning-view.ts +18 -14
- package/nexus-stage/src/seat-name-editor.ts +31 -0
- package/nexus-stage/src/session-drag.ts +64 -0
- package/nexus-stage/src/wb-directory.ts +2 -0
- package/nexus-stage/src/workbench.css +2 -0
- package/nexus-stage/src/workbench.ts +16 -5
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-DIQNDBy6.js +0 -9
- package/assets/nexusDist/assets/index-z33lHtLw.css +0 -1
package/lib/task-auth.js
CHANGED
|
@@ -487,7 +487,7 @@ export function judgeSettlement(row, settler, facts) {
|
|
|
487
487
|
return { ok: true, role: 'legacy' };
|
|
488
488
|
const target = inbound ? row.auth.target : row.team;
|
|
489
489
|
// #139: once the member that received the task is known, it alone answers for the team.
|
|
490
|
-
const members = row.deliveredTo !== undefined ? [row.deliveredTo] : facts.membersOf(target) ?? [];
|
|
490
|
+
const members = row.deliveredTo !== undefined ? [row.deliveredTo] : row.forwarded === true ? row.receiverMembers ?? [] : facts.membersOf(target) ?? [];
|
|
491
491
|
const local = inbound || row.peer === 'local' || row.peer.startsWith('local:');
|
|
492
492
|
const peerOrigin = local ? undefined : originOfUrl(row.peer);
|
|
493
493
|
// #139: the peer is its signing key, not one spelling of its address — a host
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/** Stateless ledger row types, classifiers and restore projections; moved verbatim for #258. */
|
|
2
|
+
import { parseAuthorization } from "./task-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Caller-ended summaries (abandon / cancel) share one isolation law: late
|
|
5
|
+
* receipts never rewrite the verdict — they only stamp lateReceiptAt, and
|
|
6
|
+
* the projected outcome collapses to 'abandoned'.
|
|
7
|
+
*/
|
|
8
|
+
export const CALLER_ENDED_PREFIX = /^caller-(abandoned|cancelled)/;
|
|
9
|
+
/** Hard cap on the owed book (pending + dead-lettered rows, most recent first). */
|
|
10
|
+
export const TASK_CAP = 64;
|
|
11
|
+
/** Hard cap on the settled archive (most recent kept). */
|
|
12
|
+
export const ARCHIVE_CAP = 64;
|
|
13
|
+
/** Longest receipt summary kept on a record (multi-line outcomes collapse). */
|
|
14
|
+
export const SUMMARY_CAP = 200;
|
|
15
|
+
/**
|
|
16
|
+
* Longest dispatch brief kept per row. A ledger row without its payload is
|
|
17
|
+
* unreadable to supervision — "direct-8f3a1c2b → dsh/ec4ce7f8, 3h old" says
|
|
18
|
+
* nothing about whether to nudge, abandon, or read the receipt. The brief is
|
|
19
|
+
* the first non-empty line of the message, capped; the full body never enters
|
|
20
|
+
* the ledger (it can be arbitrarily large and may quote another session's log).
|
|
21
|
+
*/
|
|
22
|
+
export const BRIEF_CAP = 120;
|
|
23
|
+
/** Reduce a dispatch body to one readable line: first non-empty line, collapsed, capped. */
|
|
24
|
+
export function briefOf(message) {
|
|
25
|
+
if (message === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
for (const raw of message.split('\n')) {
|
|
28
|
+
const line = raw.trim().replace(/\s+/g, ' ');
|
|
29
|
+
if (line !== '')
|
|
30
|
+
return line.length <= BRIEF_CAP ? line : `${line.slice(0, BRIEF_CAP - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Age at which an unsettled task auto-dead-letters. A dead row exits the
|
|
36
|
+
* owed view, disarms its async-nudge retry (a cold session cannot consume a
|
|
37
|
+
* re-steer anyway), and still accepts a late receipt from a revived target.
|
|
38
|
+
* Injectable per instance so tests can use short windows; sweeping stays
|
|
39
|
+
* lazy — every read/write entry point sweeps first.
|
|
40
|
+
*/
|
|
41
|
+
export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
42
|
+
export const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled', 'evicted'];
|
|
43
|
+
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
44
|
+
export const ECHO_SUMMARY = '(settled by content echo)';
|
|
45
|
+
const SENDER_KINDS = ['session', 'host', 'bridge', 'cli'];
|
|
46
|
+
/** Longest sender id kept; anything longer is malformed, not truncated. */
|
|
47
|
+
export const SENDER_ID_CAP = 256;
|
|
48
|
+
/**
|
|
49
|
+
* Restore-side allowlist for one persisted sender: a well-formed value comes
|
|
50
|
+
* back as a fresh object holding only kind, id and (when present) face; anything else is dropped whole (the row
|
|
51
|
+
* itself survives — a bad sender never costs the debt record).
|
|
52
|
+
* @param raw - the persisted value.
|
|
53
|
+
* @returns the sender, or undefined when absent or malformed.
|
|
54
|
+
*/
|
|
55
|
+
export function parseSender(raw) {
|
|
56
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
57
|
+
return undefined;
|
|
58
|
+
const { kind, id, face } = raw;
|
|
59
|
+
if (typeof kind !== 'string' || !SENDER_KINDS.includes(kind))
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof id !== 'string' || id === '' || id.length > SENDER_ID_CAP)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (face !== undefined && (typeof face !== 'string' || face === '' || face.length > SENDER_ID_CAP))
|
|
64
|
+
return undefined;
|
|
65
|
+
// `face` belongs to host senders only; on any other kind it is dropped, the sender kept.
|
|
66
|
+
return { kind: kind, id, ...(face !== undefined && kind === 'host' ? { face } : {}) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Spread helper for restore: `{ sender }` when the persisted value parses and
|
|
70
|
+
* the row is not inbound (an inbound row's originator is its caller, never a
|
|
71
|
+
* sender this host recorded), else nothing.
|
|
72
|
+
*/
|
|
73
|
+
export function withSender(raw, direction) {
|
|
74
|
+
if (direction === 'inbound')
|
|
75
|
+
return {};
|
|
76
|
+
const sender = parseSender(raw);
|
|
77
|
+
return sender === undefined ? {} : { sender };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* #114 S2: `{ auth }` when a persisted INBOUND row carries a record that
|
|
81
|
+
* parses whole (parseAuthorization) and belongs to THIS row — its target is
|
|
82
|
+
* the row's team and its receipt target the row's peer — else nothing: the
|
|
83
|
+
* row survives as a pre-S2 row, never with a partially adopted or borrowed
|
|
84
|
+
* record.
|
|
85
|
+
*/
|
|
86
|
+
export function withAuth(raw, entry, now) {
|
|
87
|
+
if (entry.direction !== 'inbound' && entry.direction !== 'outbound')
|
|
88
|
+
return {};
|
|
89
|
+
const auth = parseAuthorization(raw, now);
|
|
90
|
+
if (auth === undefined || auth.target !== entry.team)
|
|
91
|
+
return {};
|
|
92
|
+
if (entry.direction === 'inbound')
|
|
93
|
+
return auth.ingress === 'wire' && `local:${auth.receipt_target}` === entry.peer ? { auth } : {};
|
|
94
|
+
// Forwarded births retain the verified local CLI wire admission for audit, never borrowed remote authority.
|
|
95
|
+
if (entry.forwarded === true && typeof entry.peer === 'string' && !entry.peer.startsWith('local')) {
|
|
96
|
+
const sender = parseSender(entry.sender);
|
|
97
|
+
return auth.ingress === 'wire' && auth.proof === 'cli-identity' && sender?.kind === 'cli' && sender.id === auth.caller && auth.receipt_target === auth.caller ? { auth } : {};
|
|
98
|
+
}
|
|
99
|
+
return auth.ingress === 'local' && entry.peer === 'local' ? { auth } : {};
|
|
100
|
+
}
|
|
101
|
+
/** Routing and answer facts copied unchanged between a row and its archive entry. */
|
|
102
|
+
export function routingOf(row) {
|
|
103
|
+
return {
|
|
104
|
+
...(row.receiverMembers !== undefined ? { receiverMembers: [...row.receiverMembers] } : {}),
|
|
105
|
+
...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
|
|
106
|
+
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
107
|
+
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
108
|
+
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
109
|
+
...(row.deliveredTo !== undefined ? { deliveredTo: row.deliveredTo } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
|
|
113
|
+
export function birthFacts(peer, birth, team = '', sender, now = Date.now(), warn = console.warn) {
|
|
114
|
+
if (birth === undefined)
|
|
115
|
+
return {};
|
|
116
|
+
const fact = (v) => v !== undefined && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
117
|
+
const remote = peer !== 'local' && !peer.startsWith('local:');
|
|
118
|
+
const receiverMembers = remote && birth.forwarded === true && fact(birth.peerKey) ? parseReceiverMembers(birth.receiverMembers, warn) : undefined;
|
|
119
|
+
return {
|
|
120
|
+
...withAuth(birth.auth, { direction: 'outbound', team, peer, sender, forwarded: birth.forwarded }, now),
|
|
121
|
+
...(receiverMembers !== undefined ? { receiverMembers } : {}),
|
|
122
|
+
...(remote && birth.forwarded === true ? { forwarded: true } : {}),
|
|
123
|
+
...(remote && fact(birth.peerKey) ? { peerKey: birth.peerKey } : {}),
|
|
124
|
+
...(remote && fact(birth.peerHost) ? { peerHost: birth.peerHost } : {}),
|
|
125
|
+
...(fact(birth.deliveredTo) ? { deliveredTo: birth.deliveredTo } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
|
|
129
|
+
export const ROUTING_FACT_CAP = 256;
|
|
130
|
+
export function parseReceiverMembers(value, warn = console.warn) {
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (!Array.isArray(value)) {
|
|
134
|
+
warn('a2a: receiverMembers is not an array; ignored');
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const members = new Set();
|
|
138
|
+
if (value.length > 256)
|
|
139
|
+
warn('a2a: receiverMembers exceeds 256 entries; tail ignored');
|
|
140
|
+
for (const [index, member] of value.slice(0, 256).entries()) {
|
|
141
|
+
if (typeof member !== 'string' || !new RegExp('^[a-z0-9-]+/[0-9a-f]{8}(?:[0-9a-f]{8})?$', 'i').test(member)) {
|
|
142
|
+
warn('a2a: invalid receiverMembers entry at index ' + index + '; ignored');
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
members.add(member);
|
|
146
|
+
}
|
|
147
|
+
return members.size === 0 ? undefined : Object.freeze([...members]);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* #139: restore-side allowlist for the routing facts. Each is kept only in
|
|
151
|
+
* its shape — a non-empty string within the cap — and the peer facts only on
|
|
152
|
+
* a remote outbound row; anything else is dropped (the row survives as one
|
|
153
|
+
* born before the fields, judged by current membership and origin).
|
|
154
|
+
*/
|
|
155
|
+
export function withRouting(entry, warn = console.warn) {
|
|
156
|
+
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
157
|
+
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
158
|
+
const receiverMembers = remoteOutbound && entry.forwarded === true && fact(entry.peerKey) ? parseReceiverMembers(entry.receiverMembers, warn) : undefined;
|
|
159
|
+
return {
|
|
160
|
+
...(typeof entry.answeredAt === 'number' && Number.isFinite(entry.answeredAt) && entry.answeredAt >= 0 ? { answeredAt: entry.answeredAt } : {}),
|
|
161
|
+
...(receiverMembers !== undefined ? { receiverMembers } : {}),
|
|
162
|
+
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
163
|
+
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
164
|
+
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
165
|
+
...(fact(entry.deliveredTo) ? { deliveredTo: entry.deliveredTo } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
170
|
+
* fall back to the summary markers this ledger writes itself. The fallback
|
|
171
|
+
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
172
|
+
* only as exact as the legacy data allows.
|
|
173
|
+
* @param row - the archived record.
|
|
174
|
+
* @returns the archive path that ended it.
|
|
175
|
+
*/
|
|
176
|
+
export function archivedEndedBy(row) {
|
|
177
|
+
if (row.endedBy !== undefined)
|
|
178
|
+
return row.endedBy;
|
|
179
|
+
const summary = row.summary ?? '';
|
|
180
|
+
if (summary.startsWith('caller-cancelled'))
|
|
181
|
+
return 'cancelled';
|
|
182
|
+
if (summary.startsWith('caller-abandoned'))
|
|
183
|
+
return 'abandoned';
|
|
184
|
+
if (summary === ECHO_SUMMARY)
|
|
185
|
+
return 'echo';
|
|
186
|
+
return 'receipt';
|
|
187
|
+
}
|
|
188
|
+
/** Bound on ids and callers kept in settlement records; longer values are cut on write and refused on restore. */
|
|
189
|
+
export const SETTLEMENT_ID_CAP = 256;
|
|
190
|
+
/** Bound of the refusal ring (design §4.4). */
|
|
191
|
+
export const SETTLEMENT_REFUSAL_CAP = 64;
|
|
192
|
+
const SETTLEMENT_REASONS = ['settler-off-chain', 'settler-unverified', 'on-behalf-mismatch', 'echo-off-chain'];
|
|
193
|
+
const CHAIN_ROLES = ['target', 'target-member', 'host-face', 'compat', 'legacy', 'off-chain'];
|
|
194
|
+
const PROOF_VALUES = ['origin-claim', 'claim-mismatch', 'legacy-window', 'unbound', 'cli-identity', 'in-process', 'none'];
|
|
195
|
+
/** Restore-side allowlist for settledBy: whole or nothing. */
|
|
196
|
+
export function withSettledBy(raw) {
|
|
197
|
+
if (raw === null || typeof raw !== 'object')
|
|
198
|
+
return {};
|
|
199
|
+
const r = raw;
|
|
200
|
+
if (typeof r.caller !== 'string' || r.caller.length > SETTLEMENT_ID_CAP || typeof r.chain_role !== 'string' || !CHAIN_ROLES.includes(r.chain_role))
|
|
201
|
+
return {};
|
|
202
|
+
return { settledBy: { caller: r.caller, chain_role: r.chain_role } };
|
|
203
|
+
}
|
|
204
|
+
/** Restore-side allowlist for one refusal record: whole or nothing. */
|
|
205
|
+
export function parseRefusal(raw) {
|
|
206
|
+
if (raw === null || typeof raw !== 'object')
|
|
207
|
+
return undefined;
|
|
208
|
+
const r = raw;
|
|
209
|
+
const st = r.settler;
|
|
210
|
+
if (typeof r.taskId !== 'string' || r.taskId === '' || r.taskId.length > SETTLEMENT_ID_CAP || typeof r.at !== 'number' || !Number.isFinite(r.at))
|
|
211
|
+
return undefined;
|
|
212
|
+
if (typeof r.reason !== 'string' || !SETTLEMENT_REASONS.includes(r.reason) || typeof r.head !== 'string' || typeof r.enforced !== 'boolean')
|
|
213
|
+
return undefined;
|
|
214
|
+
if (st === null || typeof st !== 'object' || typeof st.caller !== 'string' || st.caller.length > SETTLEMENT_ID_CAP || typeof st.proof !== 'string' || !PROOF_VALUES.includes(st.proof) || (st.ingress !== 'wire' && st.ingress !== 'local'))
|
|
215
|
+
return undefined;
|
|
216
|
+
const count = r.count === undefined ? 1 : r.count;
|
|
217
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1)
|
|
218
|
+
return undefined;
|
|
219
|
+
return { taskId: r.taskId, at: r.at, settler: { caller: st.caller, proof: st.proof, ingress: st.ingress }, reason: r.reason, head: r.head.slice(0, SUMMARY_CAP), enforced: r.enforced, count };
|
|
220
|
+
}
|
package/lib/task-ledger.js
CHANGED
|
@@ -18,201 +18,12 @@
|
|
|
18
18
|
* illusion: settlements vanished while debts piled up).
|
|
19
19
|
* @module @nelsonlongxiang/dsh-open-a2a-net/task-ledger
|
|
20
20
|
*/
|
|
21
|
-
import { parseAuthorization } from "./task-auth.js";
|
|
22
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
22
|
import { dirname } from 'node:path';
|
|
24
23
|
import { parseReceipt, RECEIPT_OUTCOMES } from "./receipt.js";
|
|
25
24
|
import { classifyConservationBlock, ConservationCounters } from "./ledger-conservation.js";
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* receipts never rewrite the verdict — they only stamp lateReceiptAt, and
|
|
29
|
-
* the projected outcome collapses to 'abandoned'.
|
|
30
|
-
*/
|
|
31
|
-
const CALLER_ENDED_PREFIX = /^caller-(abandoned|cancelled)/;
|
|
32
|
-
/** Hard cap on the owed book (pending + dead-lettered rows, most recent first). */
|
|
33
|
-
export const TASK_CAP = 64;
|
|
34
|
-
/** Hard cap on the settled archive (most recent kept). */
|
|
35
|
-
export const ARCHIVE_CAP = 64;
|
|
36
|
-
/** Longest receipt summary kept on a record (multi-line outcomes collapse). */
|
|
37
|
-
export const SUMMARY_CAP = 200;
|
|
38
|
-
/**
|
|
39
|
-
* Longest dispatch brief kept per row. A ledger row without its payload is
|
|
40
|
-
* unreadable to supervision — "direct-8f3a1c2b → dsh/ec4ce7f8, 3h old" says
|
|
41
|
-
* nothing about whether to nudge, abandon, or read the receipt. The brief is
|
|
42
|
-
* the first non-empty line of the message, capped; the full body never enters
|
|
43
|
-
* the ledger (it can be arbitrarily large and may quote another session's log).
|
|
44
|
-
*/
|
|
45
|
-
export const BRIEF_CAP = 120;
|
|
46
|
-
/** Reduce a dispatch body to one readable line: first non-empty line, collapsed, capped. */
|
|
47
|
-
export function briefOf(message) {
|
|
48
|
-
if (message === undefined)
|
|
49
|
-
return undefined;
|
|
50
|
-
for (const raw of message.split('\n')) {
|
|
51
|
-
const line = raw.trim().replace(/\s+/g, ' ');
|
|
52
|
-
if (line !== '')
|
|
53
|
-
return line.length <= BRIEF_CAP ? line : `${line.slice(0, BRIEF_CAP - 1)}…`;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Age at which an unsettled task auto-dead-letters. A dead row exits the
|
|
59
|
-
* owed view, disarms its async-nudge retry (a cold session cannot consume a
|
|
60
|
-
* re-steer anyway), and still accepts a late receipt from a revived target.
|
|
61
|
-
* Injectable per instance so tests can use short windows; sweeping stays
|
|
62
|
-
* lazy — every read/write entry point sweeps first.
|
|
63
|
-
*/
|
|
64
|
-
export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
65
|
-
const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled', 'evicted'];
|
|
66
|
-
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
67
|
-
const ECHO_SUMMARY = '(settled by content echo)';
|
|
68
|
-
const SENDER_KINDS = ['session', 'host', 'bridge', 'cli'];
|
|
69
|
-
/** Longest sender id kept; anything longer is malformed, not truncated. */
|
|
70
|
-
export const SENDER_ID_CAP = 256;
|
|
71
|
-
/**
|
|
72
|
-
* Restore-side allowlist for one persisted sender: a well-formed value comes
|
|
73
|
-
* back as a fresh object holding only kind, id and (when present) face; anything else is dropped whole (the row
|
|
74
|
-
* itself survives — a bad sender never costs the debt record).
|
|
75
|
-
* @param raw - the persisted value.
|
|
76
|
-
* @returns the sender, or undefined when absent or malformed.
|
|
77
|
-
*/
|
|
78
|
-
export function parseSender(raw) {
|
|
79
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
80
|
-
return undefined;
|
|
81
|
-
const { kind, id, face } = raw;
|
|
82
|
-
if (typeof kind !== 'string' || !SENDER_KINDS.includes(kind))
|
|
83
|
-
return undefined;
|
|
84
|
-
if (typeof id !== 'string' || id === '' || id.length > SENDER_ID_CAP)
|
|
85
|
-
return undefined;
|
|
86
|
-
if (face !== undefined && (typeof face !== 'string' || face === '' || face.length > SENDER_ID_CAP))
|
|
87
|
-
return undefined;
|
|
88
|
-
// `face` belongs to host senders only; on any other kind it is dropped, the sender kept.
|
|
89
|
-
return { kind: kind, id, ...(face !== undefined && kind === 'host' ? { face } : {}) };
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Spread helper for restore: `{ sender }` when the persisted value parses and
|
|
93
|
-
* the row is not inbound (an inbound row's originator is its caller, never a
|
|
94
|
-
* sender this host recorded), else nothing.
|
|
95
|
-
*/
|
|
96
|
-
function withSender(raw, direction) {
|
|
97
|
-
if (direction === 'inbound')
|
|
98
|
-
return {};
|
|
99
|
-
const sender = parseSender(raw);
|
|
100
|
-
return sender === undefined ? {} : { sender };
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* #114 S2: `{ auth }` when a persisted INBOUND row carries a record that
|
|
104
|
-
* parses whole (parseAuthorization) and belongs to THIS row — its target is
|
|
105
|
-
* the row's team and its receipt target the row's peer — else nothing: the
|
|
106
|
-
* row survives as a pre-S2 row, never with a partially adopted or borrowed
|
|
107
|
-
* record.
|
|
108
|
-
*/
|
|
109
|
-
function withAuth(raw, entry, now) {
|
|
110
|
-
if (entry.direction !== 'inbound' && entry.direction !== 'outbound')
|
|
111
|
-
return {};
|
|
112
|
-
const auth = parseAuthorization(raw, now);
|
|
113
|
-
if (auth === undefined || auth.target !== entry.team)
|
|
114
|
-
return {};
|
|
115
|
-
if (entry.direction === 'inbound')
|
|
116
|
-
return auth.ingress === 'wire' && `local:${auth.receipt_target}` === entry.peer ? { auth } : {};
|
|
117
|
-
// #139: an outbound row carries only its local-candidate admission.
|
|
118
|
-
return auth.ingress === 'local' && entry.peer === 'local' ? { auth } : {};
|
|
119
|
-
}
|
|
120
|
-
/** Routing and answer facts copied unchanged between a row and its archive entry. */
|
|
121
|
-
function routingOf(row) {
|
|
122
|
-
return {
|
|
123
|
-
...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
|
|
124
|
-
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
125
|
-
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
126
|
-
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
127
|
-
...(row.deliveredTo !== undefined ? { deliveredTo: row.deliveredTo } : {}),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
/** The facts of one outbound birth that fit the row (see {@link withRouting} for the same shape on restore). */
|
|
131
|
-
function birthFacts(peer, birth) {
|
|
132
|
-
if (birth === undefined)
|
|
133
|
-
return {};
|
|
134
|
-
const fact = (v) => v !== undefined && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
135
|
-
const remote = peer !== 'local' && !peer.startsWith('local:');
|
|
136
|
-
return {
|
|
137
|
-
...(!remote && birth.auth !== undefined && birth.auth.ingress === 'local' ? { auth: birth.auth } : {}),
|
|
138
|
-
...(remote && birth.forwarded === true ? { forwarded: true } : {}),
|
|
139
|
-
...(remote && fact(birth.peerKey) ? { peerKey: birth.peerKey } : {}),
|
|
140
|
-
...(remote && fact(birth.peerHost) ? { peerHost: birth.peerHost } : {}),
|
|
141
|
-
...(fact(birth.deliveredTo) ? { deliveredTo: birth.deliveredTo } : {}),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/** Bound on a persisted routing fact; anything longer (or empty, or not a string) is dropped on restore. */
|
|
145
|
-
const ROUTING_FACT_CAP = 256;
|
|
146
|
-
/**
|
|
147
|
-
* #139: restore-side allowlist for the routing facts. Each is kept only in
|
|
148
|
-
* its shape — a non-empty string within the cap — and the peer facts only on
|
|
149
|
-
* a remote outbound row; anything else is dropped (the row survives as one
|
|
150
|
-
* born before the fields, judged by current membership and origin).
|
|
151
|
-
*/
|
|
152
|
-
function withRouting(entry) {
|
|
153
|
-
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
154
|
-
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
155
|
-
return {
|
|
156
|
-
...(typeof entry.answeredAt === 'number' && Number.isFinite(entry.answeredAt) && entry.answeredAt >= 0 ? { answeredAt: entry.answeredAt } : {}),
|
|
157
|
-
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
158
|
-
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
159
|
-
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
160
|
-
...(fact(entry.deliveredTo) ? { deliveredTo: entry.deliveredTo } : {}),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
165
|
-
* fall back to the summary markers this ledger writes itself. The fallback
|
|
166
|
-
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
167
|
-
* only as exact as the legacy data allows.
|
|
168
|
-
* @param row - the archived record.
|
|
169
|
-
* @returns the archive path that ended it.
|
|
170
|
-
*/
|
|
171
|
-
export function archivedEndedBy(row) {
|
|
172
|
-
if (row.endedBy !== undefined)
|
|
173
|
-
return row.endedBy;
|
|
174
|
-
const summary = row.summary ?? '';
|
|
175
|
-
if (summary.startsWith('caller-cancelled'))
|
|
176
|
-
return 'cancelled';
|
|
177
|
-
if (summary.startsWith('caller-abandoned'))
|
|
178
|
-
return 'abandoned';
|
|
179
|
-
if (summary === ECHO_SUMMARY)
|
|
180
|
-
return 'echo';
|
|
181
|
-
return 'receipt';
|
|
182
|
-
}
|
|
183
|
-
/** Bound on ids and callers kept in settlement records; longer values are cut on write and refused on restore. */
|
|
184
|
-
const SETTLEMENT_ID_CAP = 256;
|
|
185
|
-
/** Bound of the refusal ring (design §4.4). */
|
|
186
|
-
export const SETTLEMENT_REFUSAL_CAP = 64;
|
|
187
|
-
const SETTLEMENT_REASONS = ['settler-off-chain', 'settler-unverified', 'on-behalf-mismatch', 'echo-off-chain'];
|
|
188
|
-
const CHAIN_ROLES = ['target', 'target-member', 'host-face', 'compat', 'legacy', 'off-chain'];
|
|
189
|
-
const PROOF_VALUES = ['origin-claim', 'claim-mismatch', 'legacy-window', 'unbound', 'cli-identity', 'in-process', 'none'];
|
|
190
|
-
/** Restore-side allowlist for settledBy: whole or nothing. */
|
|
191
|
-
function withSettledBy(raw) {
|
|
192
|
-
if (raw === null || typeof raw !== 'object')
|
|
193
|
-
return {};
|
|
194
|
-
const r = raw;
|
|
195
|
-
if (typeof r.caller !== 'string' || r.caller.length > SETTLEMENT_ID_CAP || typeof r.chain_role !== 'string' || !CHAIN_ROLES.includes(r.chain_role))
|
|
196
|
-
return {};
|
|
197
|
-
return { settledBy: { caller: r.caller, chain_role: r.chain_role } };
|
|
198
|
-
}
|
|
199
|
-
/** Restore-side allowlist for one refusal record: whole or nothing. */
|
|
200
|
-
function parseRefusal(raw) {
|
|
201
|
-
if (raw === null || typeof raw !== 'object')
|
|
202
|
-
return undefined;
|
|
203
|
-
const r = raw;
|
|
204
|
-
const st = r.settler;
|
|
205
|
-
if (typeof r.taskId !== 'string' || r.taskId === '' || r.taskId.length > SETTLEMENT_ID_CAP || typeof r.at !== 'number' || !Number.isFinite(r.at))
|
|
206
|
-
return undefined;
|
|
207
|
-
if (typeof r.reason !== 'string' || !SETTLEMENT_REASONS.includes(r.reason) || typeof r.head !== 'string' || typeof r.enforced !== 'boolean')
|
|
208
|
-
return undefined;
|
|
209
|
-
if (st === null || typeof st !== 'object' || typeof st.caller !== 'string' || st.caller.length > SETTLEMENT_ID_CAP || typeof st.proof !== 'string' || !PROOF_VALUES.includes(st.proof) || (st.ingress !== 'wire' && st.ingress !== 'local'))
|
|
210
|
-
return undefined;
|
|
211
|
-
const count = r.count === undefined ? 1 : r.count;
|
|
212
|
-
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1)
|
|
213
|
-
return undefined;
|
|
214
|
-
return { taskId: r.taskId, at: r.at, settler: { caller: st.caller, proof: st.proof, ingress: st.ingress }, reason: r.reason, head: r.head.slice(0, SUMMARY_CAP), enforced: r.enforced, count };
|
|
215
|
-
}
|
|
25
|
+
import { CALLER_ENDED_PREFIX, TASK_CAP, ARCHIVE_CAP, SUMMARY_CAP, TASK_STALE_TTL_MS, ARCHIVE_ENDS, ECHO_SUMMARY, ROUTING_FACT_CAP, SETTLEMENT_ID_CAP, SETTLEMENT_REFUSAL_CAP, withSender, withAuth, routingOf, birthFacts, withRouting, withSettledBy, parseRefusal, archivedEndedBy } from "./task-ledger-rows.js";
|
|
26
|
+
export * from "./task-ledger-rows.js";
|
|
216
27
|
/**
|
|
217
28
|
* Bounded outbound-task ledger with whole-file persistence. Pure of any
|
|
218
29
|
* routing knowledge: callers {@link track} a task when a route leaves it
|
|
@@ -235,6 +46,7 @@ export class TaskLedger {
|
|
|
235
46
|
archiveCap;
|
|
236
47
|
/** Injectable clock (option `now`). */
|
|
237
48
|
now;
|
|
49
|
+
warn;
|
|
238
50
|
/**
|
|
239
51
|
* @param path - persistence file (`<dsh-home>/a2a/tasks.json`); empty = no persistence.
|
|
240
52
|
* @param options - TTL/capacity overrides; defaults come from the exported constants.
|
|
@@ -244,6 +56,7 @@ export class TaskLedger {
|
|
|
244
56
|
this.staleTtlMs = options?.staleTtlMs ?? TASK_STALE_TTL_MS;
|
|
245
57
|
this.archiveCap = options?.archiveCap ?? ARCHIVE_CAP;
|
|
246
58
|
this.now = options?.now ?? (() => Date.now());
|
|
59
|
+
this.warn = options?.warn ?? console.warn;
|
|
247
60
|
this.restore();
|
|
248
61
|
}
|
|
249
62
|
/**
|
|
@@ -255,6 +68,10 @@ export class TaskLedger {
|
|
|
255
68
|
this.sweep(this.now());
|
|
256
69
|
return [...this.tasks];
|
|
257
70
|
}
|
|
71
|
+
/** #275: old forwarded canvas debts cannot be safely upgraded by inventing receiver authority. */
|
|
72
|
+
settlementEnforceBlockers() {
|
|
73
|
+
return this.list().filter(row => row.direction === 'outbound' && row.forwarded === true && row.receiptExpected !== false && row.team.includes('/canvas/') && (row.deliveredTo === row.team || (row.deliveredTo === undefined && (row.receiverMembers === undefined || row.receiverMembers.length === 0)))).map(row => row.taskId);
|
|
74
|
+
}
|
|
258
75
|
/**
|
|
259
76
|
* The settled archive, most recently resolved first. Rows land here the
|
|
260
77
|
* moment their receipt correlates and stay until the archive cap evicts
|
|
@@ -302,7 +119,7 @@ export class TaskLedger {
|
|
|
302
119
|
...(contextId !== undefined && contextId !== '' ? { contextId } : {}),
|
|
303
120
|
...(brief !== undefined && brief !== '' ? { brief } : {}),
|
|
304
121
|
...(sender !== undefined ? { sender } : {}),
|
|
305
|
-
...birthFacts(peer, birth),
|
|
122
|
+
...birthFacts(peer, birth, team, sender, this.now(), this.warn),
|
|
306
123
|
status: 'pending',
|
|
307
124
|
}, ...this.tasks];
|
|
308
125
|
this.trimOwed();
|
|
@@ -326,7 +143,7 @@ export class TaskLedger {
|
|
|
326
143
|
...(contextId !== undefined && contextId !== '' ? { contextId } : {}),
|
|
327
144
|
...(brief !== undefined && brief !== '' ? { brief } : {}),
|
|
328
145
|
...(sender !== undefined ? { sender } : {}),
|
|
329
|
-
...birthFacts(peer, birth),
|
|
146
|
+
...birthFacts(peer, birth, team, sender, this.now(), this.warn),
|
|
330
147
|
status: 'pending',
|
|
331
148
|
}, ...this.tasks];
|
|
332
149
|
this.trimOwed();
|
|
@@ -818,7 +635,7 @@ export class TaskLedger {
|
|
|
818
635
|
...(entry.receiptExpected === false ? { receiptExpected: false } : {}),
|
|
819
636
|
...withSender(entry.sender, entry.direction),
|
|
820
637
|
...withAuth(entry.auth, entry, this.now()),
|
|
821
|
-
...withRouting(entry),
|
|
638
|
+
...withRouting(entry, this.warn),
|
|
822
639
|
...withSettledBy(entry.settledBy),
|
|
823
640
|
});
|
|
824
641
|
continue;
|
|
@@ -837,7 +654,7 @@ export class TaskLedger {
|
|
|
837
654
|
// #114 S3: a malformed sender is dropped whole; the row survives.
|
|
838
655
|
...withSender(entry.sender, entry.direction),
|
|
839
656
|
...withAuth(entry.auth, entry, this.now()),
|
|
840
|
-
...withRouting(entry),
|
|
657
|
+
...withRouting(entry, this.warn),
|
|
841
658
|
status: entry.status === 'dead' ? 'dead' : 'pending',
|
|
842
659
|
...(entry.status === 'dead'
|
|
843
660
|
? {
|
|
@@ -875,7 +692,7 @@ export class TaskLedger {
|
|
|
875
692
|
...(entry.receiptExpected === false ? { receiptExpected: false } : {}),
|
|
876
693
|
...withSender(entry.sender, entry.direction),
|
|
877
694
|
...withAuth(entry.auth, entry, this.now()),
|
|
878
|
-
...withRouting(entry),
|
|
695
|
+
...withRouting(entry, this.warn),
|
|
879
696
|
...withSettledBy(entry.settledBy),
|
|
880
697
|
});
|
|
881
698
|
}
|
package/lib/tools/info.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
11
11
|
import { peerDirectoryRows } from "../peer-directory.js";
|
|
12
|
+
import { canvasMemberships, canvasOwners } from "../canvas-discovery.js";
|
|
12
13
|
import { peerContentMeta } from "../node-self.js";
|
|
13
14
|
/**
|
|
14
15
|
* Build the five registrars. Call createInfoTools after its deps exist, then
|
|
@@ -16,7 +17,7 @@ import { peerContentMeta } from "../node-self.js";
|
|
|
16
17
|
* registry order the model sees must not change.
|
|
17
18
|
*/
|
|
18
19
|
export function createInfoTools(deps) {
|
|
19
|
-
const { ctx, config, logger, session, lanIp,
|
|
20
|
+
const { ctx, config, logger, session, lanIp, peerStore, taskLedger, joinedSessions, teamMemberships, canvasStore, exposureStore, idempotencyStore, cardCache, inFlightRoutes, recentActivity, teamAuditRing, reportRing, sessionNodes, id8, sessionTeamOf, nodeMetadataOf, canvasTeamOf, canvasTeamsOfSession, localHandleResolver, memoizedCardFetch, mapBounded, SWEEP_CONCURRENCY, a2aJoinGateRefusal, observeAuthorization, settlePeerCard, client, listDirectoryTeams, } = deps;
|
|
20
21
|
// S5 (owner directive 2026-10-20): information-plane scoping. The host's
|
|
21
22
|
// joined-team scope is the union of every local node's roster
|
|
22
23
|
// declarations — a host-level set, not per-session: supervisor seats need
|
|
@@ -104,7 +105,7 @@ export function createInfoTools(deps) {
|
|
|
104
105
|
teams: {
|
|
105
106
|
type: 'object',
|
|
106
107
|
additionalProperties: true,
|
|
107
|
-
description: 'roster: allowlisted teams declared via a2a_team_join; canvas: workbench-assigned
|
|
108
|
+
description: 'roster: allowlisted teams declared via a2a_team_join; canvas: local workbench-assigned names (compatibility); canvasDetails: local/remote membership with qualified team, owner URL and memberCount.',
|
|
108
109
|
},
|
|
109
110
|
allowlist: {
|
|
110
111
|
type: 'object',
|
|
@@ -124,7 +125,7 @@ export function createInfoTools(deps) {
|
|
|
124
125
|
type: 'text',
|
|
125
126
|
text: value.face === 'host'
|
|
126
127
|
? `Host face on team ${String(value.node?.team ?? '?')}: routing rides the bare node team (能发即能收)`
|
|
127
|
-
: `Node ${String(value.node?.id ?? '?')}: joined=${String(value.node?.joined)}, live=${String(value.node?.live)}; roster teams: ${(value.teams?.roster ?? []).join(', ') || 'none'}; canvas: ${(value.teams?.canvas ?? []).join(', ') || 'none'}; allowlist effect: ${String(value.allowlist?.effect ?? '?')}`,
|
|
128
|
+
: `Node ${String(value.node?.name ?? '')} ${String(value.node?.id ?? '?')}: joined=${String(value.node?.joined)}, live=${String(value.node?.live)}; roster teams: ${(value.teams?.roster ?? []).join(', ') || 'none'}; canvas: ${(value.teams?.canvas ?? []).join(', ') || 'none'}; canvas owners: ${JSON.stringify(value.teams?.canvasDetails ?? [])}; allowlist effect: ${String(value.allowlist?.effect ?? '?')}`,
|
|
128
129
|
}],
|
|
129
130
|
},
|
|
130
131
|
presentCall: () => ({ card: 'generic', title: 'A2A whoami', kind: 'other', rawInput: null }),
|
|
@@ -140,6 +141,8 @@ export function createInfoTools(deps) {
|
|
|
140
141
|
}
|
|
141
142
|
const joined = joinedSessions.has(agentId);
|
|
142
143
|
const live = sessionNodes.has(agentId);
|
|
144
|
+
await listDirectoryTeams(false, true); // refresh through the existing bounded directory, not a second discovery path
|
|
145
|
+
const canvasDetails = canvasMemberships(canvasOwners(canvasStore.list().map(name => ({ name, team: canvasTeamOf(name), members: canvasStore.membersOf(name) })), deps.ownCardUrl, [...cardCache.entries()].flatMap(([url, entry]) => entry.card === undefined ? [] : [{ url, card: entry.card }])), `${config.team}/${id8(agentId)}`, agentId, localHandleResolver());
|
|
143
146
|
const refusals = teamAuditRing
|
|
144
147
|
.filter(entry => entry.id === agentId && !entry.ok)
|
|
145
148
|
.slice(-3)
|
|
@@ -151,6 +154,7 @@ export function createInfoTools(deps) {
|
|
|
151
154
|
face: 'session',
|
|
152
155
|
node: {
|
|
153
156
|
id: agentId,
|
|
157
|
+
...(exec.agent !== undefined ? { name: nodeMetadataOf(exec.agent).name, ...(nodeMetadataOf(exec.agent).seatName !== undefined ? { seatName: nodeMetadataOf(exec.agent).seatName } : {}) } : {}),
|
|
154
158
|
...(joined ? { team: `${config.team}/${id8(agentId)}` } : {}),
|
|
155
159
|
joined,
|
|
156
160
|
live,
|
|
@@ -158,6 +162,7 @@ export function createInfoTools(deps) {
|
|
|
158
162
|
teams: {
|
|
159
163
|
roster: [...teamMemberships.teamsOf(agentId)],
|
|
160
164
|
canvas: [...canvasTeamsOfSession(agentId)],
|
|
165
|
+
canvasDetails,
|
|
161
166
|
},
|
|
162
167
|
allowlist: {
|
|
163
168
|
patterns,
|
|
@@ -209,6 +214,10 @@ export function createInfoTools(deps) {
|
|
|
209
214
|
team: { type: 'string', required: true },
|
|
210
215
|
session: { type: 'string', required: true },
|
|
211
216
|
name: { type: 'string', required: true },
|
|
217
|
+
seatName: { type: 'string', description: 'Human-owned stable display label, never a route identity.' },
|
|
218
|
+
kind: { type: 'string', enum: ['canvas'] },
|
|
219
|
+
owner: { type: 'string', description: 'Canvas owner host URL; discovery metadata, not a --home requirement.' },
|
|
220
|
+
memberCount: { type: 'number' },
|
|
212
221
|
description: { type: 'string', required: true },
|
|
213
222
|
local: { type: 'boolean', description: 'true when the team is served by this host (loopback candidate).' },
|
|
214
223
|
origin: { type: 'string', description: 'The publishing host (node session label, LAN IP when known) — the natural grouping for fleet rows.' },
|
|
@@ -226,7 +235,7 @@ export function createInfoTools(deps) {
|
|
|
226
235
|
type: 'text',
|
|
227
236
|
text: (value.teams.length === 0
|
|
228
237
|
? `No A2A teams${(value.query ?? '') === '' ? '' : ` matching "${String(args.query)}"`} are currently reachable.`
|
|
229
|
-
: value.teams.map(team => `- ${team.team}${team.local === true ? ' [this host]' : ''}${team.name === '' ? '' : ` (${team.name})`}${team.description === '' ? '' : ` — ${team.description}`}${typeof team.origin === 'string' && team.origin !== '' && team.local !== true ? ` @ ${team.origin}` : ''}${typeof team.workspace === 'string' && team.workspace !== '' ? ` [${team.workspace}]` : ''}`).join('\n'))
|
|
238
|
+
: value.teams.map(team => `- ${team.team}${team.local === true ? ' [this host]' : ''}${team.name === '' ? '' : ` (${team.name})`}${team.kind === 'canvas' ? ` [canvas owner=${team.owner ?? '?'} members=${String(team.memberCount ?? '?')}]` : ''}${team.description === '' ? '' : ` — ${team.description}`}${typeof team.origin === 'string' && team.origin !== '' && team.local !== true ? ` @ ${team.origin}` : ''}${typeof team.workspace === 'string' && team.workspace !== '' ? ` [${team.workspace}]` : ''}`).join('\n'))
|
|
230
239
|
+ (value.note === undefined ? '' : `\n${value.note}`),
|
|
231
240
|
}],
|
|
232
241
|
},
|
|
@@ -299,7 +308,7 @@ export function createInfoTools(deps) {
|
|
|
299
308
|
return { ok: true, query: '', teams, ...(note !== undefined ? { note } : {}) };
|
|
300
309
|
const matches = (team) => team.team.toLowerCase().includes(query) || team.name.toLowerCase().includes(query) || team.description.toLowerCase().includes(query)
|
|
301
310
|
|| team.session.toLowerCase().includes(query)
|
|
302
|
-
|| (team.origin ?? '').toLowerCase().includes(query) || (team.workspace ?? '').toLowerCase().includes(query) || (team.via ?? '').toLowerCase().includes(query);
|
|
311
|
+
|| (team.owner ?? '').toLowerCase().includes(query) || (team.origin ?? '').toLowerCase().includes(query) || (team.workspace ?? '').toLowerCase().includes(query) || (team.via ?? '').toLowerCase().includes(query);
|
|
303
312
|
return { ok: true, query, teams: teams.filter(matches), ...(note !== undefined ? { note } : {}) };
|
|
304
313
|
},
|
|
305
314
|
}));
|
|
@@ -45,6 +45,7 @@ export interface A2aClientOptions {
|
|
|
45
45
|
readonly originClaim?: (target: {
|
|
46
46
|
readonly url: string;
|
|
47
47
|
readonly method: string;
|
|
48
|
+
readonly body?: string;
|
|
48
49
|
}) => Record<string, string>;
|
|
49
50
|
/** Delayed-callback seam (HTTP timeouts). */
|
|
50
51
|
readonly schedule: A2aSchedule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { A2aPeerCard } from './types.ts';
|
|
2
|
+
export interface CanvasOwner {
|
|
3
|
+
team: string;
|
|
4
|
+
name: string;
|
|
5
|
+
owner: string;
|
|
6
|
+
memberCount: number;
|
|
7
|
+
local: boolean;
|
|
8
|
+
members: readonly string[];
|
|
9
|
+
scopeMembers?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function canvasOwners(local: readonly {
|
|
12
|
+
name: string;
|
|
13
|
+
team: string;
|
|
14
|
+
members: readonly string[];
|
|
15
|
+
}[], ownUrl: string, peers: readonly {
|
|
16
|
+
url: string;
|
|
17
|
+
card: A2aPeerCard;
|
|
18
|
+
}[]): CanvasOwner[];
|
|
19
|
+
export declare function canvasMemberships(owners: readonly CanvasOwner[], handle: string, localId?: string, resolveLocal?: (ref: string) => string | undefined): Omit<CanvasOwner, 'members' | 'scopeMembers'>[];
|
|
20
|
+
export declare function canvasFailure(team: string, owners: readonly CanvasOwner[], reason: string): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TaskLedger } 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,8 +10,16 @@ export interface ForwardRequest {
|
|
|
9
10
|
noWait: boolean;
|
|
10
11
|
localCliProof: boolean;
|
|
11
12
|
guarded: boolean;
|
|
13
|
+
auth?: TaskAuthorization;
|
|
14
|
+
}
|
|
15
|
+
/** Birth barrier exists only for eligible, actively dispatched tasks; receipts never reserve a slot. */
|
|
16
|
+
export declare class ForwardBirths {
|
|
17
|
+
private readonly pending;
|
|
18
|
+
wait(taskId: string): Promise<void> | undefined;
|
|
19
|
+
begin(taskId: string): (() => void) | undefined;
|
|
12
20
|
}
|
|
13
21
|
export interface DirectForwardDeps {
|
|
22
|
+
births: ForwardBirths;
|
|
14
23
|
enabled(): boolean;
|
|
15
24
|
isolated(): boolean;
|
|
16
25
|
deposited(handle: string): boolean;
|