super-dsh 0.1.6 → 0.1.9
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/client/index.js +741 -256
- package/cordis.patch.yml +16 -0
- package/package.json +7 -2
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +29 -18
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +96 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +15 -1
- package/superd-hub/dist/index.js +16 -0
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +6 -0
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +62 -25
- package/superd-hub/dist/switch-core.js +53 -30
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/lib/client/index.js +741 -256
- package/superd-hub/package.json +11 -2
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub-to-hub A2A envelope protocol v1 (postoffice R1; ADR 0011, spec
|
|
3
|
+
* machine-mesh-a2a D16–D19, plan 2026-09-28-machine-mesh-postoffice-rework
|
|
4
|
+
* 契约要点).
|
|
5
|
+
*
|
|
6
|
+
* THE only versioned cross-machine contract in the mesh (D16): everything
|
|
7
|
+
* hub-to-hub rides these envelopes — opaque payloads addressed by
|
|
8
|
+
* `<m4>-<a6>`; the session plane never crosses a machine boundary. Local
|
|
9
|
+
* delivery is the SAME protocol short-circuited in-process (R4), so wire
|
|
10
|
+
* shape = local shape by construction.
|
|
11
|
+
*
|
|
12
|
+
* Frame: `{v: 1, envelope}`. Request envelopes (initiator hub → target hub):
|
|
13
|
+
* new-conversation — create + inject (conversationId = initiator-minted
|
|
14
|
+
* shortId, title already normalized to the
|
|
15
|
+
* '[undefined]' placeholder when unspecified, D17)
|
|
16
|
+
* message — resume + inject on the adopted shortId
|
|
17
|
+
* stop — cancel the target machine's local turn
|
|
18
|
+
* Return envelopes (target hub → initiator hub):
|
|
19
|
+
* reply — final assistant text of a captured completion (D3: never empty;
|
|
20
|
+
* an aborted turn without text is a notice instead)
|
|
21
|
+
* notice — bounded state account (summary ≤120, mirrors upstream
|
|
22
|
+
* CONTEXT_SUMMARY_MAX_CHARS)
|
|
23
|
+
*
|
|
24
|
+
* Endpoints (registered by mesh-rpc in R3, consumed here):
|
|
25
|
+
* POST /api/superd/a2a body = frame → receipt
|
|
26
|
+
* GET /api/superd/a2a/outbox?machine=<m4> → claim & clear the buffered
|
|
27
|
+
* envelopes addressed to that machine
|
|
28
|
+
* (declarative claim inside the trust
|
|
29
|
+
* model, D1 — the consumer holds full
|
|
30
|
+
* authority over its own outbox).
|
|
31
|
+
*
|
|
32
|
+
* Receipts (contract): new/message → `{outcome,messageId,conversationId,
|
|
33
|
+
* sessionId}` ('delivered' on every successful prompt submission — end-to-end
|
|
34
|
+
* delivery complete; 'failed' carries an error body); stop → `{stopped,scope,reason?}`.
|
|
35
|
+
*
|
|
36
|
+
* Guards are STRICT: exact key sets per kind, non-empty strings everywhere,
|
|
37
|
+
* no unknown fields. Version skew is the outer `v`'s job; within v1 a
|
|
38
|
+
* malformed envelope is a protocol violation and fails loud (the collision
|
|
39
|
+
* case — adopting a foreign shortId — likewise rejects loud at ingest, R3).
|
|
40
|
+
*/
|
|
41
|
+
import type { WireError } from './envelope.js';
|
|
42
|
+
/** The wire-protocol version this module speaks (and its guards accept). */
|
|
43
|
+
export declare const A2A_WIRE_VERSION = 1;
|
|
44
|
+
/** Registry/wire title placeholder when the operator specified none (D17). */
|
|
45
|
+
export declare const A2A_TITLE_PLACEHOLDER = "[undefined]";
|
|
46
|
+
/** The a2a ingest endpoint (fence + cookie gate ride the connection channel). */
|
|
47
|
+
export declare const A2A_ENDPOINT_PATH = "/api/superd/a2a";
|
|
48
|
+
/** The outbox claim endpoint — `?machine=<m4>` names the CLAIMING machine. */
|
|
49
|
+
export declare const A2A_OUTBOX_PATH = "/api/superd/a2a/outbox";
|
|
50
|
+
/** Bound for a notice summary — mirrors upstream CONTEXT_SUMMARY_MAX_CHARS (119 chars + '…' when bounded). */
|
|
51
|
+
export declare const A2A_NOTICE_SUMMARY_MAX = 120;
|
|
52
|
+
/** Wire call budget — matches mesh-link's FETCH_TIMEOUT_MS (10s). */
|
|
53
|
+
export declare const A2A_WIRE_TIMEOUT_MS = 10000;
|
|
54
|
+
/** Request-side destination: machine id (m4) + agent id (a6). */
|
|
55
|
+
export interface A2aWireTarget {
|
|
56
|
+
machine: string;
|
|
57
|
+
id: string;
|
|
58
|
+
}
|
|
59
|
+
/** Return-side destination: the initiator's machine/agent/session (FULL-length sessionId, D6). */
|
|
60
|
+
export interface A2aWireOrigin {
|
|
61
|
+
machine: string;
|
|
62
|
+
agent: string;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
}
|
|
65
|
+
/** Prompt delivery mode (upstream `SessionPromptRequest.mode`); default 'queue' — peer semantics. */
|
|
66
|
+
export type A2aWireDelivery = 'queue' | 'steer';
|
|
67
|
+
export interface A2aNewConversationEnvelope {
|
|
68
|
+
kind: 'new-conversation';
|
|
69
|
+
to: A2aWireTarget;
|
|
70
|
+
/** Initiator-minted shortId; the target adopts it verbatim (mirror row). */
|
|
71
|
+
conversationId: string;
|
|
72
|
+
/** Already normalized: '[undefined]' when the operator specified none (D17). */
|
|
73
|
+
title: string;
|
|
74
|
+
message: string;
|
|
75
|
+
from: A2aWireOrigin;
|
|
76
|
+
messageId: string;
|
|
77
|
+
delivery?: A2aWireDelivery;
|
|
78
|
+
}
|
|
79
|
+
export interface A2aMessageEnvelope {
|
|
80
|
+
kind: 'message';
|
|
81
|
+
to: A2aWireTarget;
|
|
82
|
+
conversationId: string;
|
|
83
|
+
message: string;
|
|
84
|
+
from: A2aWireOrigin;
|
|
85
|
+
messageId: string;
|
|
86
|
+
delivery?: A2aWireDelivery;
|
|
87
|
+
}
|
|
88
|
+
export interface A2aStopEnvelope {
|
|
89
|
+
kind: 'stop';
|
|
90
|
+
to: A2aWireTarget;
|
|
91
|
+
conversationId: string;
|
|
92
|
+
scope: 'turn' | 'session';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Sender identity on a return envelope — the machine+agent that PRODUCED the
|
|
96
|
+
* reply/notice. Additive v1 field: absent on older peers, in which case the
|
|
97
|
+
* receiver falls back to registry attribution (the local row for the
|
|
98
|
+
* conversation points at the peer). Never the receiver's own id.
|
|
99
|
+
*/
|
|
100
|
+
export interface A2aReturnSender {
|
|
101
|
+
readonly machine: string;
|
|
102
|
+
readonly agent: string;
|
|
103
|
+
}
|
|
104
|
+
export interface A2aReplyEnvelope {
|
|
105
|
+
kind: 'reply';
|
|
106
|
+
to: A2aWireOrigin;
|
|
107
|
+
/** Conversation shortId (requests say `conversationId`; returns say `conversation` — contract asymmetry, kept verbatim). */
|
|
108
|
+
conversation: string;
|
|
109
|
+
text: string;
|
|
110
|
+
messageId: string;
|
|
111
|
+
/** Who replied — the hosting machine + host agent (additive v1). */
|
|
112
|
+
sender?: A2aReturnSender;
|
|
113
|
+
/** D20 (additive v1): present iff the text is the runtime's TRANSCRIPTION of the peer's turn-end last output — the peer never sent it through the channel (latch never set). The delivering side renders the provenance prefix from this flag; absent = a channel reply the peer chose to send. */
|
|
114
|
+
transcribed?: true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Why a runtime-owned notice was produced — THE cause taxonomy of the a2a
|
|
118
|
+
* face (0.1.7-h R2: this crosses the wire now; the additive `reason` field
|
|
119
|
+
* rides the notice envelope and the receiver renders it into the model-facing
|
|
120
|
+
* envelope line, so "turn ended, no reply" stops being three different facts
|
|
121
|
+
* wearing one sentence). 'peer' = an account minted by the PEER hub and
|
|
122
|
+
* relayed in; absent wire reason (older peer) falls back to 'peer'.
|
|
123
|
+
*/
|
|
124
|
+
export type A2aNoticeReason = 'send-failed' | 'stream-dead' | 'stop' | 'aborted' | 'peer' | 'delivered' | 'undelivered';
|
|
125
|
+
export interface A2aNoticeEnvelope {
|
|
126
|
+
kind: 'notice';
|
|
127
|
+
to: A2aWireOrigin;
|
|
128
|
+
conversation: string;
|
|
129
|
+
/** Bounded ≤ {@link A2A_NOTICE_SUMMARY_MAX}. */
|
|
130
|
+
summary: string;
|
|
131
|
+
messageId: string;
|
|
132
|
+
/** Who noticed — the hosting machine + host agent (additive v1). */
|
|
133
|
+
sender?: A2aReturnSender;
|
|
134
|
+
/** R2 (additive 0.1.7-h): the cause — rendered into the model-facing notice line; absent = pre-0.1.7-h peer, receiver falls back to 'peer'. */
|
|
135
|
+
reason?: A2aNoticeReason;
|
|
136
|
+
}
|
|
137
|
+
export type A2aEnvelope = A2aNewConversationEnvelope | A2aMessageEnvelope | A2aStopEnvelope | A2aReplyEnvelope | A2aNoticeEnvelope;
|
|
138
|
+
/** The v1 outer frame — the versioned unit that crosses the wire. */
|
|
139
|
+
export interface A2aWireFrame {
|
|
140
|
+
v: 1;
|
|
141
|
+
envelope: A2aEnvelope;
|
|
142
|
+
}
|
|
143
|
+
/** Receipt for new-conversation / message envelopes (inbox-acceptance). */
|
|
144
|
+
export interface A2aSendReceipt {
|
|
145
|
+
outcome: 'delivered' | 'failed';
|
|
146
|
+
messageId: string;
|
|
147
|
+
conversationId?: string;
|
|
148
|
+
/** Target session ('' when the failure predates session resolution). */
|
|
149
|
+
sessionId: string;
|
|
150
|
+
/** Present iff outcome === 'failed'. */
|
|
151
|
+
error?: WireError;
|
|
152
|
+
}
|
|
153
|
+
/** Receipt for stop envelopes; 'turn-only' reports the session-scope degradation honestly (no session-close RPC upstream). */
|
|
154
|
+
export interface A2aStopReceipt {
|
|
155
|
+
stopped: boolean;
|
|
156
|
+
scope: 'turn' | 'turn-only';
|
|
157
|
+
reason?: string;
|
|
158
|
+
}
|
|
159
|
+
export type A2aWireReceipt = A2aSendReceipt | A2aStopReceipt;
|
|
160
|
+
/**
|
|
161
|
+
* Typed address-resolution failure (D7): THROWN by resolvers and wire
|
|
162
|
+
* helpers so the WHY survives the boundary — consumers (P4 exec, P5
|
|
163
|
+
* injection) branch on `kind` instead of parsing message strings. The three
|
|
164
|
+
* kinds name the actual problem: a typo'd a6 on a known machine is not a
|
|
165
|
+
* pairing issue, and an unpaired machine is not an agent problem.
|
|
166
|
+
*
|
|
167
|
+
* Postoffice R1 note: the class lives here (the protocol layer) since the
|
|
168
|
+
* wire helpers classify transport failures with it; `a2a-service.js`
|
|
169
|
+
* re-exports it unchanged for its existing consumers.
|
|
170
|
+
*/
|
|
171
|
+
export declare class A2aAddressError extends Error {
|
|
172
|
+
/** Which failure this is. */
|
|
173
|
+
readonly kind: 'unknown-agent' | 'unknown-machine' | 'unreachable';
|
|
174
|
+
/** The address that failed to resolve. */
|
|
175
|
+
readonly address: A2aWireTarget;
|
|
176
|
+
/** The machine the blame lands on (m4; equals `address.machine`). */
|
|
177
|
+
readonly machine: string;
|
|
178
|
+
constructor(kind: 'unknown-agent' | 'unknown-machine' | 'unreachable', address: A2aWireTarget);
|
|
179
|
+
/** The honest branch text — one place, shared by every consumer of the error. */
|
|
180
|
+
static messageOf(kind: 'unknown-agent' | 'unknown-machine' | 'unreachable', address: A2aWireTarget): string;
|
|
181
|
+
}
|
|
182
|
+
/** The target address of any envelope (request `to` directly; return `to` folded agent→id). */
|
|
183
|
+
export declare function a2aAddressOf(envelope: A2aEnvelope): A2aWireTarget;
|
|
184
|
+
/** Strict v1 envelope guard: exact key sets, non-empty strings, bounded notice summaries. */
|
|
185
|
+
export declare function isA2aEnvelope(v: unknown): v is A2aEnvelope;
|
|
186
|
+
/** Strict v1 frame guard: `{v: 1, envelope}` with a valid envelope inside. */
|
|
187
|
+
export declare function isA2aWireFrame(v: unknown): v is A2aWireFrame;
|
|
188
|
+
/** Decode one unknown value as a frame; errors name the failure for the ingest side's 400s. */
|
|
189
|
+
export declare function decodeA2aWireFrame(v: unknown): {
|
|
190
|
+
ok: true;
|
|
191
|
+
envelope: A2aEnvelope;
|
|
192
|
+
} | {
|
|
193
|
+
ok: false;
|
|
194
|
+
error: string;
|
|
195
|
+
};
|
|
196
|
+
/** Wrap one envelope in the v1 outer frame. */
|
|
197
|
+
export declare function a2aWireFrameOf(envelope: A2aEnvelope): A2aWireFrame;
|
|
198
|
+
/** D17 title normalization: unspecified and empty become the placeholder; a given title stays verbatim. */
|
|
199
|
+
export declare function normalizeA2aTitle(title: string | undefined): string;
|
|
200
|
+
/** Wire-helper options: the cookie riding along, an injectable fetch, and a timeout budget. */
|
|
201
|
+
export interface A2aWireOptions {
|
|
202
|
+
/** The claiming/paired machine's request cookie (empty → header omitted, mesh-link discipline). */
|
|
203
|
+
cookie?: string;
|
|
204
|
+
fetchImpl?: typeof fetch;
|
|
205
|
+
timeoutMs?: number;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* POST one envelope to a hub's a2a ingest and decode the receipt.
|
|
209
|
+
* Transport failures (network error, non-2xx, 401 cookie rejection) map to
|
|
210
|
+
* {@link A2aAddressError} kind 'unreachable' with the cause attached; a 200
|
|
211
|
+
* whose body is not a v1 receipt shape is a loud protocol error (never a
|
|
212
|
+
* guessed receipt).
|
|
213
|
+
*/
|
|
214
|
+
export declare function postA2aEnvelope(origin: string, envelope: A2aEnvelope, opts?: A2aWireOptions): Promise<A2aWireReceipt>;
|
|
215
|
+
/**
|
|
216
|
+
* Claim (and clear, server-side) the envelopes buffered for THIS machine:
|
|
217
|
+
* `GET <origin>/api/superd/a2a/outbox?machine=<m4>` with the cookie riding
|
|
218
|
+
* along. Same failure taxonomy as {@link postA2aEnvelope}.
|
|
219
|
+
*/
|
|
220
|
+
export declare function fetchA2aOutbox(origin: string, machine: string, opts?: A2aWireOptions): Promise<A2aEnvelope[]>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/** The wire-protocol version this module speaks (and its guards accept). */
|
|
2
|
+
export const A2A_WIRE_VERSION = 1;
|
|
3
|
+
/** Registry/wire title placeholder when the operator specified none (D17). */
|
|
4
|
+
export const A2A_TITLE_PLACEHOLDER = '[undefined]';
|
|
5
|
+
/** The a2a ingest endpoint (fence + cookie gate ride the connection channel). */
|
|
6
|
+
export const A2A_ENDPOINT_PATH = '/api/superd/a2a';
|
|
7
|
+
/** The outbox claim endpoint — `?machine=<m4>` names the CLAIMING machine. */
|
|
8
|
+
export const A2A_OUTBOX_PATH = '/api/superd/a2a/outbox';
|
|
9
|
+
/** Bound for a notice summary — mirrors upstream CONTEXT_SUMMARY_MAX_CHARS (119 chars + '…' when bounded). */
|
|
10
|
+
export const A2A_NOTICE_SUMMARY_MAX = 120;
|
|
11
|
+
/** Wire call budget — matches mesh-link's FETCH_TIMEOUT_MS (10s). */
|
|
12
|
+
export const A2A_WIRE_TIMEOUT_MS = 10_000;
|
|
13
|
+
// ---------------------------------------------------------------- address error
|
|
14
|
+
/**
|
|
15
|
+
* Typed address-resolution failure (D7): THROWN by resolvers and wire
|
|
16
|
+
* helpers so the WHY survives the boundary — consumers (P4 exec, P5
|
|
17
|
+
* injection) branch on `kind` instead of parsing message strings. The three
|
|
18
|
+
* kinds name the actual problem: a typo'd a6 on a known machine is not a
|
|
19
|
+
* pairing issue, and an unpaired machine is not an agent problem.
|
|
20
|
+
*
|
|
21
|
+
* Postoffice R1 note: the class lives here (the protocol layer) since the
|
|
22
|
+
* wire helpers classify transport failures with it; `a2a-service.js`
|
|
23
|
+
* re-exports it unchanged for its existing consumers.
|
|
24
|
+
*/
|
|
25
|
+
export class A2aAddressError extends Error {
|
|
26
|
+
/** Which failure this is. */
|
|
27
|
+
kind;
|
|
28
|
+
/** The address that failed to resolve. */
|
|
29
|
+
address;
|
|
30
|
+
/** The machine the blame lands on (m4; equals `address.machine`). */
|
|
31
|
+
machine;
|
|
32
|
+
constructor(kind, address) {
|
|
33
|
+
super(A2aAddressError.messageOf(kind, address));
|
|
34
|
+
this.name = 'A2aAddressError';
|
|
35
|
+
this.kind = kind;
|
|
36
|
+
this.address = address;
|
|
37
|
+
this.machine = address.machine;
|
|
38
|
+
}
|
|
39
|
+
/** The honest branch text — one place, shared by every consumer of the error. */
|
|
40
|
+
static messageOf(kind, address) {
|
|
41
|
+
if (kind === 'unknown-agent') {
|
|
42
|
+
return `a2a: unknown-agent ${address.machine}-${address.id} (no such agent on machine ${address.machine})`;
|
|
43
|
+
}
|
|
44
|
+
if (kind === 'unknown-machine') {
|
|
45
|
+
// 0.1.7-h R3 (dev3 first-person 0.1.7-g §6): a malformed address such as
|
|
46
|
+
// "no-such-agent-zzz" parses machine="no" — an error naming only "no"
|
|
47
|
+
// sends the model hunting for a machine it never typed. Name the
|
|
48
|
+
// fragment AND the full address; split-at-first-dash concat is the raw
|
|
49
|
+
// input, so this reconstruction is exact.
|
|
50
|
+
return `a2a: unknown-machine "${address.machine}" — address "${address.machine}-${address.id}" is not a paired machine (addresses are "<m4>-<a6>")`;
|
|
51
|
+
}
|
|
52
|
+
return `a2a: no gateway for ${address.machine}-${address.id} — machine unreachable or not paired`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The target address of any envelope (request `to` directly; return `to` folded agent→id). */
|
|
56
|
+
export function a2aAddressOf(envelope) {
|
|
57
|
+
const to = envelope.to;
|
|
58
|
+
return 'id' in to ? { machine: to.machine, id: to.id } : { machine: to.machine, id: to.agent };
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------- guards
|
|
61
|
+
function isRecord(v) {
|
|
62
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
63
|
+
}
|
|
64
|
+
function nonEmptyString(v) {
|
|
65
|
+
return typeof v === 'string' && v !== '';
|
|
66
|
+
}
|
|
67
|
+
/** R2 (0.1.7-i): the wire notice reason vocabulary — mirror of the type union. */
|
|
68
|
+
const NOTICE_REASONS = new Set(['send-failed', 'stream-dead', 'stop', 'aborted', 'peer', 'delivered', 'undelivered']);
|
|
69
|
+
function isNoticeReason(v) {
|
|
70
|
+
return typeof v === 'string' && NOTICE_REASONS.has(v);
|
|
71
|
+
}
|
|
72
|
+
function hasExactKeys(v, keys, optional = []) {
|
|
73
|
+
const expected = new Set([...keys, ...optional]);
|
|
74
|
+
for (const key of Object.keys(v)) {
|
|
75
|
+
if (!expected.has(key))
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
if (!(key in v))
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function isTarget(v) {
|
|
85
|
+
return isRecord(v) && hasExactKeys(v, ['machine', 'id']) && nonEmptyString(v.machine) && nonEmptyString(v.id);
|
|
86
|
+
}
|
|
87
|
+
function isReturnSender(v) {
|
|
88
|
+
return isRecord(v) && hasExactKeys(v, ['machine', 'agent']) && nonEmptyString(v.machine) && nonEmptyString(v.agent);
|
|
89
|
+
}
|
|
90
|
+
function isOrigin(v) {
|
|
91
|
+
return isRecord(v) && hasExactKeys(v, ['machine', 'agent', 'sessionId']) &&
|
|
92
|
+
nonEmptyString(v.machine) && nonEmptyString(v.agent) && nonEmptyString(v.sessionId);
|
|
93
|
+
}
|
|
94
|
+
function isDelivery(v) {
|
|
95
|
+
return v === undefined || v === 'queue' || v === 'steer';
|
|
96
|
+
}
|
|
97
|
+
/** Strict v1 envelope guard: exact key sets, non-empty strings, bounded notice summaries. */
|
|
98
|
+
export function isA2aEnvelope(v) {
|
|
99
|
+
if (!isRecord(v) || typeof v['kind'] !== 'string')
|
|
100
|
+
return false;
|
|
101
|
+
switch (v['kind']) {
|
|
102
|
+
case 'new-conversation':
|
|
103
|
+
return hasExactKeys(v, ['kind', 'to', 'conversationId', 'title', 'message', 'from', 'messageId'], ['delivery']) &&
|
|
104
|
+
isTarget(v['to']) && nonEmptyString(v['conversationId']) && nonEmptyString(v['title']) &&
|
|
105
|
+
nonEmptyString(v['message']) && isOrigin(v['from']) && nonEmptyString(v['messageId']) && isDelivery(v['delivery']);
|
|
106
|
+
case 'message':
|
|
107
|
+
return hasExactKeys(v, ['kind', 'to', 'conversationId', 'message', 'from', 'messageId'], ['delivery']) &&
|
|
108
|
+
isTarget(v['to']) && nonEmptyString(v['conversationId']) && nonEmptyString(v['message']) &&
|
|
109
|
+
isOrigin(v['from']) && nonEmptyString(v['messageId']) && isDelivery(v['delivery']);
|
|
110
|
+
case 'stop':
|
|
111
|
+
return hasExactKeys(v, ['kind', 'to', 'conversationId', 'scope']) &&
|
|
112
|
+
isTarget(v['to']) && nonEmptyString(v['conversationId']) && (v['scope'] === 'turn' || v['scope'] === 'session');
|
|
113
|
+
case 'reply':
|
|
114
|
+
return hasExactKeys(v, ['kind', 'to', 'conversation', 'text', 'messageId'], ['sender', 'transcribed']) &&
|
|
115
|
+
isOrigin(v['to']) && nonEmptyString(v['conversation']) && nonEmptyString(v['text']) && nonEmptyString(v['messageId']) &&
|
|
116
|
+
(isReturnSender(v['sender']) || v['sender'] === undefined) &&
|
|
117
|
+
(v['transcribed'] === undefined || v['transcribed'] === true);
|
|
118
|
+
case 'notice':
|
|
119
|
+
// 0.1.7-i R2: `reason` rides additively (the send-failed notice made it
|
|
120
|
+
// load-bearing) — older peers omit it, the guard validates the vocabulary.
|
|
121
|
+
return hasExactKeys(v, ['kind', 'to', 'conversation', 'summary', 'messageId'], ['sender', 'reason']) &&
|
|
122
|
+
isOrigin(v['to']) && nonEmptyString(v['conversation']) && nonEmptyString(v['summary']) &&
|
|
123
|
+
typeof v['summary'] === 'string' && v['summary'].length <= A2A_NOTICE_SUMMARY_MAX && nonEmptyString(v['messageId']) &&
|
|
124
|
+
(isReturnSender(v['sender']) || v['sender'] === undefined) &&
|
|
125
|
+
(isNoticeReason(v['reason']) || v['reason'] === undefined);
|
|
126
|
+
default:
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Strict v1 frame guard: `{v: 1, envelope}` with a valid envelope inside. */
|
|
131
|
+
export function isA2aWireFrame(v) {
|
|
132
|
+
return isRecord(v) && v['v'] === A2A_WIRE_VERSION && isA2aEnvelope(v['envelope']);
|
|
133
|
+
}
|
|
134
|
+
/** Decode one unknown value as a frame; errors name the failure for the ingest side's 400s. */
|
|
135
|
+
export function decodeA2aWireFrame(v) {
|
|
136
|
+
if (!isRecord(v))
|
|
137
|
+
return { ok: false, error: 'a2a-wire: frame must be a JSON object' };
|
|
138
|
+
if (v['v'] !== A2A_WIRE_VERSION) {
|
|
139
|
+
return { ok: false, error: `a2a-wire: unsupported frame version ${JSON.stringify(v['v'])} (protocol v${A2A_WIRE_VERSION})` };
|
|
140
|
+
}
|
|
141
|
+
if (!isA2aEnvelope(v['envelope'])) {
|
|
142
|
+
return { ok: false, error: `a2a-wire: malformed v${A2A_WIRE_VERSION} envelope (strict shape check failed)` };
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, envelope: v['envelope'] };
|
|
145
|
+
}
|
|
146
|
+
/** Wrap one envelope in the v1 outer frame. */
|
|
147
|
+
export function a2aWireFrameOf(envelope) {
|
|
148
|
+
return { v: A2A_WIRE_VERSION, envelope };
|
|
149
|
+
}
|
|
150
|
+
/** D17 title normalization: unspecified and empty become the placeholder; a given title stays verbatim. */
|
|
151
|
+
export function normalizeA2aTitle(title) {
|
|
152
|
+
return title === undefined || title === '' ? A2A_TITLE_PLACEHOLDER : title;
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------- receipts (guards)
|
|
155
|
+
/**
|
|
156
|
+
* D28 rollout tolerance (0.1.8-b): the pre-0.1.8 packed `injected` names the
|
|
157
|
+
* SAME fact as `delivered`. A mesh always has mixed-version windows during a
|
|
158
|
+
* rollout, so an unknown-but-legible outcome must DEGRADE, never throw. The
|
|
159
|
+
* guard accepts both; the boundary normalizes the legacy word.
|
|
160
|
+
*/
|
|
161
|
+
const LEGACY_RECEIPT_OUTCOMES = new Set(['injected']);
|
|
162
|
+
function isReceiptOutcome(v) {
|
|
163
|
+
return v === 'delivered' || v === 'failed' || (typeof v === 'string' && LEGACY_RECEIPT_OUTCOMES.has(v));
|
|
164
|
+
}
|
|
165
|
+
/** Rewrite a legacy receipt outcome in place (the parsed body is ours after the guard). */
|
|
166
|
+
function normalizeReceiptOutcome(body) {
|
|
167
|
+
const raw = body.outcome;
|
|
168
|
+
if (typeof raw === 'string' && LEGACY_RECEIPT_OUTCOMES.has(raw))
|
|
169
|
+
body.outcome = 'delivered';
|
|
170
|
+
}
|
|
171
|
+
function isWireError(v) {
|
|
172
|
+
return isRecord(v) && nonEmptyString(v['code']) && nonEmptyString(v['message']);
|
|
173
|
+
}
|
|
174
|
+
function isSendReceipt(v) {
|
|
175
|
+
if (!isRecord(v))
|
|
176
|
+
return false;
|
|
177
|
+
if (!hasExactKeys(v, ['outcome', 'messageId', 'sessionId'], ['conversationId', 'error']))
|
|
178
|
+
return false;
|
|
179
|
+
if (!isReceiptOutcome(v['outcome']))
|
|
180
|
+
return false;
|
|
181
|
+
if (!nonEmptyString(v['messageId']))
|
|
182
|
+
return false;
|
|
183
|
+
if (typeof v['sessionId'] !== 'string')
|
|
184
|
+
return false;
|
|
185
|
+
if ('conversationId' in v && !nonEmptyString(v['conversationId']))
|
|
186
|
+
return false;
|
|
187
|
+
if ('error' in v && !isWireError(v['error']))
|
|
188
|
+
return false;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function isStopReceipt(v) {
|
|
192
|
+
if (!isRecord(v))
|
|
193
|
+
return false;
|
|
194
|
+
if (!hasExactKeys(v, ['stopped', 'scope'], ['reason']))
|
|
195
|
+
return false;
|
|
196
|
+
if (typeof v['stopped'] !== 'boolean')
|
|
197
|
+
return false;
|
|
198
|
+
if (v['scope'] !== 'turn' && v['scope'] !== 'turn-only')
|
|
199
|
+
return false;
|
|
200
|
+
if ('reason' in v && !nonEmptyString(v['reason']))
|
|
201
|
+
return false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function errMsg(err) {
|
|
205
|
+
return err instanceof Error ? err.message : String(err);
|
|
206
|
+
}
|
|
207
|
+
/** Throw one classified unreachable failure with the underlying cause attached (the canonical message stays the classification's). */
|
|
208
|
+
function throwUnreachable(envelope, cause) {
|
|
209
|
+
const error = new A2aAddressError('unreachable', a2aAddressOf(envelope));
|
|
210
|
+
error.cause = cause;
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* POST one envelope to a hub's a2a ingest and decode the receipt.
|
|
215
|
+
* Transport failures (network error, non-2xx, 401 cookie rejection) map to
|
|
216
|
+
* {@link A2aAddressError} kind 'unreachable' with the cause attached; a 200
|
|
217
|
+
* whose body is not a v1 receipt shape is a loud protocol error (never a
|
|
218
|
+
* guessed receipt).
|
|
219
|
+
*/
|
|
220
|
+
export async function postA2aEnvelope(origin, envelope, opts = {}) {
|
|
221
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
222
|
+
const cookie = opts.cookie ?? '';
|
|
223
|
+
let res;
|
|
224
|
+
try {
|
|
225
|
+
res = await fetchImpl(`${origin}${A2A_ENDPOINT_PATH}`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'content-type': 'application/json', ...(cookie === '' ? {} : { cookie }) },
|
|
228
|
+
body: JSON.stringify(a2aWireFrameOf(envelope)),
|
|
229
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? A2A_WIRE_TIMEOUT_MS),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
throwUnreachable(envelope, err);
|
|
234
|
+
}
|
|
235
|
+
if (res.status !== 200) {
|
|
236
|
+
throwUnreachable(envelope, `ingest POST answered status ${res.status}${res.status === 401 ? ' (cookie rejected)' : ''}`);
|
|
237
|
+
}
|
|
238
|
+
let body;
|
|
239
|
+
try {
|
|
240
|
+
body = await res.json();
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
throw new Error(`a2a-wire: ingest receipt from ${origin} is not JSON — ${errMsg(err)}`);
|
|
244
|
+
}
|
|
245
|
+
if (isSendReceipt(body)) {
|
|
246
|
+
normalizeReceiptOutcome(body);
|
|
247
|
+
return body;
|
|
248
|
+
}
|
|
249
|
+
if (isStopReceipt(body))
|
|
250
|
+
return body;
|
|
251
|
+
throw new Error(`a2a-wire: ingest receipt from ${origin} is not a v1 receipt shape — ${JSON.stringify(body)}`);
|
|
252
|
+
}
|
|
253
|
+
/** The outbox claim body: `{v: 1, envelopes}` — envelopes validated on arrival, fail-loud on any malformed entry. */
|
|
254
|
+
function decodeOutboxBody(v, origin) {
|
|
255
|
+
if (!isRecord(v) || v['v'] !== A2A_WIRE_VERSION || !Array.isArray(v['envelopes'])) {
|
|
256
|
+
throw new Error(`a2a-wire: outbox body from ${origin} is not {v: 1, envelopes: []} — ${JSON.stringify(v)}`);
|
|
257
|
+
}
|
|
258
|
+
const envelopes = [];
|
|
259
|
+
for (const [index, entry] of v['envelopes'].entries()) {
|
|
260
|
+
if (!isA2aEnvelope(entry)) {
|
|
261
|
+
throw new Error(`a2a-wire: outbox entry #${index} from ${origin} is not a v1 envelope — ${JSON.stringify(entry)}`);
|
|
262
|
+
}
|
|
263
|
+
envelopes.push(entry);
|
|
264
|
+
}
|
|
265
|
+
return envelopes;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Claim (and clear, server-side) the envelopes buffered for THIS machine:
|
|
269
|
+
* `GET <origin>/api/superd/a2a/outbox?machine=<m4>` with the cookie riding
|
|
270
|
+
* along. Same failure taxonomy as {@link postA2aEnvelope}.
|
|
271
|
+
*/
|
|
272
|
+
export async function fetchA2aOutbox(origin, machine, opts = {}) {
|
|
273
|
+
if (!nonEmptyString(machine)) {
|
|
274
|
+
throw new Error('a2a-wire: outbox claim requires a non-empty machine id');
|
|
275
|
+
}
|
|
276
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
277
|
+
const cookie = opts.cookie ?? '';
|
|
278
|
+
let res;
|
|
279
|
+
try {
|
|
280
|
+
res = await fetchImpl(`${origin}${A2A_OUTBOX_PATH}?machine=${encodeURIComponent(machine)}`, {
|
|
281
|
+
method: 'GET',
|
|
282
|
+
headers: cookie === '' ? {} : { cookie },
|
|
283
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? A2A_WIRE_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const error = new A2aAddressError('unreachable', { machine, id: '' });
|
|
288
|
+
error.cause = err;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
if (res.status !== 200) {
|
|
292
|
+
const error = new A2aAddressError('unreachable', { machine, id: '' });
|
|
293
|
+
error.cause =
|
|
294
|
+
`outbox claim answered status ${res.status}${res.status === 401 ? ' (cookie rejected)' : ''}`;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
let body;
|
|
298
|
+
try {
|
|
299
|
+
body = await res.json();
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
throw new Error(`a2a-wire: outbox body from ${origin} is not JSON — ${errMsg(err)}`);
|
|
303
|
+
}
|
|
304
|
+
return decodeOutboxBody(body, origin);
|
|
305
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The persisted map: roster key (`native` for ctx0, else the adapter key) → a6 id. */
|
|
2
|
+
export type AgentIdMap = Record<string, string>;
|
|
3
|
+
/** Read the id map; a missing or unreadable/corrupt file degrades to `{}` (never throws). */
|
|
4
|
+
export declare function agentIdMap(home: string): AgentIdMap;
|
|
5
|
+
/**
|
|
6
|
+
* Lazily mint + persist one agent's a6 id, deduped against every id already in
|
|
7
|
+
* the map; an already-minted label is returned unchanged (idempotent). The
|
|
8
|
+
* `native` label is minted like any other.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureAgentId(home: string, label: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent short ids (mesh P1, spec §3.2): one stable a6 id per roster row,
|
|
3
|
+
* owner-minted and persisted in `<home>/superd/agent-ids.json`.
|
|
4
|
+
*
|
|
5
|
+
* Pure module — no cordis dependencies. Same persistence discipline as
|
|
6
|
+
* machines.ts (0600, atomic tmp+rename writes, no in-process cache): every
|
|
7
|
+
* mint is a read-modify-write of the whole map, deduped against every id
|
|
8
|
+
* already in the file. A missing or unreadable/corrupt file reads as `{}`
|
|
9
|
+
* (never throws), so a corrupted map simply re-mints on next need. The mint
|
|
10
|
+
* itself is machines.ts's `mintShortId` (shared alphabet + collision re-roll);
|
|
11
|
+
* this module only adds the a6 length and its own store.
|
|
12
|
+
*/
|
|
13
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { mintShortId } from './machines.js';
|
|
16
|
+
function agentIdsPath(home) {
|
|
17
|
+
return join(home, 'superd', 'agent-ids.json');
|
|
18
|
+
}
|
|
19
|
+
/** Read the id map; a missing or unreadable/corrupt file degrades to `{}` (never throws). */
|
|
20
|
+
export function agentIdMap(home) {
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = readFileSync(agentIdsPath(home), 'utf8');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
36
|
+
return {};
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [key, id] of Object.entries(parsed)) {
|
|
39
|
+
if (key !== '' && typeof id === 'string' && id !== '')
|
|
40
|
+
out[key] = id;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Persist the whole map: recursive mkdir, tmp write, chmod 0600, atomic rename. */
|
|
45
|
+
function saveAgentIds(home, map) {
|
|
46
|
+
const dir = join(home, 'superd');
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
const target = agentIdsPath(home);
|
|
49
|
+
const tmp = `${target}.tmp`;
|
|
50
|
+
writeFileSync(tmp, JSON.stringify(map, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
51
|
+
chmodSync(tmp, 0o600);
|
|
52
|
+
renameSync(tmp, target);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lazily mint + persist one agent's a6 id, deduped against every id already in
|
|
56
|
+
* the map; an already-minted label is returned unchanged (idempotent). The
|
|
57
|
+
* `native` label is minted like any other.
|
|
58
|
+
*/
|
|
59
|
+
export function ensureAgentId(home, label) {
|
|
60
|
+
const map = agentIdMap(home);
|
|
61
|
+
const existing = map[label];
|
|
62
|
+
if (existing !== undefined)
|
|
63
|
+
return existing;
|
|
64
|
+
const id = mintShortId(6, Object.values(map));
|
|
65
|
+
map[label] = id;
|
|
66
|
+
saveAgentIds(home, map);
|
|
67
|
+
return id;
|
|
68
|
+
}
|