@nelsonlongxiang/dsh-open-a2a-net 0.6.38 → 0.6.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -335
- package/README_zh.md +241 -236
- package/assets/nexusDist/assets/index-C7HagaGy.css +1 -1
- package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -9
- package/cli/a2a.mjs +12 -4
- package/cli/serve.mjs +132 -0
- package/cordis.patch.yml +21 -21
- package/lib/canvas-id.js +27 -0
- package/lib/canvas-store.js +7 -1
- package/lib/client.js +63 -63
- package/lib/config.js +125 -0
- package/lib/direct-forward.js +64 -18
- package/lib/faces/admin.js +4 -1
- package/lib/faces/wire.js +24 -6
- package/lib/flood-guard.js +3 -1
- package/lib/index.js +43 -145
- package/lib/live-roots.js +39 -0
- package/lib/origin-claim.js +3 -2
- package/lib/queue-guard.js +3 -1
- package/lib/skill/references/cli.md +214 -203
- package/lib/skill/references/routing.md +4 -0
- package/lib/task-auth.js +14 -10
- package/lib/types/canvas-id.d.ts +9 -0
- package/lib/types/config.d.ts +278 -0
- package/lib/types/direct-forward.d.ts +25 -3
- package/lib/types/faces/admin.d.ts +2 -0
- package/lib/types/faces/wire.d.ts +7 -0
- package/lib/types/index.d.ts +3 -277
- package/lib/types/live-roots.d.ts +35 -0
- package/lib/types/origin-claim.d.ts +1 -0
- package/lib/types/queue-guard.d.ts +2 -0
- package/lib/types/task-auth.d.ts +3 -0
- package/lib/types/types.d.ts +2 -0
- package/nexus-stage/package-lock.json +2262 -2262
- package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
- package/nexus-stage/src/edges.ts +72 -72
- package/nexus-stage/src/layout-wire.ts +158 -158
- package/nexus-stage/src/seat.ts +35 -35
- package/nexus-stage/src/stage-name.ts +13 -13
- package/nexus-stage/src/tokens.ts +31 -31
- package/package.json +218 -218
package/lib/task-auth.js
CHANGED
|
@@ -32,7 +32,11 @@ export const VIA_OF = {
|
|
|
32
32
|
/** The refusal text of a decision, or undefined when admitted — the shape the faces have always read. */
|
|
33
33
|
export const refusalOf = (admission) => (admission.ok ? undefined : admission.refusal);
|
|
34
34
|
const admitted = (rule, basis = []) => ({ ok: true, via: VIA_OF[rule], rule, basis });
|
|
35
|
-
|
|
35
|
+
/** #282 S1: a refusal may carry the one step the caller (or this host's operator) can take next. */
|
|
36
|
+
const refused = (refusal, next) => ({ ok: false, refusal, ...(next !== undefined ? { next } : {}) });
|
|
37
|
+
/** The next step of a refused decision, or undefined. */
|
|
38
|
+
export const nextOf = (a) => (a.ok ? undefined : a.next);
|
|
39
|
+
const linkNext = (caller, target) => `ask this host's operator to put ${caller} in a canvas team${target !== undefined ? ` with ${target}` : ''} in the A2A workbench, or route through a member of a team you already share`;
|
|
36
40
|
const ZONE_ID8 = /^[a-z0-9-]+\/[0-9a-f]{8}$/i;
|
|
37
41
|
const teamlessText = (caller) => `caller "${caller}" declares no team membership on this host's roster — a teamless node has no network (S3 phase-3 admission). Declare a shared team on both ends, or route through a teamed member.`;
|
|
38
42
|
/**
|
|
@@ -178,7 +182,7 @@ export function sameZoneAdmission(r, caller, targetTeam) {
|
|
|
178
182
|
return hit;
|
|
179
183
|
return refused(declared.size === 0
|
|
180
184
|
? teamlessText(caller)
|
|
181
|
-
: `caller "${caller}" shares no team with target "${targetTeam}" — declared [${[...declared.keys()].join(', ')}], target links [${[...links].join(', ')}] (F5 target-level admission: a host-level link alone does not open every session team)
|
|
185
|
+
: `caller "${caller}" shares no team with target "${targetTeam}" — declared [${[...declared.keys()].join(', ')}], target links [${[...links].join(', ')}] (F5 target-level admission: a host-level link alone does not open every session team).`, linkNext(caller, targetTeam));
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
const hit = pass(t => routable.has(t) || otherLocalDeclarations.has(t));
|
|
@@ -186,7 +190,7 @@ export function sameZoneAdmission(r, caller, targetTeam) {
|
|
|
186
190
|
return hit;
|
|
187
191
|
return refused(declared.size === 0
|
|
188
192
|
? teamlessText(caller)
|
|
189
|
-
: `caller "${caller}" declares teams [${[...declared.keys()].join(', ')}] — none is routable on this node (shared-team admission). Declare a shared team on both ends, or route through a teamed member
|
|
193
|
+
: `caller "${caller}" declares teams [${[...declared.keys()].join(', ')}] — none is routable on this node (shared-team admission). Declare a shared team on both ends, or route through a teamed member.`, linkNext(caller, targetTeam));
|
|
190
194
|
}
|
|
191
195
|
/**
|
|
192
196
|
* W1: the joined-face gate for the LOCAL dispatch candidate — only a
|
|
@@ -216,7 +220,7 @@ export function callerAdmission(r, caller, targetTeam, cliProof) {
|
|
|
216
220
|
if (!r.teamScopeRouting)
|
|
217
221
|
return admitted('scope-off');
|
|
218
222
|
if (caller === '') {
|
|
219
|
-
return refused('caller_session is required — anonymous deliveries are refused (S3 phase-3b). Pass caller_session as <zone>/<id8>.');
|
|
223
|
+
return refused('caller_session is required — anonymous deliveries are refused (S3 phase-3b). Pass caller_session as <zone>/<id8>.', 'send caller_session as your <zone>/<id8> handle (a2a-net does this for you)');
|
|
220
224
|
}
|
|
221
225
|
if (caller === r.zone)
|
|
222
226
|
return admitted('host-face');
|
|
@@ -228,21 +232,21 @@ export function callerAdmission(r, caller, targetTeam, cliProof) {
|
|
|
228
232
|
const basis = r.canvasNames().filter(name => r.canvasRemoteMembersOf(name).includes(caller)).map(name => r.canvasTeamOf(name));
|
|
229
233
|
if (basis.length > 0)
|
|
230
234
|
return admitted('remote-ref-foreign', basis);
|
|
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
|
|
235
|
+
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.`, linkNext(caller, targetTeam));
|
|
232
236
|
}
|
|
233
237
|
// A foreign CLI handle is never a free-form node fact: its operator link,
|
|
234
238
|
// pinned home, bound signer, and fresh online row must ALL hold.
|
|
235
239
|
if (CLI_HANDLE_RE.test(caller) && !r.cliDeposited(caller)) {
|
|
236
240
|
const basis = r.canvasNames().filter(name => r.canvasRemoteMembersOf(name).includes(caller)).map(name => r.canvasTeamOf(name));
|
|
237
241
|
if (basis.length === 0)
|
|
238
|
-
return refused(`remote CLI ${caller} has no operator-established canvas link
|
|
242
|
+
return refused(`remote CLI ${caller} has no operator-established canvas link`, linkNext(caller, targetTeam));
|
|
239
243
|
const home = r.cliHomeOf?.(caller);
|
|
240
244
|
if (home === undefined)
|
|
241
|
-
return refused(`remote CLI ${caller} home is not pinned`);
|
|
245
|
+
return refused(`remote CLI ${caller} home is not pinned`, `ask this host's operator to pin ${caller}'s home host when adding it to the canvas team (workbench member dialog)`);
|
|
242
246
|
if (cliProof?.bound !== true || cliHomeOrigin(cliProof.origin) !== home || cliHomeOrigin(cliProof.viaUrl) !== home)
|
|
243
|
-
return refused(`remote CLI ${caller} signer does not match pinned home`);
|
|
247
|
+
return refused(`remote CLI ${caller} signer does not match pinned home`, `send through ${caller}'s pinned home host only`);
|
|
244
248
|
if (cliProof.online !== true)
|
|
245
|
-
return refused(`remote CLI ${caller} is offline or home card could not be read`);
|
|
249
|
+
return refused(`remote CLI ${caller} is offline or home card could not be read`, 'run `a2a-net publish` on the home host, then send again with a new task id');
|
|
246
250
|
return admitted('remote-ref-cli', basis);
|
|
247
251
|
}
|
|
248
252
|
// Free-form label: only this host's own node facts (V3).
|
|
@@ -257,7 +261,7 @@ export function callerAdmission(r, caller, targetTeam, cliProof) {
|
|
|
257
261
|
// gh#45: a deposited CLI handle is a published node fact of this host.
|
|
258
262
|
if (r.cliDeposited(caller))
|
|
259
263
|
return admitted('cli-deposit');
|
|
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
|
|
264
|
+
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.`, 'send caller_session as a <zone>/<id8> handle this host has linked; free-form labels are never admitted');
|
|
261
265
|
}
|
|
262
266
|
/** Revalidate only mutable operator authority, not already-verified birth-time identity or liveness. */
|
|
263
267
|
export function recheckRemoteCliAuthorization(r, auth) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** First 8 hex of sha256 over the base64 SPKI DER public key; '' when there is no key. */
|
|
2
|
+
export declare function canvasOwnerFingerprint(publicKey: string | undefined): string;
|
|
3
|
+
/** Split `<zone>/canvas/<name>@<fp8>` into the plain team and the owner fingerprint. */
|
|
4
|
+
export declare function splitCanvasOwner(team: string): {
|
|
5
|
+
team: string;
|
|
6
|
+
owner?: string;
|
|
7
|
+
};
|
|
8
|
+
/** The display/routing id of a canvas team owned by the card holding `publicKey`. */
|
|
9
|
+
export declare function qualifiedCanvasId(team: string, publicKey: string | undefined): string;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/** The plugin row's configuration: the `Config` interface and its Schemastery schema (moved out of index.ts, #77). */
|
|
2
|
+
import s from '@deepseek-ai/schemastery';
|
|
3
|
+
import type { SettlementMode } from './task-auth.ts';
|
|
4
|
+
/** One signed zone delegation published on this node's agent card. */
|
|
5
|
+
export interface DelegateConfig {
|
|
6
|
+
/** The delegated name; resolves zone-relatively at route time. */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
/** Base URL of the zone the name resolves in. */
|
|
9
|
+
readonly url: string;
|
|
10
|
+
/** Base64 SPKI DER binding to the target zone's card key; `''` leaves the delegation unbound. */
|
|
11
|
+
readonly publicKey: string;
|
|
12
|
+
}
|
|
13
|
+
/** Deployment-varying A2A node facts; every field has a declared default. */
|
|
14
|
+
export interface Config {
|
|
15
|
+
/** `X-API-Key` value sent on peer requests and required by the control routes; empty disables the header. */
|
|
16
|
+
readonly apiKey: string;
|
|
17
|
+
/**
|
|
18
|
+
* This node's caller label. `''` (the default) derives `dsh-host-<8 hex>`
|
|
19
|
+
* from a per-home node id created on first use, so two deployments mounting
|
|
20
|
+
* defaults do not collide while a persisted home keeps the label stable
|
|
21
|
+
* across restarts.
|
|
22
|
+
*/
|
|
23
|
+
readonly session: string;
|
|
24
|
+
/** Team name this node exposes for direct routing. */
|
|
25
|
+
readonly team: string;
|
|
26
|
+
/** S4 fix 3: audit (default) or refuse host-side (agent-less) routing calls. */
|
|
27
|
+
readonly gateAgentLessCalls: 'audit' | 'refuse';
|
|
28
|
+
/** #114 S4: settlement authorization — off, observe (record would-refuse, settle as before), enforce. */
|
|
29
|
+
readonly settlementAuth: SettlementMode;
|
|
30
|
+
/**
|
|
31
|
+
* Publish this node's `/.well-known/agent-card.json` on the shared web
|
|
32
|
+
* server (peer discovery). Requires the web server in the composition;
|
|
33
|
+
* the card carries this node's team, capabilities, referral list, and
|
|
34
|
+
* joined session teams so peers find it without a central directory.
|
|
35
|
+
*/
|
|
36
|
+
readonly announce: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* gh#193: send nothing to any peer. Every outbound peer request — card
|
|
39
|
+
* reads, directory sweeps, routes, log reads, canvas pings and canvas-op
|
|
40
|
+
* relays — goes through one fetch that refuses while this is on, so a
|
|
41
|
+
* test host stays dark even with learned peers in `a2a/peers.json`
|
|
42
|
+
* (which is left untouched). Inbound faces keep answering. Default false.
|
|
43
|
+
*/
|
|
44
|
+
readonly isolated: boolean;
|
|
45
|
+
/** Forward online local CLI deliveries one hop after a local miss. Default on from 0.6.38; explicit false opts out. */
|
|
46
|
+
readonly forwardDirect: boolean;
|
|
47
|
+
/** Human-facing name published on the agent card. */
|
|
48
|
+
readonly agentName: string;
|
|
49
|
+
/**
|
|
50
|
+
* Base URLs of seed peer nodes. `a2a_teams` lists teams discovered from
|
|
51
|
+
* the peers' agent cards and `a2a_route` connects to the owning peer's
|
|
52
|
+
* `/a2a/direct` endpoint. The list is a seed set: referral URLs learned
|
|
53
|
+
* from fetched cards join a bounded, quality-scored peer store persisted
|
|
54
|
+
* at `<dsh-home>/a2a/peers.json`.
|
|
55
|
+
*/
|
|
56
|
+
readonly peers: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Zone delegation records published (signed) on this node's agent card:
|
|
59
|
+
* each entry publishes `name` as resolvable in the zone at `url`, so a
|
|
60
|
+
* route for `name` walks the delegation chain (at most 5 hops, cycles
|
|
61
|
+
* fail closed).
|
|
62
|
+
*/
|
|
63
|
+
readonly delegates: DelegateConfig[];
|
|
64
|
+
/**
|
|
65
|
+
* gh#9+gh#10 OriginClaim window (docs/protocol/delivery-origin-auth.md §4):
|
|
66
|
+
* while true (default), a delivery/log request with NO envelope still
|
|
67
|
+
* walks N3 admission and increments the legacyInbound warn metric; when
|
|
68
|
+
* the operator closes it (post fleet flip), an absent envelope rejects
|
|
69
|
+
* `untrusted-origin` — same code as a failed signature (M1: a
|
|
70
|
+
* self-asserted already-linked name must not pass on shape alone).
|
|
71
|
+
*/
|
|
72
|
+
readonly originClaimWindow: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* OriginClaim unbound window (docs/protocol/delivery-origin-auth.md §2.1):
|
|
75
|
+
* while true (default), this host also signs the legacy unbound form and
|
|
76
|
+
* accepts a peer's unbound-only envelope (counted as unboundInbound);
|
|
77
|
+
* closed, it signs and accepts the audience-bound form only.
|
|
78
|
+
*/
|
|
79
|
+
readonly originClaimUnboundWindow: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Extra origins this host answers to as an OriginClaim audience, beyond
|
|
82
|
+
* its card URL and the address a request arrives on (port-mapped or
|
|
83
|
+
* proxied hosts name the URL peers dial).
|
|
84
|
+
*/
|
|
85
|
+
readonly originClaimAudiences: string[];
|
|
86
|
+
/**
|
|
87
|
+
* #19 D2: card-face sessionTeams excerpt exposure — 'full' | 'masked'
|
|
88
|
+
* | 'off' (owner-level; default full). See the schema comment.
|
|
89
|
+
*/
|
|
90
|
+
readonly exposureExcerpt: 'full' | 'masked' | 'off';
|
|
91
|
+
/**
|
|
92
|
+
* Override for the OriginClaim callerUrl anchor. Loopback-bound hosts
|
|
93
|
+
* behind a pocket tunnel must name the card URL peers can actually
|
|
94
|
+
* fetch (e.g. http://192.168.3.157:13080); empty uses the web server's
|
|
95
|
+
* own address.
|
|
96
|
+
*/
|
|
97
|
+
readonly originClaimUrl: string;
|
|
98
|
+
/**
|
|
99
|
+
* gh#8 (N9) escape hatch: when true, team-name resolution also considers
|
|
100
|
+
* the referral-learned peer tail (the legacy whole-store basis) instead
|
|
101
|
+
* of the seeded/delegate subset only. Default false — a team name binds
|
|
102
|
+
* to hosts the operator linked; fleets relying on referral discovery for
|
|
103
|
+
* routing migrate by adding explicit seeds, then leave this off.
|
|
104
|
+
*/
|
|
105
|
+
readonly referralTeamResolution: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Harness home slice for the node keypair and peer store; `''` (the
|
|
108
|
+
* default) uses the process DSH home. The Ed25519 key lives at
|
|
109
|
+
* `<home>/a2a/node-key.pem` and is generated on first announce; a
|
|
110
|
+
* persisted key keeps the node's card identity stable across restarts.
|
|
111
|
+
*/
|
|
112
|
+
readonly dshHome: string;
|
|
113
|
+
/**
|
|
114
|
+
* Agent-card validity. Announce re-signs at TTL/4 so three lost refreshes
|
|
115
|
+
* still leave a live card; a node silent for one TTL disappears from every
|
|
116
|
+
* peer's discovery without manual eviction.
|
|
117
|
+
*/
|
|
118
|
+
readonly cardTtlMs: number;
|
|
119
|
+
/**
|
|
120
|
+
* Enable per-session A2A nodes: each live top-level session can join the
|
|
121
|
+
* network explicitly — label `<session>-<agentId8>`, team
|
|
122
|
+
* `<team>/<agentId8>`, node facts carrying the session title and a
|
|
123
|
+
* recent-activity excerpt. Joining is opt-in through the sidebar control's
|
|
124
|
+
* host routes; joined teams ride this node's card and dispatch through
|
|
125
|
+
* its `/a2a/direct` endpoint.
|
|
126
|
+
*/
|
|
127
|
+
readonly sessionNodes: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Prewarm every cold joined session's agent after the plugin mounts (dev
|
|
130
|
+
* boxes and always-on collaboration hosts that restart often: the network
|
|
131
|
+
* state returns without opening each session). Each wake materializes a
|
|
132
|
+
* full agent — a main-thread log replay — so the prewarm is deferred,
|
|
133
|
+
* foreground-yielding, and cancellable (see `wakePrewarmDelayMs` /
|
|
134
|
+
* `wakePrewarmQuietMs`); the default stays off for deployments that would
|
|
135
|
+
* rather pay on demand (wake-on-route and the sidebar's wake button remain
|
|
136
|
+
* available either way).
|
|
137
|
+
*/
|
|
138
|
+
readonly wakeJoinedOnBoot: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Idle delay between loader settlement and the first boot prewarm wake:
|
|
141
|
+
* boot traffic gets a clear window before any log replay starts. `0`
|
|
142
|
+
* restores the old fire-at-settle behavior.
|
|
143
|
+
*/
|
|
144
|
+
readonly wakePrewarmDelayMs: number;
|
|
145
|
+
/**
|
|
146
|
+
* Foreground quiet window: a wake/route demand inside this window (or any
|
|
147
|
+
* outbound route still in flight) postpones the next prewarm step. Panel
|
|
148
|
+
* polls never count. `0` disables the yield.
|
|
149
|
+
*/
|
|
150
|
+
readonly wakePrewarmQuietMs: number;
|
|
151
|
+
/**
|
|
152
|
+
* Pause between consecutive boot prewarm wakes. Each wake replays a full
|
|
153
|
+
* session log — the zstd decode yields the event loop only every 500ms —
|
|
154
|
+
* so unbounded concurrent wakes of several huge logs starve every request
|
|
155
|
+
* for tens of seconds after a restart. The serial queue with this pause
|
|
156
|
+
* keeps the preheat while capping decode saturation, and wake-on-route
|
|
157
|
+
* never waits on this queue.
|
|
158
|
+
*/
|
|
159
|
+
readonly wakeBootStaggerMs: number;
|
|
160
|
+
/**
|
|
161
|
+
* F8: continuous desired-state reconciliation. The boot prewarm is a
|
|
162
|
+
* one-shot queue; the reconciler re-derives the due set every tick and
|
|
163
|
+
* wakes one cold intent per pass — covering post-boot drift, failed
|
|
164
|
+
* wakes, and gateways that mounted late. Default on.
|
|
165
|
+
*/
|
|
166
|
+
readonly wakeReconcile: boolean;
|
|
167
|
+
/** Cadence between reconciliation ticks (floored at 50ms). */
|
|
168
|
+
readonly wakeReconcileIntervalMs: number;
|
|
169
|
+
/** First-retry delay after a failed reconcile wake; doubles per attempt. */
|
|
170
|
+
readonly wakeReconcileBackoffBaseMs: number;
|
|
171
|
+
/** Backoff ceiling; corrupt-log rows park as needs-repair and never retry. */
|
|
172
|
+
readonly wakeReconcileMaxBackoffMs: number;
|
|
173
|
+
/**
|
|
174
|
+
* S4 (gh#11 N7, operator directive: team creation is a human act): the
|
|
175
|
+
* joinable-team curation list for a2a_team_join. ONLY an EXACT entry
|
|
176
|
+
* authorizes a join; trailing-`*` pattern entries stay readable here
|
|
177
|
+
* (whoami shows them) but carry no mint rights — a pattern that
|
|
178
|
+
* pre-authorized every future name under it would let a model mint teams
|
|
179
|
+
* nobody created. Empty list denies every join (default).
|
|
180
|
+
*/
|
|
181
|
+
readonly teamJoinAllowlist: string[];
|
|
182
|
+
/** Human-owned stable display labels keyed by full local session id; never a routing identity. */
|
|
183
|
+
readonly seatNames: Record<string, unknown>;
|
|
184
|
+
/**
|
|
185
|
+
* S3 (phase-2 default-ON, owner ruling 2026-09-01): route dispatch checks
|
|
186
|
+
* caller↔target shared team membership before addressing; set false to
|
|
187
|
+
* restore the open outbound seam. (Kept in sync with the schema comment.)
|
|
188
|
+
*/
|
|
189
|
+
readonly teamScopeRouting: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* S5 (owner directive 2026-10-20): information-plane scoping — the
|
|
192
|
+
* read-only fleet tools (a2a_teams / a2a_status / a2a_probe) disclose
|
|
193
|
+
* only teams joined by this host's nodes (the union of local roster
|
|
194
|
+
* declarations). [this host] rows stay visible; a2a_tasks (this host's
|
|
195
|
+
* own ledger) stays unscoped. Set false for the open plane.
|
|
196
|
+
*/
|
|
197
|
+
readonly teamScopeInfo: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Peer boundary model (docs/design/peer-boundary-model.md) — connection
|
|
200
|
+
* layer: an empty list keeps the historical open-referral posture; a
|
|
201
|
+
* non-empty list admits a referral only when its host matches an entry
|
|
202
|
+
* (exact host[:port], bare domain suffix, or full URL). Seeds from
|
|
203
|
+
* `peers` are owner-configured and unaffected.
|
|
204
|
+
*/
|
|
205
|
+
readonly peerAllowlist: string[];
|
|
206
|
+
/**
|
|
207
|
+
* Peer boundary model — connection layer: when false, referral URLs are
|
|
208
|
+
* neither learned nor offered; the tracked set stays the configured seed
|
|
209
|
+
* set for the lifetime of the config that named them.
|
|
210
|
+
*/
|
|
211
|
+
readonly referralLearning: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* v0.5.23 (async-stall): see the schema comment on asyncNudgeDelayMs.
|
|
214
|
+
*/
|
|
215
|
+
readonly asyncNudgeDelayMs: number;
|
|
216
|
+
/** gh#224: receipt-owing tasks one target may queue on this host before new deliveries are refused (0 = off). */
|
|
217
|
+
readonly targetQueueCap: number;
|
|
218
|
+
/**
|
|
219
|
+
* How long the state route serves the cold-row id set from cache before
|
|
220
|
+
* re-enumerating the persistence layer. The panel polls every 2s; the
|
|
221
|
+
* default keeps one full enumeration per 5s instead of one per poll.
|
|
222
|
+
*/
|
|
223
|
+
readonly stateColdRowsTtlMs: number;
|
|
224
|
+
/**
|
|
225
|
+
* Serve window for a verified peer card from the shared cache. Cards are
|
|
226
|
+
* signed for days and re-signed at TTL/4, so 60s is semantically safe; a
|
|
227
|
+
* peer that restarts with a fresh card is picked up on the next window.
|
|
228
|
+
*/
|
|
229
|
+
readonly cardCacheTtlMs: number;
|
|
230
|
+
/** Shorter window for unreachable/invalid cards, so a revived peer reappears quickly. */
|
|
231
|
+
readonly cardCacheNegativeTtlMs: number;
|
|
232
|
+
/**
|
|
233
|
+
* Serve window for the panel\x27s remote rows. The refresh itself rides the
|
|
234
|
+
* shared card cache, so this is pure sweep cadence: how often real
|
|
235
|
+
* network activity happens while the panel polls every 2s.
|
|
236
|
+
*/
|
|
237
|
+
readonly remoteRowsTtlMs: number;
|
|
238
|
+
/**
|
|
239
|
+
* `final` reply budget: how long an inbound direct route waits for the
|
|
240
|
+
* session's next assistant message before answering with a timeout
|
|
241
|
+
* notice. Without it a session that never idles (or never produces text)
|
|
242
|
+
* leaves the caller hanging until `routeTimeoutMs` expires.
|
|
243
|
+
*/
|
|
244
|
+
readonly flushTimeoutMs: number;
|
|
245
|
+
/**
|
|
246
|
+
* `a2a_route` execution budget: how long a call may plausibly wait for a
|
|
247
|
+
* remote session's reply (default 30 minutes).
|
|
248
|
+
*/
|
|
249
|
+
readonly routeTimeoutMs: number;
|
|
250
|
+
/**
|
|
251
|
+
* Age at which an unsettled async task auto-dead-letters (exits the owed
|
|
252
|
+
* view, disarms the nudge, still settles on a late receipt). Default
|
|
253
|
+
* 24h; the mesh anomaly suite injects sub-second windows to exercise the
|
|
254
|
+
* dead-letter → revival lifecycle without wall-clock waits.
|
|
255
|
+
*/
|
|
256
|
+
readonly taskStaleTtlMs: number;
|
|
257
|
+
/**
|
|
258
|
+
* Opt-in native-teams inbound bridge: when composed with
|
|
259
|
+
* `@nelsonlongxiang/dsh-native-teams`, a routed team name that its
|
|
260
|
+
* registry classifies as an unambiguous local claim dispatches through
|
|
261
|
+
* its authoritative routing seam (`/a2a/direct`, the outbound A2A tools,
|
|
262
|
+
* and the directory listing). Off by default — exposing every registered
|
|
263
|
+
* team to the network is an operator decision (exposure-grants
|
|
264
|
+
* governance: grants are deliberate, never ambient). Dispatcher-level
|
|
265
|
+
* only: inbound callers address the team, never its individual members
|
|
266
|
+
* (members stay visible-not-addressable).
|
|
267
|
+
*/
|
|
268
|
+
readonly nativeTeamsInbound?: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Reply-wait budget for one native-teams round (the bridge's answer
|
|
271
|
+
* deadline, mirroring the steer path's 180s deadline). A round still
|
|
272
|
+
* running past it answers the honest delivered-unsettled shape instead
|
|
273
|
+
* of parking the caller; the round itself keeps going.
|
|
274
|
+
*/
|
|
275
|
+
readonly nativeRoundWaitMs?: number;
|
|
276
|
+
}
|
|
277
|
+
/** Schemastery configuration for the A2A client plugin row. */
|
|
278
|
+
export declare const Config: s<Config>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type TaskLedger } from './task-ledger.ts';
|
|
2
|
-
import type
|
|
1
|
+
import { type TaskLedger, type TaskRecord } from './task-ledger.ts';
|
|
2
|
+
import { type TaskAuthorization } from './task-auth.ts';
|
|
3
3
|
import type { A2aPeerCard, A2aRouteResult } from './types.ts';
|
|
4
4
|
export interface ForwardRequest {
|
|
5
5
|
team: string;
|
|
@@ -12,9 +12,17 @@ export interface ForwardRequest {
|
|
|
12
12
|
guarded: boolean;
|
|
13
13
|
auth?: TaskAuthorization;
|
|
14
14
|
}
|
|
15
|
+
/** An early receipt waits at most this long for its forward row to be born (#298). */
|
|
16
|
+
export declare const BIRTH_WAIT_CAP_MS = 60000;
|
|
15
17
|
/** Birth barrier exists only for eligible, actively dispatched tasks; receipts never reserve a slot. */
|
|
16
18
|
export declare class ForwardBirths {
|
|
19
|
+
private readonly waitCapMs;
|
|
17
20
|
private readonly pending;
|
|
21
|
+
constructor(waitCapMs?: number);
|
|
22
|
+
/**
|
|
23
|
+
* Resolves when the birth is released or after the cap, whichever is first. After the cap the receipt
|
|
24
|
+
* is judged by the normal rules against whatever row exists then — never accepted in advance (#298).
|
|
25
|
+
*/
|
|
18
26
|
wait(taskId: string): Promise<void> | undefined;
|
|
19
27
|
begin(taskId: string): (() => void) | undefined;
|
|
20
28
|
}
|
|
@@ -23,7 +31,6 @@ export interface DirectForwardDeps {
|
|
|
23
31
|
enabled(): boolean;
|
|
24
32
|
isolated(): boolean;
|
|
25
33
|
deposited(handle: string): boolean;
|
|
26
|
-
memberLinked(handle: string): boolean;
|
|
27
34
|
scope(handle: string, team: string): Promise<{
|
|
28
35
|
ok: true;
|
|
29
36
|
} | {
|
|
@@ -39,3 +46,18 @@ export interface DirectForwardDeps {
|
|
|
39
46
|
activity(team: string, peer: string, ok: boolean): void;
|
|
40
47
|
}
|
|
41
48
|
export declare function createDirectForward(d: DirectForwardDeps): (request: ForwardRequest) => Promise<A2aRouteResult>;
|
|
49
|
+
/**
|
|
50
|
+
* #282 S3 (same team authorizes): the admission basis for a receipt delivered to a local CLI mailbox
|
|
51
|
+
* from a member the home never linked — only for that CLI's own pending forward, only from a receiver
|
|
52
|
+
* frozen at birth, only when the body-bound (Sig3) claim's card key is the frozen peer key. Anything else:
|
|
53
|
+
* undefined. Trust assumption (design D2): the selected owner host is honest about its own canvas — it may
|
|
54
|
+
* list any label as a receiver, including one of this host's session labels in the same zone.
|
|
55
|
+
*/
|
|
56
|
+
export declare function forwardReceiptBasis(row: TaskRecord | undefined, caller: string, cliHandle: string, claim: {
|
|
57
|
+
readonly caller: string;
|
|
58
|
+
readonly bound: boolean;
|
|
59
|
+
readonly bodyBound?: true;
|
|
60
|
+
readonly card: {
|
|
61
|
+
readonly publicKey: string;
|
|
62
|
+
};
|
|
63
|
+
}): string | undefined;
|
|
@@ -151,6 +151,8 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
|
|
|
151
151
|
lastAt: number;
|
|
152
152
|
}>;
|
|
153
153
|
readonly ownCardUrl: string;
|
|
154
|
+
/** #282 S2: this host's canvas owner fingerprint. */
|
|
155
|
+
readonly ownCanvasFingerprint: string;
|
|
154
156
|
ownerSeatNames: Record<string, string>;
|
|
155
157
|
ownerAllowlist: string[];
|
|
156
158
|
ownerTeamScopeInfo: boolean;
|
|
@@ -111,6 +111,7 @@ export type OriginClaimResult = {
|
|
|
111
111
|
card: A2aPeerCard;
|
|
112
112
|
bound: boolean;
|
|
113
113
|
cli?: true;
|
|
114
|
+
bodyBound?: true;
|
|
114
115
|
} | {
|
|
115
116
|
ok: false;
|
|
116
117
|
reason: string;
|
|
@@ -131,10 +132,16 @@ export interface WireDeps {
|
|
|
131
132
|
readonly rawBody?: Uint8Array;
|
|
132
133
|
}) => Promise<OriginClaimResult>;
|
|
133
134
|
readonly forwardDirect: (request: ForwardRequest) => Promise<A2aRouteResult>;
|
|
135
|
+
/** #282 S2: this host's canvas owner fingerprint. */
|
|
136
|
+
readonly ownCanvasFingerprint: string;
|
|
134
137
|
readonly diagnoseCanvas: (team: string, reason: string, caller?: string) => Promise<string>;
|
|
135
138
|
readonly remoteCliProof: (handle: string, claim: Extract<OriginClaimResult, {
|
|
136
139
|
ok: true;
|
|
137
140
|
}>) => Promise<RemoteCliProof | undefined>;
|
|
141
|
+
/** #282 S3: the basis for a receipt to a local CLI from a member frozen on this host's own forward, else undefined. */
|
|
142
|
+
readonly forwardReceiptBasis: (taskId: string, caller: string, cliHandle: string, claim: Extract<OriginClaimResult, {
|
|
143
|
+
ok: true;
|
|
144
|
+
}>) => string | undefined;
|
|
138
145
|
readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
|
|
139
146
|
/** This host's own card URL (read when a request lands; set once the web server settles). */
|
|
140
147
|
readonly ownCardUrl: string;
|