@nelsonlongxiang/dsh-open-a2a-net 0.6.30 → 0.6.32
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 +4 -1
- package/README_zh.md +3 -0
- package/assets/nexusDist/assets/index-D5H8R8de.js +9 -0
- package/assets/nexusDist/assets/index-D9GYZyGZ.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +445 -57
- package/lib/a2a-client.js +9 -4
- package/lib/auth-observer.js +254 -0
- package/lib/blob-store.js +210 -0
- package/lib/card.js +4 -2
- package/lib/cli-deposit.js +122 -7
- package/lib/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/faces/admin.js +133 -43
- package/lib/faces/canvas-notify.js +12 -3
- package/lib/faces/files.js +155 -0
- package/lib/faces/nodes.js +171 -0
- package/lib/faces/session-log.js +162 -0
- package/lib/faces/wire.js +206 -39
- package/lib/index.js +809 -1720
- package/lib/joined-store.js +23 -0
- package/lib/ledger-conservation.js +97 -0
- package/lib/legacy/teams-bridge-inbound.js +156 -0
- package/lib/node-bind.js +534 -0
- package/lib/node-github.js +72 -0
- package/lib/node-self.js +453 -0
- package/lib/node-store.js +484 -0
- package/lib/node-tasks.js +342 -0
- package/lib/origin-claim.js +246 -0
- package/lib/peer-directory.js +134 -0
- package/lib/receipt.js +26 -1
- package/lib/skill/SKILL.md +3 -1
- package/lib/skill/references/cli.md +82 -3
- package/lib/skill/references/collaboration.md +146 -73
- package/lib/skill/references/discovery.md +29 -0
- package/lib/skill/references/intake.md +107 -0
- package/lib/skill/references/router.md +96 -0
- package/lib/skill/references/setup.md +2 -1
- package/lib/task-auth.js +483 -0
- package/lib/task-ledger.js +324 -12
- package/lib/team-store.js +24 -1
- package/lib/tools/act.js +383 -0
- package/lib/tools/files.js +201 -0
- package/lib/tools/info.js +684 -0
- package/lib/types/a2a-client.d.ts +10 -4
- package/lib/types/auth-observer.d.ts +110 -0
- package/lib/types/blob-store.d.ts +83 -0
- package/lib/types/cli-deposit.d.ts +38 -0
- package/lib/types/faces/admin.d.ts +14 -2
- package/lib/types/faces/canvas-notify.d.ts +4 -1
- package/lib/types/faces/files.d.ts +53 -0
- package/lib/types/faces/nodes.d.ts +69 -0
- package/lib/types/faces/session-log.d.ts +44 -0
- package/lib/types/faces/wire.d.ts +50 -5
- package/lib/types/index.d.ts +27 -0
- package/lib/types/joined-store.d.ts +9 -0
- package/lib/types/ledger-conservation.d.ts +77 -0
- package/lib/types/legacy/teams-bridge-inbound.d.ts +94 -0
- package/lib/types/node-bind.d.ts +220 -0
- package/lib/types/node-github.d.ts +35 -0
- package/lib/types/node-self.d.ts +178 -0
- package/lib/types/node-store.d.ts +180 -0
- package/lib/types/node-tasks.d.ts +121 -0
- package/lib/types/origin-claim.d.ts +119 -0
- package/lib/types/peer-directory.d.ts +39 -0
- package/lib/types/receipt.d.ts +12 -0
- package/lib/types/task-auth.d.ts +271 -0
- package/lib/types/task-ledger.d.ts +143 -6
- package/lib/types/team-store.d.ts +9 -0
- package/lib/types/tools/act.d.ts +52 -0
- package/lib/types/tools/files.d.ts +38 -0
- package/lib/types/tools/info.d.ts +102 -0
- package/lib/types/types.d.ts +6 -0
- package/nexus-stage/src/canvas-ops.ts +17 -2
- package/nexus-stage/src/canvas-wire.ts +27 -19
- package/nexus-stage/src/federation.ts +1 -0
- package/nexus-stage/src/main.ts +41 -18
- package/nexus-stage/src/planning-member-handles.ts +34 -0
- package/nexus-stage/src/planning-mirror.ts +47 -3
- package/nexus-stage/src/planning-reachability.ts +28 -0
- package/nexus-stage/src/planning-types.ts +1 -1
- package/nexus-stage/src/planning-view.ts +30 -27
- package/nexus-stage/src/planning.css +4 -0
- package/nexus-stage/src/wb-directory.ts +13 -0
- package/nexus-stage/src/workbench.css +3 -0
- package/nexus-stage/src/workbench.ts +5 -1
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-C_x3iAth.js +0 -9
- package/assets/nexusDist/assets/index-Dz8N80tI.css +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project a verified peer card into directory rows and identify its own
|
|
3
|
+
* advertised canvas teams (#159). The card's core is signed; sessionTeams,
|
|
4
|
+
* teamMemberships, and canvasOrg are unsigned, serve-fresh claims. Nothing
|
|
5
|
+
* here grants admission: the caller must still apply its existing scope and
|
|
6
|
+
* the receiver must still enforce OriginClaim and target-level membership.
|
|
7
|
+
*
|
|
8
|
+
* @module src/peer-directory
|
|
9
|
+
*/
|
|
10
|
+
import { CANVAS_MEMBER_CAP, CANVAS_NAME_MAX, CANVAS_TEAM_CAP } from "./canvas-store.js";
|
|
11
|
+
/** Only this card's own valid canvas namespace may name its teams. */
|
|
12
|
+
export function advertisedCanvasTeams(card) {
|
|
13
|
+
const found = [];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const entry of (card.canvasOrg ?? []).slice(0, CANVAS_TEAM_CAP)) {
|
|
16
|
+
const name = entry.name;
|
|
17
|
+
if (name === '' || name !== name.trim() || name.length > CANVAS_NAME_MAX || name.includes('/') || /^\d+$/.test(name))
|
|
18
|
+
continue;
|
|
19
|
+
if (entry.team !== card.team + '/canvas/' + name || entry.members.length > CANVAS_MEMBER_CAP)
|
|
20
|
+
continue;
|
|
21
|
+
if (seen.has(entry.team))
|
|
22
|
+
continue;
|
|
23
|
+
seen.add(entry.team);
|
|
24
|
+
found.push(entry);
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
/** A nested zone may itself end in /canvas; preserve its own session alias. */
|
|
29
|
+
function sessionAliasAllowed(card, team) {
|
|
30
|
+
if (!/^.+\/canvas\/[^/]+$/.test(team))
|
|
31
|
+
return true;
|
|
32
|
+
const prefix = card.team + '/';
|
|
33
|
+
const suffix = team.startsWith(prefix) ? team.slice(prefix.length) : '';
|
|
34
|
+
return suffix !== '' && !suffix.includes('/');
|
|
35
|
+
}
|
|
36
|
+
/** Match a session claim without treating another zone's canvas as a session. */
|
|
37
|
+
export function sessionCandidateMatches(card, team) {
|
|
38
|
+
return sessionAliasAllowed(card, team) && (card.sessionTeams ?? []).some(entry => entry.team === team);
|
|
39
|
+
}
|
|
40
|
+
/** A member the publisher also serves as its own session is not ours. */
|
|
41
|
+
function peerPublishesSession(card, handle) {
|
|
42
|
+
return (card.sessionTeams ?? []).some(entry => entry.team === handle);
|
|
43
|
+
}
|
|
44
|
+
/** Match a scoped caller to this peer's canvas before sending the payload. */
|
|
45
|
+
export function canvasCandidateMatches(card, team, caller, resolveLocal) {
|
|
46
|
+
if (caller !== undefined && (resolveLocal?.(caller.handle) !== caller.id || peerPublishesSession(card, caller.handle)))
|
|
47
|
+
return false;
|
|
48
|
+
return advertisedCanvasTeams(card).some(entry => entry.team === team && (caller === undefined || entry.members.includes(caller.handle)));
|
|
49
|
+
}
|
|
50
|
+
/** A naked team string is unsafe when both a canvas and another face claim it. */
|
|
51
|
+
export function canvasTargetAmbiguous(cards, team, caller, resolveLocal, hasDelegation) {
|
|
52
|
+
const owners = cards.filter((card) => card !== undefined && advertisedCanvasTeams(card).some(entry => entry.team === team));
|
|
53
|
+
if (owners.length === 0)
|
|
54
|
+
return false;
|
|
55
|
+
if (caller !== undefined) {
|
|
56
|
+
if (resolveLocal?.(caller.handle) !== caller.id)
|
|
57
|
+
return true;
|
|
58
|
+
const ownerCollision = owners.some(card => peerPublishesSession(card, caller.handle)
|
|
59
|
+
&& advertisedCanvasTeams(card).some(entry => entry.team === team && entry.members.includes(caller.handle)));
|
|
60
|
+
if (ownerCollision)
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Distinct signed publishers with the same qualified group name cannot be
|
|
64
|
+
// selected by the team-only API. URL aliases of one signed card are one.
|
|
65
|
+
const eligible = owners.filter(card => canvasCandidateMatches(card, team, caller, resolveLocal));
|
|
66
|
+
const identities = new Set(eligible.map(card => JSON.stringify([card.publicKey, card.session])));
|
|
67
|
+
return identities.size > 1 || hasDelegation || cards.some(card => card !== undefined && (card.team === team || sessionCandidateMatches(card, team)));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Existing peer rows plus optional canvas rows for the model-tool directory.
|
|
71
|
+
* The GUI's remote-node rows call with includeCanvas=false, so a group is
|
|
72
|
+
* never rendered as an extra session node. Membership is carried only as an
|
|
73
|
+
* internal scope hint and stripped before the tool's public result.
|
|
74
|
+
*/
|
|
75
|
+
export function peerDirectoryRows(card, peer, includeCanvas) {
|
|
76
|
+
// The origin fields (publisher session label + LAN IP) are natural
|
|
77
|
+
// grouping dimensions: a fleet's rows cluster by the host that owns
|
|
78
|
+
// them, no manual grouping required.
|
|
79
|
+
const origin = card.lanIp !== undefined ? card.session + ' (' + card.lanIp + ')' : card.session;
|
|
80
|
+
// via = the peer URL the card was fetched from: the panel shows which
|
|
81
|
+
// host:port each remote row came from, so an unknown node is traceable
|
|
82
|
+
// to its publishing endpoint at a glance.
|
|
83
|
+
// legacy: the publisher's card carries no roster capability — a
|
|
84
|
+
// pre-roster node (0.5.43-). Rows from it are marked so the fleet
|
|
85
|
+
// audit (and the phase-2 teamed-only default flip) knows which peers
|
|
86
|
+
// never grow the roster face.
|
|
87
|
+
const legacy = card.capabilities?.roster !== true;
|
|
88
|
+
const rows = [
|
|
89
|
+
{ team: card.team, session: card.session, name: card.name, description: '', origin, via: peer, ...(legacy ? { legacy } : {}) },
|
|
90
|
+
];
|
|
91
|
+
// Roster reader half: the card's unsigned teamMemberships map each
|
|
92
|
+
// published session team to its declared roster teams, so a remote
|
|
93
|
+
// node's teamed/teamless state is visible without visiting its host.
|
|
94
|
+
const rosterOf = new Map();
|
|
95
|
+
for (const membership of card.teamMemberships ?? []) {
|
|
96
|
+
const list = rosterOf.get(membership.node) ?? [];
|
|
97
|
+
list.push(membership.team);
|
|
98
|
+
rosterOf.set(membership.node, list);
|
|
99
|
+
}
|
|
100
|
+
for (const entry of card.sessionTeams ?? []) {
|
|
101
|
+
// A foreign canvas-shaped claim cannot masquerade as a session. A valid
|
|
102
|
+
// own-zone session stays visible even if its zone ends in /canvas.
|
|
103
|
+
if (!sessionAliasAllowed(card, entry.team))
|
|
104
|
+
continue;
|
|
105
|
+
const declared = rosterOf.get(entry.team);
|
|
106
|
+
rows.push({ team: entry.team, session: card.session, name: entry.name, description: entry.description, origin, via: peer, ...(entry.workspace !== undefined ? { workspace: entry.workspace } : {}), ...(legacy ? { legacy } : {}), ...(declared !== undefined && declared.length > 0 ? { teams: declared } : {}) });
|
|
107
|
+
}
|
|
108
|
+
const publishedSessions = new Set((card.sessionTeams ?? []).map(entry => entry.team));
|
|
109
|
+
if (includeCanvas)
|
|
110
|
+
for (const entry of advertisedCanvasTeams(card)) {
|
|
111
|
+
const declared = rosterOf.get(entry.team);
|
|
112
|
+
rows.push({ team: entry.team, session: card.session, name: entry.name, description: 'canvas team', origin, via: peer, ...(legacy ? { legacy } : {}), ...(declared !== undefined && declared.length > 0 ? { teams: declared } : {}), canvasMembers: entry.members.filter(ref => !publishedSessions.has(ref)) });
|
|
113
|
+
}
|
|
114
|
+
return rows;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether one caller label can receive network traffic back. Composite
|
|
118
|
+
* `<zone>/<id>` shapes route anywhere (N1); bare zone names resolve to a
|
|
119
|
+
* live initiator per host, so cross-host they would steer whichever node
|
|
120
|
+
* shares the name — treated as unroutable everywhere.
|
|
121
|
+
*/
|
|
122
|
+
export function routableCallerLabel(callerSession) {
|
|
123
|
+
return callerSession !== undefined &&
|
|
124
|
+
(/[a-z0-9-]+\/[0-9a-f]{8}$/i.test(callerSession) || /[a-z0-9-]+\/[a-z0-9]{4,}$/i.test(callerSession));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Why an async route from such a caller cannot expect its receipt: the
|
|
128
|
+
* autosend arm skips it and the steered hint is omitted, so the debt has
|
|
129
|
+
* no paying path — say so at dispatch time instead of silently stranding
|
|
130
|
+
* a pending row.
|
|
131
|
+
*/
|
|
132
|
+
export function unroutableCallerNote(label) {
|
|
133
|
+
return `\n(Note: caller label "${label}" is not network-routable, so the target cannot auto-send a receipt back to you — reconcile via context_id follow-ups instead.)`;
|
|
134
|
+
}
|
package/lib/receipt.js
CHANGED
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
* open with a brace can never be mistaken for machine payloads.
|
|
12
12
|
* @module @nelsonlongxiang/dsh-open-a2a-net/receipt
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* #114 S5: what a task id may not contain. The id is spliced into the
|
|
16
|
+
* `[A2A direct] (task <id>)` steer header and the `[A2A receipt] task <id>`
|
|
17
|
+
* correlation header; a `)` would let one id's header start with another's
|
|
18
|
+
* (`foo) x` → `(task foo) x)`), and whitespace or control characters break
|
|
19
|
+
* the `\S+` correlation and the header's one-line shape.
|
|
20
|
+
*/
|
|
21
|
+
const TASK_ID_FORBIDDEN = /[\s)\u0000-\u001f\u007f]/;
|
|
22
|
+
/**
|
|
23
|
+
* Refusal text for an unusable task id, or undefined when the id is fine.
|
|
24
|
+
* Both delivery paths (wire face, local candidate) refuse at intake.
|
|
25
|
+
*/
|
|
26
|
+
export function taskIdRefusal(taskId) {
|
|
27
|
+
return TASK_ID_FORBIDDEN.test(taskId)
|
|
28
|
+
? 'task_id must not contain whitespace, ")" or control characters — it is spliced into the [A2A direct] (task <id>) and [A2A receipt] task <id> headers'
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
14
31
|
/** The network's fixed, case-sensitive correlation template. */
|
|
15
32
|
export const RECEIPT_PATTERN = /^\[A2A receipt\] task (\S+)\s*([\s\S]*)$/;
|
|
16
33
|
/** Controlled outcome vocabulary — the ONLY branchable field of an envelope. */
|
|
@@ -23,7 +40,7 @@ export const RECEIPT_OUTCOMES = [
|
|
|
23
40
|
'sync_completed',
|
|
24
41
|
'unknown',
|
|
25
42
|
];
|
|
26
|
-
const KNOWN_ENVELOPE_KEYS = ['outcome', 'idempotencyKey', 'sessionKey', 'elapsedMs', 'artifacts'];
|
|
43
|
+
const KNOWN_ENVELOPE_KEYS = ['outcome', 'idempotencyKey', 'sessionKey', 'elapsedMs', 'artifacts', 'onBehalfOf', 'authRef'];
|
|
27
44
|
void KNOWN_ENVELOPE_KEYS;
|
|
28
45
|
function tryEnvelopeLine(line) {
|
|
29
46
|
const trimmed = line.trim();
|
|
@@ -46,6 +63,10 @@ function tryEnvelopeLine(line) {
|
|
|
46
63
|
fields.sessionKey = record['sessionKey'];
|
|
47
64
|
if (typeof record['elapsedMs'] === 'number' && Number.isFinite(record['elapsedMs']))
|
|
48
65
|
fields.elapsedMs = record['elapsedMs'];
|
|
66
|
+
if (typeof record['onBehalfOf'] === 'string' && record['onBehalfOf'] !== '')
|
|
67
|
+
fields.onBehalfOf = record['onBehalfOf'];
|
|
68
|
+
if (typeof record['authRef'] === 'string' && /^[0-9a-f]{16}$/.test(record['authRef']))
|
|
69
|
+
fields.authRef = record['authRef'];
|
|
49
70
|
if (Object.keys(fields).length === 0)
|
|
50
71
|
return undefined;
|
|
51
72
|
return fields;
|
|
@@ -95,6 +116,10 @@ export function formatReceipt(taskId, summary, fields) {
|
|
|
95
116
|
ordered['sessionKey'] = fields.sessionKey;
|
|
96
117
|
if (fields.elapsedMs !== undefined)
|
|
97
118
|
ordered['elapsedMs'] = fields.elapsedMs;
|
|
119
|
+
if (fields.onBehalfOf !== undefined)
|
|
120
|
+
ordered['onBehalfOf'] = fields.onBehalfOf;
|
|
121
|
+
if (fields.authRef !== undefined)
|
|
122
|
+
ordered['authRef'] = fields.authRef;
|
|
98
123
|
const keys = Object.keys(ordered);
|
|
99
124
|
if (keys.length === 0)
|
|
100
125
|
return header;
|
package/lib/skill/SKILL.md
CHANGED
|
@@ -112,4 +112,6 @@ renders the same data for humans.
|
|
|
112
112
|
- [membership.md](references/membership.md) — join/leave, allowlist, canvas teams
|
|
113
113
|
- [pitfalls.md](references/pitfalls.md) — operational pitfalls observed in production
|
|
114
114
|
- [cli.md](references/cli.md) — the `a2a-net` CLI: deposit, mailbox, inbox hooks, limits
|
|
115
|
-
- [collaboration.md](references/collaboration.md) — collaboration norm (Chinese): single-seat first, decide-then-report, release tiers, stuck gates, credentials, receipts
|
|
115
|
+
- [collaboration.md](references/collaboration.md) — collaboration norm (Chinese): single-seat first, decide-then-report with no pre-authorization (a high-tier change needs one non-self node's review PASS on the same SHA), release tiers, stuck gates, credentials, receipts
|
|
116
|
+
- [intake.md](references/intake.md) — intake node (Chinese): handling `workbench-event` deliveries from workbench-market — accept/decline/ask with a reason, open a GitHub issue on accept, write the decision back with the reply token
|
|
117
|
+
- [router.md](references/router.md) — the chief's system prompt (Chinese), self-contained: route workbench events and owner requests to the right project's dev, order cross-project resource conflicts (it authorizes nothing since collaboration v1.18); immutable layer + self-kept mutable layer
|
|
@@ -28,6 +28,11 @@ ten model tools.
|
|
|
28
28
|
its card is no longer served, but it stays in the host's directory as an
|
|
29
29
|
OFFLINE row (gh#47; the newest 16 are kept) so canvas refs to it never
|
|
30
30
|
dangle silently. Publish again (same handle, same key) to come back.
|
|
31
|
+
- A handle stays pinned to the key it was published with, live or offline
|
|
32
|
+
(gh#137): publishing it with a different key (e.g. after `init --force
|
|
33
|
+
--uuid <same>`) is refused with "held by another key". Run `a2a-net revoke`
|
|
34
|
+
first; the next publish then takes the new key. A key swap is therefore
|
|
35
|
+
always an explicit act, never a silent overwrite.
|
|
31
36
|
- While deposited, the host serves the card at
|
|
32
37
|
`<home>/a2a/cli/<handle>/.well-known/agent-card.json` and lists the handle
|
|
33
38
|
in its directory as "cli — deposited identity (no wake; mailbox)".
|
|
@@ -37,16 +42,75 @@ ten model tools.
|
|
|
37
42
|
405 with an empty body. The fix is a host upgrade, not a retry.
|
|
38
43
|
- `a2a-net revoke` deregisters early.
|
|
39
44
|
|
|
45
|
+
## Files (#91 file channel)
|
|
46
|
+
|
|
47
|
+
- `a2a-net file send <path…> <team> [--note TEXT]` stages each file (≤64 MiB,
|
|
48
|
+
≤16 per send) on the home host by sha256, granted to `<team>` for 24h, and
|
|
49
|
+
routes ONE message with one `[A2A file] v1 sha256=… size=… name=… from=<home>`
|
|
50
|
+
line per file. File bytes never ride a message.
|
|
51
|
+
- `a2a-net file fetch "<reference line>" [--out DIR]` pulls a referenced file,
|
|
52
|
+
checks its size and sha256, and saves it as `<sha12>-<name>` (no links
|
|
53
|
+
followed, no overwrite of other content). A CLI identity proves itself only
|
|
54
|
+
on its HOME host, so only files whose `from` is the home host can be fetched;
|
|
55
|
+
for a file on another host, ask the sender to send it through your home host.
|
|
56
|
+
- Sessions use the `a2a_file_send` / `a2a_file_fetch` tools for the same thing.
|
|
57
|
+
|
|
40
58
|
## Sending
|
|
41
59
|
|
|
42
60
|
- `a2a-net route <team> <message…>` delivers a round with a signed
|
|
43
61
|
OriginClaim envelope to the HOME host's `/a2a/direct`. It is async by
|
|
44
62
|
default: the receipt comes back to `a2a-net inbox`.
|
|
63
|
+
- The envelope (`X-A2A-Caller`, `X-A2A-Caller-Url`, `X-A2A-Caller-Ts`,
|
|
64
|
+
`X-A2A-Caller-Sig`) is signed with the identity key, over the identity's
|
|
65
|
+
deposit face on the home host. It also carries the bound form
|
|
66
|
+
(`X-A2A-Caller-Aud`, `X-A2A-Caller-Sig2`, docs/protocol/delivery-origin-auth.md
|
|
67
|
+
§2.1): a signature over the origin, method and path the CLI actually
|
|
68
|
+
dials, so it is good only at that host's `/a2a/direct`. A host with gh#133 verifies it on the
|
|
69
|
+
delivery face, from its own deposit table (it never dials the CLI's
|
|
70
|
+
address back, so a proxy or tailnet address is fine): the delivery's
|
|
71
|
+
authorization record says `proof: cli-identity` with the full handle as
|
|
72
|
+
the locally deposited identity; it is not market-origin proof and does not
|
|
73
|
+
count as an absent-envelope legacy delivery.
|
|
74
|
+
- An older host refuses the envelope as `not-derivable`. The CLI says so on
|
|
75
|
+
stderr and resends the same task id once unsigned, the way earlier CLIs
|
|
76
|
+
always sent (accepted only while the host's OriginClaim window is open).
|
|
77
|
+
- Dialling the home through an address the host does not list as its own
|
|
78
|
+
(a Tailscale address, a card-proxy port such as 13080, a pocket tunnel)
|
|
79
|
+
makes the bound form miss. While the host's `originClaimUnboundWindow` is
|
|
80
|
+
open the route still lands and the miss shows only on the host, in
|
|
81
|
+
`boundMissInbound`; `a2a-net status` prints that gauge beside the address
|
|
82
|
+
the CLI dials. Once the window is closed the route is refused as
|
|
83
|
+
`wrong-audience`: stderr names the dialled origin and the fix — the host
|
|
84
|
+
operator adds it to `originClaimAudiences`, or `--home` / `$A2A_HOME`
|
|
85
|
+
points at an address the host lists. A proxy that rewrites the path
|
|
86
|
+
(`<home>` with a path prefix) fails the same way as `bad-sig`. These
|
|
87
|
+
refusals are never resent unsigned. A redirect from the home is never
|
|
88
|
+
followed either, on any command (the API key, pocket cookie and claim
|
|
89
|
+
would travel with it): the command fails naming the status and
|
|
90
|
+
`Location`, and a route is not resent.
|
|
91
|
+
- An identity may not be named after the host's zone (the deposit is refused).
|
|
45
92
|
- `--sync` waits for the target's reply and prints it:
|
|
46
93
|
`reply from <team> · task <id> · context <ctx>` followed by the reply text.
|
|
47
94
|
When the round has not settled (status other than completed) a warning
|
|
48
95
|
goes to stderr: reconcile by task id before re-sending.
|
|
49
96
|
- `--context ID` continues that conversation (sends it as `context_id`).
|
|
97
|
+
- Every route and report carries a task id born in the CLI
|
|
98
|
+
(`direct-<12 hex>`). The host runs one task id at most once (same id and
|
|
99
|
+
same message: 409 replay; different message: 409 conflict). Resending is
|
|
100
|
+
therefore safe:
|
|
101
|
+
- A dropped connection or a 5xx is resent automatically, up to 3 times,
|
|
102
|
+
with the SAME id (backoff 1s, 2s, 4s).
|
|
103
|
+
- If it still fails, stderr names the id: rerun with `--task <id>` to
|
|
104
|
+
resend it without risking a second execution.
|
|
105
|
+
- A resend the host already has prints `already received … — not sent a
|
|
106
|
+
second time` with the earlier outcome from `/a2a/query`: `pending` (an
|
|
107
|
+
async delivery waiting for its receipt) or `completed` (with its reply).
|
|
108
|
+
If that earlier attempt failed, the CLI exits 1 with its reason.
|
|
109
|
+
- A `--sync` resend whose round has no reply yet exits 1: rerun later
|
|
110
|
+
with `--task <id> --sync` to fetch the reply.
|
|
111
|
+
- Reusing an id for a different message, or switching `--sync` on or off,
|
|
112
|
+
is refused (exit 1): drop `--task` to send it as a new task.
|
|
113
|
+
- `report` takes `--task` the same way.
|
|
50
114
|
- Direct delivery does not relay: only teams on the home host are reachable.
|
|
51
115
|
A team on another host answers "No live DSH session node accepts team".
|
|
52
116
|
- Admission: a deposited handle is a node fact of its home host (N3). It
|
|
@@ -61,12 +125,27 @@ ten model tools.
|
|
|
61
125
|
|
|
62
126
|
- Deliveries addressed to a deposited handle, receipts included, land in its
|
|
63
127
|
mailbox on the home host and answer `receipt: "mailbox"`. Entries are
|
|
64
|
-
`{at, from, text}
|
|
65
|
-
|
|
128
|
+
`{at, from, text, seq}`, newest first, 64 kept. A read does not remove
|
|
129
|
+
anything.
|
|
130
|
+
- `seq` numbers a handle's mail 1, 2, 3 … in arrival order. It is never
|
|
131
|
+
reused and survives restarts. The read also carries `epoch`; `next_seq`;
|
|
132
|
+
`oldest_seq`; and `dropped`, the count of mail the 64-row cap pushed out.
|
|
133
|
+
`epoch` changes whenever seq numbers could repeat: the store starts empty,
|
|
134
|
+
its snapshot is unreadable or renumbered, or a write to disk failed (the
|
|
135
|
+
next restart would reload older numbers).
|
|
136
|
+
- Text over 4000 chars is cut and marked `truncated: true, chars: <original>`.
|
|
137
|
+
- The host's state face totals both losses for every handle as
|
|
138
|
+
`cliMailbox: {dropped, truncated}`.
|
|
66
139
|
- `a2a-net inbox` lists the whole mailbox.
|
|
67
140
|
- `inbox --new [--peek]` shows unread mail, oldest first. "Unread" is a local
|
|
68
141
|
cursor (`$A2A_CLI_HOME/inbox.cursor`, tied to the handle) that advances
|
|
69
|
-
unless `--peek`.
|
|
142
|
+
unless `--peek`. On a host with `seq` the cursor is the last seq read:
|
|
143
|
+
- Mail the cap pushed out before you read it is reported once, by
|
|
144
|
+
`--new`, `--wait` and `--hook` alike (stderr, or inside the hook frame):
|
|
145
|
+
`N messages were pushed out of the mailbox before they were read`. It
|
|
146
|
+
cannot be recovered; tell the user.
|
|
147
|
+
- On an older host, or after the store starts over (a new `epoch`), the
|
|
148
|
+
cursor falls back to the newest `at` seen.
|
|
70
149
|
- `inbox --wait [--timeout 30m] [--interval 15s] [--renew]` blocks until
|
|
71
150
|
unread mail arrives, then prints it and exits 0; exits 3 on timeout.
|
|
72
151
|
`--renew` re-publishes the deposit when it is gone or under 10 minutes from
|