@nelsonlongxiang/dsh-open-a2a-net 0.6.32 → 0.6.34
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 +12 -3
- package/README_zh.md +12 -3
- package/assets/nexusDist/assets/index-D2BGWOeU.js +9 -0
- package/assets/nexusDist/assets/index-z33lHtLw.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +5 -2
- package/lib/a2a-client.js +9 -2
- package/lib/auth-observer.js +1 -1
- package/lib/canvas-store.js +50 -6
- package/lib/card.js +2 -1
- package/lib/cli-home.js +44 -0
- package/lib/client/A2aControl.js +1 -1
- package/lib/client.js +65 -65
- package/lib/client.js.map +1 -1
- package/lib/direct-forward.js +61 -0
- package/lib/faces/admin.js +84 -9
- package/lib/faces/wire.js +57 -16
- package/lib/index.js +118 -67
- package/lib/layout-store.js +3 -2
- package/lib/outbound-scope.js +25 -0
- package/lib/queue-guard.js +37 -0
- package/lib/roster-op.js +104 -0
- package/lib/skill/references/cli.md +6 -5
- package/lib/skill/references/pitfalls.md +1 -4
- package/lib/skill/references/router.md +1 -1
- package/lib/task-auth.js +52 -3
- package/lib/task-ledger.js +4 -1
- package/lib/team-store.js +49 -1
- package/lib/tools/info.js +19 -3
- package/lib/types/a2a-client.d.ts +1 -1
- package/lib/types/auth-observer.d.ts +2 -2
- package/lib/types/canvas-store.d.ts +14 -1
- package/lib/types/cli-home.d.ts +24 -0
- package/lib/types/client/A2aControl.d.ts +1 -1
- package/lib/types/direct-forward.d.ts +32 -0
- package/lib/types/faces/admin.d.ts +9 -1
- package/lib/types/faces/wire.d.ts +13 -4
- package/lib/types/index.d.ts +4 -0
- package/lib/types/outbound-scope.d.ts +21 -0
- package/lib/types/queue-guard.d.ts +32 -0
- package/lib/types/roster-op.d.ts +21 -0
- package/lib/types/runtime.d.ts +1 -1
- package/lib/types/task-auth.d.ts +17 -2
- package/lib/types/task-ledger.d.ts +6 -1
- package/lib/types/team-store.d.ts +7 -0
- package/lib/types/types.d.ts +8 -0
- package/nexus-stage/src/canvas-ops.ts +2 -1
- package/nexus-stage/src/cli-home-picker.ts +42 -0
- package/nexus-stage/src/layout-doc.ts +6 -4
- package/nexus-stage/src/main.ts +36 -4
- package/nexus-stage/src/planning-card.ts +61 -0
- package/nexus-stage/src/planning-frame-hue.ts +13 -0
- package/nexus-stage/src/planning-legend.ts +1 -0
- package/nexus-stage/src/planning-menus.ts +63 -12
- package/nexus-stage/src/planning-roster.ts +15 -0
- package/nexus-stage/src/planning-types.ts +1 -1
- package/nexus-stage/src/planning-view.ts +43 -59
- package/nexus-stage/src/planning.css +19 -0
- package/nexus-stage/src/workbench.css +4 -0
- package/nexus-stage/src/workbench.ts +24 -5
- package/nexus-stage/src/world.ts +5 -1
- package/package.json +4 -2
- package/assets/nexusDist/assets/index-D5H8R8de.js +0 -9
- package/assets/nexusDist/assets/index-D9GYZyGZ.css +0 -1
package/lib/task-auth.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* @module @nelsonlongxiang/dsh-open-a2a-net/task-auth
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
|
+
import { CLI_HANDLE_RE } from "./cli-deposit.js";
|
|
17
|
+
import { cliHomeOrigin } from "./cli-home.js";
|
|
16
18
|
/** Every rule's basis — the mapping the authorization record stores beside the rule. */
|
|
17
19
|
export const VIA_OF = {
|
|
18
20
|
'scope-off': 'open',
|
|
@@ -22,6 +24,7 @@ export const VIA_OF = {
|
|
|
22
24
|
'canvas-shared': 'canvas-member',
|
|
23
25
|
'remote-ref-samezone': 'canvas-member',
|
|
24
26
|
'remote-ref-foreign': 'canvas-member',
|
|
27
|
+
'remote-ref-cli': 'canvas-member',
|
|
25
28
|
'peer-org': 'peer-org',
|
|
26
29
|
'node-fact': 'joined-session',
|
|
27
30
|
'cli-deposit': 'cli-deposit',
|
|
@@ -209,7 +212,7 @@ export function localCandidateAdmission(r, callerSession, targetTeam) {
|
|
|
209
212
|
return joinedFaceAdmission(r, callerSession, targetTeam) ?? admitted('local-exempt');
|
|
210
213
|
}
|
|
211
214
|
/** Inbound admission for the peer faces (/a2a/direct, /a2a/log): caller_session is the contract. */
|
|
212
|
-
export function callerAdmission(r, caller, targetTeam) {
|
|
215
|
+
export function callerAdmission(r, caller, targetTeam, cliProof) {
|
|
213
216
|
if (!r.teamScopeRouting)
|
|
214
217
|
return admitted('scope-off');
|
|
215
218
|
if (caller === '') {
|
|
@@ -227,6 +230,21 @@ export function callerAdmission(r, caller, targetTeam) {
|
|
|
227
230
|
return admitted('remote-ref-foreign', basis);
|
|
228
231
|
return refused(`caller "${caller}" has no operator-established link on this node — cross-host admission requires a canvas remote membership this host's operator added (N3/N4: the caller's own card claims are self-description, not links). Ask this host's operator to add the member in the A2A workbench, or route through a linked member.`);
|
|
229
232
|
}
|
|
233
|
+
// A foreign CLI handle is never a free-form node fact: its operator link,
|
|
234
|
+
// pinned home, bound signer, and fresh online row must ALL hold.
|
|
235
|
+
if (CLI_HANDLE_RE.test(caller) && !r.cliDeposited(caller)) {
|
|
236
|
+
const basis = r.canvasNames().filter(name => r.canvasRemoteMembersOf(name).includes(caller)).map(name => r.canvasTeamOf(name));
|
|
237
|
+
if (basis.length === 0)
|
|
238
|
+
return refused(`remote CLI ${caller} has no operator-established canvas link`);
|
|
239
|
+
const home = r.cliHomeOf?.(caller);
|
|
240
|
+
if (home === undefined)
|
|
241
|
+
return refused(`remote CLI ${caller} home is not pinned`);
|
|
242
|
+
if (cliProof?.bound !== true || cliHomeOrigin(cliProof.origin) !== home || cliHomeOrigin(cliProof.viaUrl) !== home)
|
|
243
|
+
return refused(`remote CLI ${caller} signer does not match pinned home`);
|
|
244
|
+
if (cliProof.online !== true)
|
|
245
|
+
return refused(`remote CLI ${caller} is offline or home card could not be read`);
|
|
246
|
+
return admitted('remote-ref-cli', basis);
|
|
247
|
+
}
|
|
230
248
|
// Free-form label: only this host's own node facts (V3).
|
|
231
249
|
if (r.joined().includes(caller))
|
|
232
250
|
return admitted('node-fact');
|
|
@@ -241,6 +259,18 @@ export function callerAdmission(r, caller, targetTeam) {
|
|
|
241
259
|
return admitted('cli-deposit');
|
|
242
260
|
return refused(`caller_session "${caller}" matches no node fact on this host (joined session id, live node label, or deposited CLI handle) — free-form caller labels are self-description and are not admissible (N3). Pass caller_session as <zone>/<id8> with an operator-established link.`);
|
|
243
261
|
}
|
|
262
|
+
/** Revalidate only mutable operator authority, not already-verified birth-time identity or liveness. */
|
|
263
|
+
export function recheckRemoteCliAuthorization(r, auth) {
|
|
264
|
+
if (!r.teamScopeRouting)
|
|
265
|
+
return admitted('scope-off');
|
|
266
|
+
const basis = r.canvasNames().filter(name => r.canvasRemoteMembersOf(name).includes(auth.caller)).map(name => r.canvasTeamOf(name));
|
|
267
|
+
if (basis.length === 0)
|
|
268
|
+
return refused(`remote CLI ${auth.caller} has no operator-established canvas link`);
|
|
269
|
+
const home = r.cliHomeOf?.(auth.caller);
|
|
270
|
+
if (home === undefined || home !== cliHomeOrigin(auth.carrier_url))
|
|
271
|
+
return refused(`remote CLI ${auth.caller} pinned home changed`);
|
|
272
|
+
return admitted('remote-ref-cli', basis);
|
|
273
|
+
}
|
|
244
274
|
const sortedUnique = (values) => [...new Set(values)].sort();
|
|
245
275
|
const pairKey = (pair) => JSON.stringify(pair);
|
|
246
276
|
/**
|
|
@@ -265,6 +295,7 @@ export function canonicalAllowlist(inputs) {
|
|
|
265
295
|
roster,
|
|
266
296
|
canvas,
|
|
267
297
|
cli: sortedUnique(inputs.cli),
|
|
298
|
+
...(inputs.cliHome !== undefined && inputs.cliHome.length > 0 ? { cliHome: [...new Map(inputs.cliHome.map(p => [JSON.stringify(p), p])).values()].sort((a, b) => pairKey(a) < pairKey(b) ? -1 : pairKey(a) > pairKey(b) ? 1 : 0) } : {}),
|
|
268
299
|
links: sortedUnique(inputs.links),
|
|
269
300
|
});
|
|
270
301
|
}
|
|
@@ -278,7 +309,7 @@ export const AUTH_BASIS_CAP = 64;
|
|
|
278
309
|
const PROOFS = ['origin-claim', 'claim-mismatch', 'legacy-window', 'unbound', 'cli-identity', 'in-process', 'none'];
|
|
279
310
|
const RULES = Object.keys(VIA_OF);
|
|
280
311
|
/** Rules that admit on shared teams: their basis lists at least one, each once. Every other rule has an empty basis. */
|
|
281
|
-
const SHARED_RULES = ['roster-shared', 'canvas-shared', 'remote-ref-samezone', 'remote-ref-foreign', 'peer-org'];
|
|
312
|
+
const SHARED_RULES = ['roster-shared', 'canvas-shared', 'remote-ref-samezone', 'remote-ref-foreign', 'remote-ref-cli', 'peer-org'];
|
|
282
313
|
/** A record born more than this far in the future is a forgery or a broken clock, not a birth. */
|
|
283
314
|
export const AUTH_CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;
|
|
284
315
|
/** Does a verified claim's signed zone cover the judged caller (the same name, or `<zone>/…`)? */
|
|
@@ -292,6 +323,7 @@ export function buildAuthorization(admission, facts) {
|
|
|
292
323
|
...(facts.claim !== undefined ? { claim: Object.freeze({ caller: facts.claim.caller, origin: facts.claim.origin }) } : {}),
|
|
293
324
|
ingress: facts.ingress,
|
|
294
325
|
via: admission.via,
|
|
326
|
+
...(facts.carrierUrl !== undefined ? { carrier_url: facts.carrierUrl } : {}),
|
|
295
327
|
admission_rule: admission.rule,
|
|
296
328
|
basis: Object.freeze([...admission.basis]),
|
|
297
329
|
allowlist_version: facts.allowlistVersion,
|
|
@@ -317,6 +349,7 @@ export function canonicalAuthorization(auth) {
|
|
|
317
349
|
...(auth.claim !== undefined ? { claim: { caller: auth.claim.caller, origin: auth.claim.origin } } : {}),
|
|
318
350
|
ingress: auth.ingress,
|
|
319
351
|
via: auth.via,
|
|
352
|
+
...(auth.carrier_url !== undefined ? { carrier_url: auth.carrier_url } : {}),
|
|
320
353
|
admission_rule: auth.admission_rule,
|
|
321
354
|
basis: sortedUnique(auth.basis),
|
|
322
355
|
allowlist_version: auth.allowlist_version,
|
|
@@ -335,6 +368,17 @@ export function authorizationRef(auth) {
|
|
|
335
368
|
return createHash('sha256').update(canonicalAuthorization(auth)).digest('hex').slice(0, 16);
|
|
336
369
|
}
|
|
337
370
|
const boundedString = (v, allowEmpty = false) => typeof v === 'string' && (allowEmpty || v !== '') && v.length <= AUTH_STRING_CAP;
|
|
371
|
+
const validCarrierUrl = (v) => {
|
|
372
|
+
if (!boundedString(v))
|
|
373
|
+
return false;
|
|
374
|
+
try {
|
|
375
|
+
const url = new URL(v);
|
|
376
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin === v;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
338
382
|
/**
|
|
339
383
|
* Restore-side allowlist for one persisted record (design §2.5): all of it or
|
|
340
384
|
* none of it — a record with any field outside its closed set or bounds, or
|
|
@@ -362,6 +406,8 @@ export function parseAuthorization(raw, now = Date.now()) {
|
|
|
362
406
|
const rule = r.admission_rule;
|
|
363
407
|
if (r.via !== VIA_OF[rule])
|
|
364
408
|
return undefined;
|
|
409
|
+
if (rule === 'remote-ref-cli' ? !validCarrierUrl(r.carrier_url) : r.carrier_url !== undefined)
|
|
410
|
+
return undefined;
|
|
365
411
|
if (!Array.isArray(r.basis) || r.basis.length > AUTH_BASIS_CAP || !r.basis.every(b => boundedString(b)))
|
|
366
412
|
return undefined;
|
|
367
413
|
if (new Set(r.basis).size !== r.basis.length)
|
|
@@ -390,10 +436,13 @@ export function parseAuthorization(raw, now = Date.now()) {
|
|
|
390
436
|
const claimProof = ['origin-claim', 'claim-mismatch', 'unbound', 'cli-identity'].includes(r.proof);
|
|
391
437
|
if ((claim !== undefined) !== claimProof)
|
|
392
438
|
return undefined;
|
|
393
|
-
if (claim !== undefined && r.proof === 'origin-claim' && !claimCovers(r.caller, claim.caller))
|
|
439
|
+
if (claim !== undefined && r.proof === 'origin-claim' && (rule === 'remote-ref-cli' ? claim.origin !== r.carrier_url : !claimCovers(r.caller, claim.caller)))
|
|
440
|
+
return undefined;
|
|
441
|
+
if (rule === 'remote-ref-cli' && (r.proof !== 'origin-claim' || claim === undefined))
|
|
394
442
|
return undefined;
|
|
395
443
|
return buildAuthorization({ ok: true, via: VIA_OF[rule], rule, basis: r.basis }, {
|
|
396
444
|
caller: r.caller, proof: r.proof, ...(claim !== undefined ? { claim } : {}),
|
|
445
|
+
...(rule === 'remote-ref-cli' ? { carrierUrl: r.carrier_url } : {}),
|
|
397
446
|
ingress: r.ingress, allowlistVersion: r.allowlist_version, target: r.target,
|
|
398
447
|
receiptTarget: r.receipt_target, receivedAt: r.received_at, targetBusy: r.target_busy === true,
|
|
399
448
|
});
|
package/lib/task-ledger.js
CHANGED
|
@@ -65,7 +65,7 @@ export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
|
65
65
|
const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled', 'evicted'];
|
|
66
66
|
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
67
67
|
const ECHO_SUMMARY = '(settled by content echo)';
|
|
68
|
-
const SENDER_KINDS = ['session', 'host', 'bridge'];
|
|
68
|
+
const SENDER_KINDS = ['session', 'host', 'bridge', 'cli'];
|
|
69
69
|
/** Longest sender id kept; anything longer is malformed, not truncated. */
|
|
70
70
|
export const SENDER_ID_CAP = 256;
|
|
71
71
|
/**
|
|
@@ -120,6 +120,7 @@ function withAuth(raw, entry, now) {
|
|
|
120
120
|
/** #139: the routing facts settlement reads, copied between a row and its archive entry. */
|
|
121
121
|
function routingOf(row) {
|
|
122
122
|
return {
|
|
123
|
+
...(row.forwarded === true ? { forwarded: true } : {}),
|
|
123
124
|
...(row.peerKey !== undefined ? { peerKey: row.peerKey } : {}),
|
|
124
125
|
...(row.peerHost !== undefined ? { peerHost: row.peerHost } : {}),
|
|
125
126
|
...(row.deliveredTo !== undefined ? { deliveredTo: row.deliveredTo } : {}),
|
|
@@ -133,6 +134,7 @@ function birthFacts(peer, birth) {
|
|
|
133
134
|
const remote = peer !== 'local' && !peer.startsWith('local:');
|
|
134
135
|
return {
|
|
135
136
|
...(!remote && birth.auth !== undefined && birth.auth.ingress === 'local' ? { auth: birth.auth } : {}),
|
|
137
|
+
...(remote && birth.forwarded === true ? { forwarded: true } : {}),
|
|
136
138
|
...(remote && fact(birth.peerKey) ? { peerKey: birth.peerKey } : {}),
|
|
137
139
|
...(remote && fact(birth.peerHost) ? { peerHost: birth.peerHost } : {}),
|
|
138
140
|
...(fact(birth.deliveredTo) ? { deliveredTo: birth.deliveredTo } : {}),
|
|
@@ -150,6 +152,7 @@ function withRouting(entry) {
|
|
|
150
152
|
const fact = (v) => typeof v === 'string' && v !== '' && v.length <= ROUTING_FACT_CAP;
|
|
151
153
|
const remoteOutbound = entry.direction === 'outbound' && typeof entry.peer === 'string' && entry.peer !== 'local' && !entry.peer.startsWith('local:');
|
|
152
154
|
return {
|
|
155
|
+
...(remoteOutbound && entry.forwarded === true ? { forwarded: true } : {}),
|
|
153
156
|
...(remoteOutbound && fact(entry.peerKey) ? { peerKey: entry.peerKey } : {}),
|
|
154
157
|
...(remoteOutbound && fact(entry.peerHost) ? { peerHost: entry.peerHost } : {}),
|
|
155
158
|
...(fact(entry.deliveredTo) ? { deliveredTo: entry.deliveredTo } : {}),
|
package/lib/team-store.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* fresh by the state route.
|
|
11
11
|
* @module @nelsonlongxiang/dsh-open-a2a-net/team-store
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { dirname } from 'node:path';
|
|
15
15
|
/** Upper bounds: a curation surface, not a database. */
|
|
16
16
|
export const TEAM_NODE_CAP = 64;
|
|
@@ -115,6 +115,54 @@ export class TeamMembershipStore {
|
|
|
115
115
|
this.notify();
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
/** One atomic roster replacement for the owner's workbench gesture. A failed disk write changes no in-memory membership. */
|
|
119
|
+
replaceTeam(team, members) {
|
|
120
|
+
const clean = validTeam(team);
|
|
121
|
+
if (clean === undefined || new Set(members).size !== members.length || members.some(id => id === ''))
|
|
122
|
+
return { ok: false, error: 'invalid roster team or member ids' };
|
|
123
|
+
if (this.membersOf(clean).join('\0') === members.join('\0'))
|
|
124
|
+
return { ok: true };
|
|
125
|
+
const wanted = new Set(members);
|
|
126
|
+
const next = this.entries.map(entry => ({ session: entry.session, teams: wanted.has(entry.session) ? [...entry.teams] : entry.teams.filter(t => t !== clean) }))
|
|
127
|
+
.filter(entry => entry.teams.length > 0);
|
|
128
|
+
for (const id of members) {
|
|
129
|
+
let entry = next.find(row => row.session === id);
|
|
130
|
+
if (entry === undefined) {
|
|
131
|
+
if (next.length >= TEAM_NODE_CAP)
|
|
132
|
+
return { ok: false, error: 'roster node capacity reached' };
|
|
133
|
+
entry = { session: id, teams: [] };
|
|
134
|
+
next.push(entry);
|
|
135
|
+
}
|
|
136
|
+
if (!entry.teams.includes(clean)) {
|
|
137
|
+
if (entry.teams.length >= TEAMS_PER_NODE_CAP)
|
|
138
|
+
return { ok: false, error: `session ${id} reached the team limit` };
|
|
139
|
+
entry.teams.push(clean);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (this.path !== '') {
|
|
144
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
145
|
+
const tmp = `${this.path}.${String(process.pid)}.tmp`;
|
|
146
|
+
try {
|
|
147
|
+
writeFileSync(tmp, JSON.stringify({ memberships: next }), { mode: 0o600 });
|
|
148
|
+
renameSync(tmp, this.path);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
try {
|
|
152
|
+
unlinkSync(tmp);
|
|
153
|
+
}
|
|
154
|
+
catch { /* the original snapshot stays */ }
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return { ok: false, error: `roster persistence failed: ${String(error)}` };
|
|
161
|
+
}
|
|
162
|
+
this.entries = next;
|
|
163
|
+
this.notify();
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
118
166
|
/** Drop every declaration of one session (a left or archived node). */
|
|
119
167
|
dropSession(session) {
|
|
120
168
|
const before = this.entries.length;
|
package/lib/tools/info.js
CHANGED
|
@@ -480,12 +480,14 @@ export function createInfoTools(deps) {
|
|
|
480
480
|
deadAt: { type: 'number' },
|
|
481
481
|
direction: { type: 'string', enum: ['inbound', 'outbound'] },
|
|
482
482
|
receiptExpected: { type: 'boolean' },
|
|
483
|
+
forwarded: { type: 'boolean' },
|
|
484
|
+
deliveredTo: { type: 'string' },
|
|
483
485
|
reason: { type: 'string', enum: ['fire-forget', 'unconsumed'] },
|
|
484
486
|
sender: {
|
|
485
487
|
type: 'object',
|
|
486
488
|
additionalProperties: false,
|
|
487
489
|
properties: {
|
|
488
|
-
kind: { type: 'string', required: true, enum: ['session', 'host', 'bridge'] },
|
|
490
|
+
kind: { type: 'string', required: true, enum: ['session', 'host', 'bridge', 'cli'] },
|
|
489
491
|
id: { type: 'string', required: true },
|
|
490
492
|
face: { type: 'string' },
|
|
491
493
|
},
|
|
@@ -506,11 +508,12 @@ export function createInfoTools(deps) {
|
|
|
506
508
|
resolvedAt: { type: 'number', required: true },
|
|
507
509
|
summary: { type: 'string' },
|
|
508
510
|
brief: { type: 'string' },
|
|
511
|
+
forwarded: { type: 'boolean' },
|
|
509
512
|
sender: {
|
|
510
513
|
type: 'object',
|
|
511
514
|
additionalProperties: false,
|
|
512
515
|
properties: {
|
|
513
|
-
kind: { type: 'string', required: true, enum: ['session', 'host', 'bridge'] },
|
|
516
|
+
kind: { type: 'string', required: true, enum: ['session', 'host', 'bridge', 'cli'] },
|
|
514
517
|
id: { type: 'string', required: true },
|
|
515
518
|
face: { type: 'string' },
|
|
516
519
|
},
|
|
@@ -566,7 +569,20 @@ export function createInfoTools(deps) {
|
|
|
566
569
|
const archiveAll = taskLedger.archive();
|
|
567
570
|
return {
|
|
568
571
|
ok: true,
|
|
569
|
-
|
|
572
|
+
// Project the declared fields only: ledger rows also carry internal audit facts (the #114 auth record,
|
|
573
|
+
// #139 peerKey/peerHost) the output schema does not admit — spreading the row failed every call (0.6.32+).
|
|
574
|
+
tasks: taskLedger.list().map(({ taskId, team, peer, startedAt, contextId, brief, status, deadAt, direction, receiptExpected, forwarded, deliveredTo, reason, sender }) => ({
|
|
575
|
+
taskId, team, peer, startedAt, status,
|
|
576
|
+
...(contextId !== undefined ? { contextId } : {}),
|
|
577
|
+
...(brief !== undefined ? { brief } : {}),
|
|
578
|
+
...(deadAt !== undefined ? { deadAt } : {}),
|
|
579
|
+
...(direction !== undefined ? { direction } : {}),
|
|
580
|
+
...(receiptExpected !== undefined ? { receiptExpected } : {}),
|
|
581
|
+
...(forwarded !== undefined ? { forwarded } : {}),
|
|
582
|
+
...(deliveredTo !== undefined ? { deliveredTo } : {}),
|
|
583
|
+
...(reason !== undefined ? { reason } : {}),
|
|
584
|
+
...(sender !== undefined ? { sender } : {}),
|
|
585
|
+
})),
|
|
570
586
|
archive: archiveAll.slice(0, 5).map(({ taskId, team, startedAt, resolvedAt, summary, brief, sender }) => ({ taskId, team, startedAt, resolvedAt, ...(summary !== undefined && summary !== '' ? { summary } : {}), ...(brief !== undefined ? { brief } : {}), ...(sender !== undefined ? { sender } : {}) })),
|
|
571
587
|
archivedTotal: archiveAll.length,
|
|
572
588
|
};
|
|
@@ -116,7 +116,7 @@ export declare class A2aClient {
|
|
|
116
116
|
* @param callerSession - caller label overriding the node's label (the calling session's node label).
|
|
117
117
|
* @returns the canonical route result (success or explicit failure).
|
|
118
118
|
*/
|
|
119
|
-
routeDirect(baseUrl: string, team: string, message: string, contextId?: string, signal?: AbortSignal, callerSession?: string, asyncMode?: boolean, taskIdFromCaller?: string, callbackAddress?: string): Promise<A2aRouteResult>;
|
|
119
|
+
routeDirect(baseUrl: string, team: string, message: string, contextId?: string, signal?: AbortSignal, callerSession?: string, asyncMode?: boolean, taskIdFromCaller?: string, callbackAddress?: string, guard?: string): Promise<A2aRouteResult>;
|
|
120
120
|
/**
|
|
121
121
|
* Query a peer's `/a2a/query` outcome surface (W7 slice 2): read-only
|
|
122
122
|
* lookup of one claimed task's settled outcome. The fingerprint is the
|
|
@@ -49,7 +49,7 @@ export interface JudgedRow {
|
|
|
49
49
|
* default: the birth record's `via`).
|
|
50
50
|
* @returns the affected rows, in ledger order; unaffected rows are omitted.
|
|
51
51
|
*/
|
|
52
|
-
export declare function judgeAffected(rows: readonly JudgedRow[], version: string, judge: (caller: string, target: string, ingress: 'wire' | 'local') => Admission, cacheResting?: (row: JudgedRow) => boolean): AffectedTask[];
|
|
52
|
+
export declare function judgeAffected(rows: readonly JudgedRow[], version: string, judge: (caller: string, target: string, ingress: 'wire' | 'local', auth: TaskAuthorization) => Admission, cacheResting?: (row: JudgedRow) => boolean): AffectedTask[];
|
|
53
53
|
/** What one {@link AuthObserver.observe} call needs from the host. */
|
|
54
54
|
export interface ObserveInput {
|
|
55
55
|
readonly version: string;
|
|
@@ -57,7 +57,7 @@ export interface ObserveInput {
|
|
|
57
57
|
readonly inputs: () => unknown;
|
|
58
58
|
readonly cause: string;
|
|
59
59
|
readonly rows: () => readonly JudgedRow[];
|
|
60
|
-
readonly judge: (caller: string, target: string, ingress: 'wire' | 'local') => Admission;
|
|
60
|
+
readonly judge: (caller: string, target: string, ingress: 'wire' | 'local', auth: TaskAuthorization) => Admission;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* The persisted observer. One instance per host; every entry point is
|
|
@@ -22,6 +22,8 @@ export interface CanvasSnapshot {
|
|
|
22
22
|
readonly name: string;
|
|
23
23
|
readonly members: readonly string[];
|
|
24
24
|
}>;
|
|
25
|
+
/** Pinned CLI handle -> linked home origin (only while some canvas team holds the handle). */
|
|
26
|
+
readonly cliHome?: Readonly<Record<string, string>>;
|
|
25
27
|
}
|
|
26
28
|
/** Upper bounds: a canvas taxonomy, not a database. */
|
|
27
29
|
export declare const CANVAS_TEAM_CAP = 64;
|
|
@@ -50,6 +52,7 @@ export declare function validRemoteMemberRef(id: string): boolean;
|
|
|
50
52
|
export declare class CanvasStore {
|
|
51
53
|
/** Team entries in creation order; members in add order (routing priority). */
|
|
52
54
|
private teams;
|
|
55
|
+
private cliHome;
|
|
53
56
|
private readonly path;
|
|
54
57
|
private readonly listeners;
|
|
55
58
|
/**
|
|
@@ -74,6 +77,9 @@ export declare class CanvasStore {
|
|
|
74
77
|
hasTeam(name: string): boolean;
|
|
75
78
|
/** The named team's member ids in routing-priority order (a copy). */
|
|
76
79
|
membersOf(name: string): readonly string[];
|
|
80
|
+
/** Frozen, operator-linked CLI home; undefined means unknown or ambiguous. */
|
|
81
|
+
cliHomeOf(handle: string): string | undefined;
|
|
82
|
+
pinnedCliHomes(): ReadonlyArray<readonly [string, string]>;
|
|
77
83
|
/**
|
|
78
84
|
* The named team's remote member refs (zone-qualified team handles) in
|
|
79
85
|
* routing-priority order — the failover tier after the local live/cold
|
|
@@ -101,11 +107,14 @@ export declare class CanvasStore {
|
|
|
101
107
|
* @param name - the existing team name.
|
|
102
108
|
* @param id - the member session id.
|
|
103
109
|
*/
|
|
104
|
-
addMember(name: string, id: string
|
|
110
|
+
addMember(name: string, id: string, home?: string, options?: {
|
|
111
|
+
replaceHome?: boolean;
|
|
112
|
+
}): boolean;
|
|
105
113
|
/**
|
|
106
114
|
* Remove one member from one team (a no-op when absent).
|
|
107
115
|
*/
|
|
108
116
|
removeMember(name: string, id: string): boolean;
|
|
117
|
+
private forgetUnusedHome;
|
|
109
118
|
/**
|
|
110
119
|
* Drop one id from every team — the leave and archive paths: a session
|
|
111
120
|
* leaving the network keeps no canvas membership anywhere.
|
|
@@ -113,3 +122,7 @@ export declare class CanvasStore {
|
|
|
113
122
|
*/
|
|
114
123
|
dropMember(id: string): void;
|
|
115
124
|
}
|
|
125
|
+
/** One policy predicate for local and signed-remote canvas creation. */
|
|
126
|
+
export declare function canvasNameReserved(raw: string, zone: string, allowlist: readonly string[], roster: {
|
|
127
|
+
membersOf(team: string): readonly string[];
|
|
128
|
+
}): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { A2aPeerCard } from './types.ts';
|
|
2
|
+
/** A verified, operator-linked host's full card at link time. */
|
|
3
|
+
export interface CliHomeCandidate {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly linked: boolean;
|
|
6
|
+
readonly card?: A2aPeerCard;
|
|
7
|
+
}
|
|
8
|
+
/** Canonical root HTTP(S) origin; address aliases remain distinct trust decisions. */
|
|
9
|
+
export declare function cliHomeOrigin(value: unknown): string | undefined;
|
|
10
|
+
export type CliHomeDecision = {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
readonly home?: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly error: string;
|
|
16
|
+
};
|
|
17
|
+
/** Read-only projection of the same fresh evidence used by explicit home validation. */
|
|
18
|
+
export declare function cliHomeChoices(handle: string, candidates: readonly CliHomeCandidate[], locallyHeld?: boolean): {
|
|
19
|
+
homes: string[];
|
|
20
|
+
unreadable: number;
|
|
21
|
+
held?: true;
|
|
22
|
+
};
|
|
23
|
+
/** Count origins, not URL aliases: two card URLs on one host are one home. */
|
|
24
|
+
export declare function decideCliHome(handle: string, requested: unknown, candidates: readonly CliHomeCandidate[], locallyHeld?: boolean): CliHomeDecision;
|
|
@@ -7,7 +7,7 @@ export interface A2aSessionRow {
|
|
|
7
7
|
/** One routing outcome as the state route reports it. */
|
|
8
8
|
export interface A2aActivityRow {
|
|
9
9
|
readonly ts: number;
|
|
10
|
-
readonly dir: 'in' | 'out';
|
|
10
|
+
readonly dir: 'in' | 'out' | 'fwd';
|
|
11
11
|
readonly team: string;
|
|
12
12
|
readonly peer: string;
|
|
13
13
|
readonly ok: boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type TaskLedger } from './task-ledger.ts';
|
|
2
|
+
import type { A2aPeerCard, A2aRouteResult } from './types.ts';
|
|
3
|
+
export interface ForwardRequest {
|
|
4
|
+
team: string;
|
|
5
|
+
message: string;
|
|
6
|
+
caller: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
contextId?: string;
|
|
9
|
+
noWait: boolean;
|
|
10
|
+
localCliProof: boolean;
|
|
11
|
+
guarded: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DirectForwardDeps {
|
|
14
|
+
enabled(): boolean;
|
|
15
|
+
isolated(): boolean;
|
|
16
|
+
deposited(handle: string): boolean;
|
|
17
|
+
memberLinked(handle: string): boolean;
|
|
18
|
+
scope(handle: string, team: string): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
}>;
|
|
24
|
+
candidates(team: string, failures: string[], caller: string): Promise<{
|
|
25
|
+
urls: string[];
|
|
26
|
+
card(url: string): Promise<A2aPeerCard | undefined>;
|
|
27
|
+
}>;
|
|
28
|
+
dispatch(url: string, request: ForwardRequest, guard: string): Promise<A2aRouteResult>;
|
|
29
|
+
ledger: TaskLedger;
|
|
30
|
+
activity(team: string, peer: string, ok: boolean): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createDirectForward(d: DirectForwardDeps): (request: ForwardRequest) => Promise<A2aRouteResult>;
|
|
@@ -6,12 +6,13 @@ import type { Config } from '../index.ts';
|
|
|
6
6
|
import type { CardCacheEntry, InFlightRoute, RouteActivityEntry } from '../runtime.ts';
|
|
7
7
|
import { CanvasStore } from '../canvas-store.ts';
|
|
8
8
|
import { type CanvasNotifyDeps } from './canvas-notify.ts';
|
|
9
|
-
import type
|
|
9
|
+
import { type CliDepositStore } from '../cli-deposit.ts';
|
|
10
10
|
import type { ExposureStore } from '../exposure-store.ts';
|
|
11
11
|
import type { GroupStore } from '../group-store.ts';
|
|
12
12
|
import type { IdempotencyStore } from '../idempotency-store.ts';
|
|
13
13
|
import type { JoinedSessions } from '../joined-store.ts';
|
|
14
14
|
import type { LayoutStore } from '../layout-store.ts';
|
|
15
|
+
import type { CliHomeDecision } from '../cli-home.ts';
|
|
15
16
|
import type { PeerStore } from '../peer-store.ts';
|
|
16
17
|
import { type TaskLedger } from '../task-ledger.ts';
|
|
17
18
|
import type { TeamMembershipStore } from '../team-store.ts';
|
|
@@ -61,6 +62,12 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
|
|
|
61
62
|
readonly cliDeposits: CliDepositStore;
|
|
62
63
|
readonly groupStore: GroupStore;
|
|
63
64
|
readonly canvasStore: CanvasStore;
|
|
65
|
+
readonly cliHomeForAdd: (handle: string, requested?: unknown) => Promise<CliHomeDecision>;
|
|
66
|
+
readonly cliHomeOptions: (handle: string) => Promise<{
|
|
67
|
+
homes: string[];
|
|
68
|
+
unreadable: number;
|
|
69
|
+
held?: true;
|
|
70
|
+
}>;
|
|
64
71
|
readonly layoutStore: LayoutStore;
|
|
65
72
|
readonly taskLedger: TaskLedger;
|
|
66
73
|
/** #114 S6: observe (lazily) and return the /state `authorization` block. */
|
|
@@ -74,6 +81,7 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
|
|
|
74
81
|
team: string;
|
|
75
82
|
id: string;
|
|
76
83
|
ok: boolean;
|
|
84
|
+
actor?: string;
|
|
77
85
|
reason?: string;
|
|
78
86
|
}[];
|
|
79
87
|
readonly reportRing: {
|
|
@@ -14,13 +14,14 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
16
16
|
import type WebServer from '@deepseek-ai/dsh-host-webserver';
|
|
17
|
-
import type { A2aPeerCard } from '../types.ts';
|
|
17
|
+
import type { A2aPeerCard, A2aRouteResult } from '../types.ts';
|
|
18
|
+
import type { ForwardRequest } from '../direct-forward.ts';
|
|
18
19
|
import { CanvasStore } from '../canvas-store.ts';
|
|
19
|
-
import type
|
|
20
|
+
import { type CliDepositStore } from '../cli-deposit.ts';
|
|
20
21
|
import type { IdempotencyStore } from '../idempotency-store.ts';
|
|
21
22
|
import type { JoinedSessions } from '../joined-store.ts';
|
|
22
23
|
import { type TaskLedger } from '../task-ledger.ts';
|
|
23
|
-
import { type Admission, type SettlementRefusal, type Settler } from '../task-auth.ts';
|
|
24
|
+
import { type Admission, type RemoteCliProof, type SettlementRefusal, type Settler } from '../task-auth.ts';
|
|
24
25
|
import type { Config } from '../index.ts';
|
|
25
26
|
/**
|
|
26
27
|
* #139 (design §4.4): add `settlement: {status:'refused', reason, task_id}`
|
|
@@ -51,6 +52,7 @@ export type RouteOutcome = {
|
|
|
51
52
|
} | {
|
|
52
53
|
ok: false;
|
|
53
54
|
error: string;
|
|
55
|
+
localMiss?: true;
|
|
54
56
|
};
|
|
55
57
|
interface BridgeTeamsRegistryMirror {
|
|
56
58
|
listTeams(): Array<{
|
|
@@ -120,12 +122,17 @@ export interface WireDeps {
|
|
|
120
122
|
readonly config: Config;
|
|
121
123
|
readonly cliDeposits: CliDepositStore;
|
|
122
124
|
readonly canvasStore: CanvasStore;
|
|
125
|
+
readonly canvasNameReserved: (name: string) => boolean;
|
|
123
126
|
readonly sessionNodes: Map<string, Agent>;
|
|
124
127
|
readonly joinedSessions: JoinedSessions;
|
|
125
128
|
readonly idempotencyStore: IdempotencyStore;
|
|
126
129
|
readonly verifyOriginClaim: (req: IncomingMessage, options?: {
|
|
127
130
|
readonly cli?: boolean;
|
|
128
131
|
}) => Promise<OriginClaimResult>;
|
|
132
|
+
readonly forwardDirect: (request: ForwardRequest) => Promise<A2aRouteResult>;
|
|
133
|
+
readonly remoteCliProof: (handle: string, claim: Extract<OriginClaimResult, {
|
|
134
|
+
ok: true;
|
|
135
|
+
}>) => Promise<RemoteCliProof | undefined>;
|
|
129
136
|
readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
|
|
130
137
|
/** This host's own card URL (read when a request lands; set once the web server settles). */
|
|
131
138
|
readonly ownCardUrl: string;
|
|
@@ -134,7 +141,7 @@ export interface WireDeps {
|
|
|
134
141
|
readonly secretEqual: (a: string, b: string) => boolean;
|
|
135
142
|
readonly a2aCallerRefusal: (caller: string, targetTeam?: string) => string | undefined;
|
|
136
143
|
/** #114 S2: the same decision with the rule and basis that admitted the caller. */
|
|
137
|
-
readonly a2aCallerAdmission: (caller: string, targetTeam?: string) => Admission;
|
|
144
|
+
readonly a2aCallerAdmission: (caller: string, targetTeam?: string, cliProof?: RemoteCliProof) => Admission;
|
|
138
145
|
/** #114 S2: this host's allowlist version right now (design §2.3). */
|
|
139
146
|
readonly allowlistVersionNow: () => string;
|
|
140
147
|
/** #114 S6: record the version if it moved and re-judge affected tasks (observe only). */
|
|
@@ -158,6 +165,8 @@ export interface WireDeps {
|
|
|
158
165
|
};
|
|
159
166
|
readonly taskLedger: TaskLedger;
|
|
160
167
|
readonly refuseIfFlooded: (caller: string, message: string, team: string, taskId: string, res: ServerResponse) => boolean;
|
|
168
|
+
/** gh#224: refuses a new delivery once the target already owes `targetQueueCap` receipts. */
|
|
169
|
+
readonly refuseIfQueueFull: (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
|
|
161
170
|
/** Settles a receipt; returns the enforce refusal of the row its header names (#139), if any. */
|
|
162
171
|
readonly settleAndAnnounce: (message: string, settler?: Settler) => SettlementRefusal | undefined;
|
|
163
172
|
readonly touchWakeDemand: () => void;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface Config {
|
|
|
91
91
|
* (which is left untouched). Inbound faces keep answering. Default false.
|
|
92
92
|
*/
|
|
93
93
|
readonly isolated: boolean;
|
|
94
|
+
/** Forward online local CLI deliveries one hop after a local miss. Default off. */
|
|
95
|
+
readonly forwardDirect: boolean;
|
|
94
96
|
/** Human-facing name published on the agent card. */
|
|
95
97
|
readonly agentName: string;
|
|
96
98
|
/**
|
|
@@ -258,6 +260,8 @@ export interface Config {
|
|
|
258
260
|
* v0.5.23 (async-stall): see the schema comment on asyncNudgeDelayMs.
|
|
259
261
|
*/
|
|
260
262
|
readonly asyncNudgeDelayMs: number;
|
|
263
|
+
/** gh#224: receipt-owing tasks one target may queue on this host before new deliveries are refused (0 = off). */
|
|
264
|
+
readonly targetQueueCap: number;
|
|
261
265
|
/**
|
|
262
266
|
* How long the state route serves the cold-row id set from cache before
|
|
263
267
|
* re-enumerating the persistence layer. The panel polls every 2s; the
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** One sender scope gate shared by model routing and home-host CLI forwarding. */
|
|
2
|
+
export interface OutboundScopeRecords {
|
|
3
|
+
readonly zone: string;
|
|
4
|
+
teamsOf(id: string): readonly string[];
|
|
5
|
+
membersOf(team: string): readonly string[];
|
|
6
|
+
id8(id: string): string;
|
|
7
|
+
canvasNames(): readonly string[];
|
|
8
|
+
canvasMembers(name: string): readonly string[];
|
|
9
|
+
canvasTeam(name: string): string;
|
|
10
|
+
peerOrgs(): Iterable<readonly {
|
|
11
|
+
team?: unknown;
|
|
12
|
+
members?: unknown;
|
|
13
|
+
}[]>;
|
|
14
|
+
warmPeers(): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare function outboundScope(r: OutboundScopeRecords, id: string, handle: string, team: string): Promise<{
|
|
17
|
+
ok: true;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** gh#224 per-target queue cap (owner 2026-11-03: "at most 3 queued"). The flood guard (gh#91)
|
|
2
|
+
* bounds a sender's RATE; a slow, steady sender can still pile a dozen unanswered tasks onto one
|
|
3
|
+
* busy session. This counts what the target already owes and refuses a NEW delivery once the
|
|
4
|
+
* count reaches the cap, visibly and before anything is steered or booked.
|
|
5
|
+
*
|
|
6
|
+
* Counted ("queued for T"): rows this host's ledger still holds pending for T that expect a
|
|
7
|
+
* receipt — inbound rows born at /a2a/direct (addressed to T, or delivered to T), and same-host
|
|
8
|
+
* routes (peer 'local') to T. Never refused: a receipt for a task this host's ledger holds
|
|
9
|
+
* (terminal; refusing one strands that row — a genuine receipt always returns to the host that
|
|
10
|
+
* booked its task, so a '[A2A receipt]' header naming an unknown id is ordinary traffic and is
|
|
11
|
+
* counted), and deliveries into a CLI mailbox (its own 64-mail bound applies). A canvas team is
|
|
12
|
+
* one target: a full queue refuses the team as addressed, remote members included.
|
|
13
|
+
*/
|
|
14
|
+
import type { ServerResponse } from 'node:http';
|
|
15
|
+
import type { IdempotencyStore } from './idempotency-store.ts';
|
|
16
|
+
export declare const WIRE_ERROR_QUEUE_FULL = -32009;
|
|
17
|
+
export declare const DEFAULT_TARGET_QUEUE_CAP = 3;
|
|
18
|
+
/** The ledger fields the count reads. */
|
|
19
|
+
export interface QueueRow {
|
|
20
|
+
readonly team: string;
|
|
21
|
+
readonly peer: string;
|
|
22
|
+
readonly status: string;
|
|
23
|
+
readonly direction?: string;
|
|
24
|
+
readonly receiptExpected?: boolean;
|
|
25
|
+
readonly deliveredTo?: string;
|
|
26
|
+
}
|
|
27
|
+
/** How many receipt-owing tasks this host holds pending for `team`. */
|
|
28
|
+
export declare function queuedFor(rows: readonly QueueRow[], team: string): number;
|
|
29
|
+
/** The refusal text when `team`'s queue is full, else undefined. `cap <= 0` disables the guard. */
|
|
30
|
+
export declare function queueFullRefusal(rows: readonly QueueRow[], team: string, message: string, cap: number, mailbox: boolean, ownTask: (taskId: string) => boolean): string | undefined;
|
|
31
|
+
/** Wire-face adapter in the flood guard's shape: a refused fresh key is FAILED, not stranded pending. */
|
|
32
|
+
export declare function createQueueGuard(rows: () => readonly QueueRow[], cap: () => number, ownTask: (taskId: string) => boolean, outcomes: Pick<IdempotencyStore, 'recordOutcome'>, recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void): (caller: string, message: string, team: string, taskId: string, mailbox: boolean, res: ServerResponse) => boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type TeamMembershipStore } from './team-store.ts';
|
|
2
|
+
export interface RosterOpDeps {
|
|
3
|
+
zone: string;
|
|
4
|
+
home: string;
|
|
5
|
+
store: TeamMembershipStore;
|
|
6
|
+
joined: (id: string) => boolean;
|
|
7
|
+
canvasExists: (name: string) => boolean;
|
|
8
|
+
allowlist: () => readonly string[];
|
|
9
|
+
setAllowlist: (list: string[]) => void;
|
|
10
|
+
audit: (action: 'join' | 'leave', team: string, id: string, ok: boolean, reason: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export type RosterOpResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
team: string;
|
|
15
|
+
members: string[];
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
};
|
|
20
|
+
/** Writes complete snapshots; on a failed second write, rolls the first back. */
|
|
21
|
+
export declare function rosterOp(d: RosterOpDeps, raw: unknown): RosterOpResult;
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { A2aPeerCard } from './types.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
export interface RouteActivityEntry {
|
|
13
13
|
readonly ts: number;
|
|
14
|
-
readonly dir: 'in' | 'out';
|
|
14
|
+
readonly dir: 'in' | 'out' | 'fwd';
|
|
15
15
|
readonly team: string;
|
|
16
16
|
readonly peer: string;
|
|
17
17
|
readonly ok: boolean;
|