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,250 @@
|
|
|
1
|
+
import { ensureSelf, listRemotes, removeRemote } from './machines.js';
|
|
2
|
+
import { machineStatus, pairMachine, reprobeMachine, emitMachineRemoved } from './mesh-link.js';
|
|
3
|
+
import { A2A_ENDPOINT_PATH, A2A_OUTBOX_PATH, A2A_WIRE_VERSION, decodeA2aWireFrame } from './a2a-wire.js';
|
|
4
|
+
import { EXEC_API_BASE, parseOneShotExecBody, runOneShotExec } from './exec.js';
|
|
5
|
+
/** The shared-channel Fetch route path this API owns. */
|
|
6
|
+
export const MESH_API_BASE = '/api/superd/machines';
|
|
7
|
+
function json(status, body) {
|
|
8
|
+
return new Response(JSON.stringify(body), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { 'content-type': 'application/json' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Project one persisted row to its public wire shape (allowlist — never
|
|
14
|
+
* token, never cookie) and join the mesh-link status entry by id. */
|
|
15
|
+
function publicRow(row) {
|
|
16
|
+
return projectRow(row, machineStatus());
|
|
17
|
+
}
|
|
18
|
+
function projectRow(row, status) {
|
|
19
|
+
const entry = status.get(row.id);
|
|
20
|
+
if (entry === undefined)
|
|
21
|
+
return { id: row.id, name: row.name, label: row.label, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: 'pending' };
|
|
22
|
+
const out = { id: row.id, name: row.name, label: row.label, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: entry.status, lastProbeAt: entry.lastProbeAt };
|
|
23
|
+
if (entry.error !== undefined)
|
|
24
|
+
out.error = entry.error;
|
|
25
|
+
if (entry.versionWarning !== undefined)
|
|
26
|
+
out.versionWarning = entry.versionWarning;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function errMsg(err) {
|
|
30
|
+
return err instanceof Error ? err.message : String(err);
|
|
31
|
+
}
|
|
32
|
+
async function readJsonBody(request) {
|
|
33
|
+
let body;
|
|
34
|
+
try {
|
|
35
|
+
body = await request.json();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new Error(`mesh-api: request body is not valid JSON — ${errMsg(err)}`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
41
|
+
throw new Error('mesh-api: request body must be a JSON object');
|
|
42
|
+
}
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The mesh-machines handler: GET list / POST pair-or-reprobe / DELETE remove.
|
|
47
|
+
* @param request - the incoming `/api/superd/machines` request.
|
|
48
|
+
* @param deps - home + optional fetch override (tests inject a fixture).
|
|
49
|
+
* @returns the JSON response.
|
|
50
|
+
*/
|
|
51
|
+
export async function handleMeshMachines(request, deps) {
|
|
52
|
+
const method = request.method;
|
|
53
|
+
if (method === 'GET')
|
|
54
|
+
return handleList(deps);
|
|
55
|
+
if (method === 'POST')
|
|
56
|
+
return handlePost(request, deps);
|
|
57
|
+
if (method === 'DELETE')
|
|
58
|
+
return handleRemove(request, deps);
|
|
59
|
+
return json(405, { error: `mesh-api: method ${method} is not supported on ${MESH_API_BASE}` });
|
|
60
|
+
}
|
|
61
|
+
/** GET: self (lazily minted on first need, spec §3.1) + every remote row in
|
|
62
|
+
* its public projection, status joined from the mesh-link table. */
|
|
63
|
+
function handleList(deps) {
|
|
64
|
+
const self = ensureSelf(deps.home, 'local');
|
|
65
|
+
const status = machineStatus();
|
|
66
|
+
const machines = listRemotes(deps.home).map((row) => projectRow(row, status));
|
|
67
|
+
return json(200, { self, machines });
|
|
68
|
+
}
|
|
69
|
+
/** POST: body-dispatched — `{op: 'reprobe', id}` re-probes, anything else is
|
|
70
|
+
* a pairing input `{name, label?, tokenUrl}`. */
|
|
71
|
+
async function handlePost(request, deps) {
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = await readJsonBody(request);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return json(400, { error: errMsg(err) });
|
|
78
|
+
}
|
|
79
|
+
if (typeof body.op === 'string' && body.op !== '') {
|
|
80
|
+
if (body.op !== 'reprobe')
|
|
81
|
+
return json(400, { error: `mesh-api: unknown op "${body.op}"` });
|
|
82
|
+
const id = typeof body.id === 'string' && body.id !== '' ? body.id : '';
|
|
83
|
+
if (id === '')
|
|
84
|
+
return json(400, { error: 'mesh-api: reprobe body must be {op: "reprobe", id} with a non-empty id' });
|
|
85
|
+
let probe;
|
|
86
|
+
try {
|
|
87
|
+
probe = await reprobeMachine(deps.home, id, { fetchImpl: deps.fetchImpl });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return json(400, { error: errMsg(err) });
|
|
91
|
+
}
|
|
92
|
+
// A settled probe outcome (even unreachable/auth-expired) is an answer,
|
|
93
|
+
// not an operation failure — the status table records it either way.
|
|
94
|
+
return json(200, { probe });
|
|
95
|
+
}
|
|
96
|
+
const name = typeof body.name === 'string' && body.name !== '' ? body.name : '';
|
|
97
|
+
const tokenUrl = typeof body.tokenUrl === 'string' && body.tokenUrl !== '' ? body.tokenUrl : '';
|
|
98
|
+
if (name === '' || tokenUrl === '') {
|
|
99
|
+
return json(400, { error: 'mesh-api: pair body must be {name, label?, tokenUrl} with non-empty name and tokenUrl' });
|
|
100
|
+
}
|
|
101
|
+
const label = typeof body.label === 'string' && body.label !== '' ? body.label : undefined;
|
|
102
|
+
let outcome;
|
|
103
|
+
try {
|
|
104
|
+
outcome = await pairMachine(deps.home, { name, label, tokenUrl }, { fetchImpl: deps.fetchImpl });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// mesh-link errors are actionable and token-redacted (Task 2 fix round 1).
|
|
108
|
+
return json(400, { error: errMsg(err) });
|
|
109
|
+
}
|
|
110
|
+
return json(201, { machine: publicRow(outcome.row) });
|
|
111
|
+
}
|
|
112
|
+
/** DELETE: drop one remote row by `?id=`; `{removed: false}` for unknown ids. */
|
|
113
|
+
function handleRemove(request, deps) {
|
|
114
|
+
const id = new URL(request.url).searchParams.get('id') ?? '';
|
|
115
|
+
if (id === '')
|
|
116
|
+
return json(400, { error: `mesh-api: DELETE on ${MESH_API_BASE} requires a non-empty ?id= query parameter` });
|
|
117
|
+
const removed = removeRemote(deps.home, id);
|
|
118
|
+
if (removed)
|
|
119
|
+
emitMachineRemoved(id); // federation bus: the registrar drops every row of the machine
|
|
120
|
+
return json(200, { removed });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Register the mesh-machines API on the shared `/api` connection channel
|
|
124
|
+
* (behind the standard browser-trust + auth fence). No-op when the
|
|
125
|
+
* `connection` service is absent (e.g. fake-ctx unit tests).
|
|
126
|
+
*/
|
|
127
|
+
export function registerMeshRpc(ctx, deps) {
|
|
128
|
+
ctx.inject(['connection'], (connCtx) => {
|
|
129
|
+
const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
|
|
130
|
+
if (fetchFace === undefined)
|
|
131
|
+
return; // connection not actually present (test ctx)
|
|
132
|
+
connCtx.effect(() => fetchFace.register({
|
|
133
|
+
path: MESH_API_BASE,
|
|
134
|
+
methods: [...ROUTE_METHODS],
|
|
135
|
+
requestBody: 'buffered',
|
|
136
|
+
fetch: (request) => handleMeshMachines(request, deps),
|
|
137
|
+
}), 'superd-hub: /api/superd/machines route');
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Every method our exact paths claim on the shared `/api` channel — unknown
|
|
141
|
+
* methods land in the HANDLER's honest 405 (a PUT on a known path is "method
|
|
142
|
+
* not supported", not a fall-through host 404; 2026-09-29 field finding:
|
|
143
|
+
* PUT /api/superd/machines answered 404 because the registration only
|
|
144
|
+
* claimed GET/POST/DELETE). */
|
|
145
|
+
const ROUTE_METHODS = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'HEAD'];
|
|
146
|
+
// ---------------------------------------------------------------- a2a (postoffice R3)
|
|
147
|
+
// (A2aRpcDeps retired in R4 — registerA2aRpc takes the SHARED ingest instance)
|
|
148
|
+
/**
|
|
149
|
+
* POST /api/superd/a2a — body = one v1 wire frame `{v: 1, envelope}` → the
|
|
150
|
+
* envelope's receipt (new/message: send receipt; stop: stop receipt;
|
|
151
|
+
* reply/notice: the local delivery outcome). Malformed frames are 400s with
|
|
152
|
+
* the decoder's named reason.
|
|
153
|
+
*/
|
|
154
|
+
export async function handleA2aIngestPost(request, ingest) {
|
|
155
|
+
if (request.method !== 'POST') {
|
|
156
|
+
return json(405, { error: `mesh-api: method ${request.method} is not supported on ${A2A_ENDPOINT_PATH} (use POST)` });
|
|
157
|
+
}
|
|
158
|
+
let body;
|
|
159
|
+
try {
|
|
160
|
+
body = await request.json();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return json(400, { error: `mesh-api: a2a body is not valid JSON — ${err instanceof Error ? err.message : String(err)}` });
|
|
164
|
+
}
|
|
165
|
+
const decoded = decodeA2aWireFrame(body);
|
|
166
|
+
if (!decoded.ok)
|
|
167
|
+
return json(400, { error: decoded.error });
|
|
168
|
+
const receipt = await ingest.ingest(decoded.envelope);
|
|
169
|
+
return json(200, receipt);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* GET /api/superd/a2a/outbox?machine=<m4> — claim & clear the buffered
|
|
173
|
+
* envelopes addressed to the claiming machine (declarative claim, D1).
|
|
174
|
+
*/
|
|
175
|
+
export function handleA2aOutboxGet(request, ingest) {
|
|
176
|
+
if (request.method !== 'GET') {
|
|
177
|
+
return json(405, { error: `mesh-api: method ${request.method} is not supported on ${A2A_OUTBOX_PATH} (use GET)` });
|
|
178
|
+
}
|
|
179
|
+
const machine = new URL(request.url).searchParams.get('machine') ?? '';
|
|
180
|
+
if (machine === '') {
|
|
181
|
+
return json(400, { error: 'mesh-api: a2a outbox claim requires a non-empty ?machine= query parameter' });
|
|
182
|
+
}
|
|
183
|
+
return json(200, { v: A2A_WIRE_VERSION, envelopes: ingest.claimOutbox(machine) });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Register the a2a endpoints (POST /api/superd/a2a + GET .../outbox) on the
|
|
187
|
+
* shared `/api` connection channel — same fence as the machines route. The
|
|
188
|
+
* ingest instance is SHARED (R4): the initiator service's self-routing leg
|
|
189
|
+
* calls the very same handler these routes call. No-op when `connection` is
|
|
190
|
+
* absent.
|
|
191
|
+
*/
|
|
192
|
+
export function registerA2aRpc(ctx, ingest) {
|
|
193
|
+
ctx.inject(['connection'], (connCtx) => {
|
|
194
|
+
const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
|
|
195
|
+
if (fetchFace === undefined)
|
|
196
|
+
return; // connection not actually present (test ctx)
|
|
197
|
+
// (the ingest instance is injected — one instance shared with the service)
|
|
198
|
+
connCtx.effect(() => fetchFace.register({
|
|
199
|
+
path: A2A_ENDPOINT_PATH,
|
|
200
|
+
methods: [...ROUTE_METHODS],
|
|
201
|
+
requestBody: 'buffered',
|
|
202
|
+
fetch: (request) => handleA2aIngestPost(request, ingest),
|
|
203
|
+
}), 'superd-hub: /api/superd/a2a route');
|
|
204
|
+
connCtx.effect(() => fetchFace.register({
|
|
205
|
+
path: A2A_OUTBOX_PATH,
|
|
206
|
+
methods: [...ROUTE_METHODS],
|
|
207
|
+
requestBody: 'buffered',
|
|
208
|
+
fetch: (request) => Promise.resolve(handleA2aOutboxGet(request, ingest)),
|
|
209
|
+
}), 'superd-hub: /api/superd/a2a/outbox route');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------- exec (P4, spec §8.1)
|
|
213
|
+
/**
|
|
214
|
+
* POST /api/superd/exec — one-shot `bash -lc <cmd>` on THIS machine (spec
|
|
215
|
+
* §8.1): tail-windowed stdout/stderr, two-stage group kill on timeout. The
|
|
216
|
+
* auth trust boundary is the shared `/api` fence (same as machines/a2a);
|
|
217
|
+
* the initiator-side per-machine knob (`execEnabled`) lives on the CALLER's
|
|
218
|
+
* registry row, not here.
|
|
219
|
+
*/
|
|
220
|
+
export async function handleExecPost(request, log) {
|
|
221
|
+
if (request.method !== 'POST') {
|
|
222
|
+
return json(405, { error: `mesh-api: method ${request.method} is not supported on ${EXEC_API_BASE} (use POST)` });
|
|
223
|
+
}
|
|
224
|
+
let body;
|
|
225
|
+
try {
|
|
226
|
+
body = await request.json();
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
return json(400, { error: `mesh-api: exec body is not valid JSON — ${err instanceof Error ? err.message : String(err)}` });
|
|
230
|
+
}
|
|
231
|
+
const parsed = parseOneShotExecBody(body);
|
|
232
|
+
if (!parsed.ok)
|
|
233
|
+
return json(400, { error: `mesh-api: ${parsed.error}` });
|
|
234
|
+
const result = await runOneShotExec(parsed.value, log);
|
|
235
|
+
return json(200, result);
|
|
236
|
+
}
|
|
237
|
+
/** Register the exec endpoint on the shared `/api` connection channel. */
|
|
238
|
+
export function registerExecRpc(ctx, log) {
|
|
239
|
+
ctx.inject(['connection'], (connCtx) => {
|
|
240
|
+
const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
|
|
241
|
+
if (fetchFace === undefined)
|
|
242
|
+
return; // connection not actually present (test ctx)
|
|
243
|
+
connCtx.effect(() => fetchFace.register({
|
|
244
|
+
path: EXEC_API_BASE,
|
|
245
|
+
methods: [...ROUTE_METHODS],
|
|
246
|
+
requestBody: 'buffered',
|
|
247
|
+
fetch: (request) => handleExecPost(request, log),
|
|
248
|
+
}), 'superd-hub: /api/superd/exec route');
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Every client→server stream frame (control replies like `pong` excluded). */
|
|
2
|
+
export type MuxClientFrame = {
|
|
3
|
+
type: 'open';
|
|
4
|
+
streamId: number;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
payload: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'item';
|
|
9
|
+
streamId: number;
|
|
10
|
+
value?: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'end';
|
|
13
|
+
streamId: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'cancel';
|
|
16
|
+
streamId: number;
|
|
17
|
+
};
|
|
18
|
+
/** The injected text-socket seam (Task 2 adapts `ws` to this shape). */
|
|
19
|
+
export interface MuxClientSocket {
|
|
20
|
+
send(data: string): void;
|
|
21
|
+
onMessage(cb: (data: string) => void): void;
|
|
22
|
+
onClose(cb: () => void): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
/** The frozen openStream handle (Task 2 builds on this exact shape). */
|
|
26
|
+
export interface MuxStreamHandle {
|
|
27
|
+
/** Server items until `end`/`error`/socket close (see the header rulings). */
|
|
28
|
+
items: AsyncIterable<unknown>;
|
|
29
|
+
/** Uplink channel: one `next(value)` read = one item frame, zero buffering. */
|
|
30
|
+
upstream: AsyncIterable<unknown>;
|
|
31
|
+
/** Uplink half-close: sends `{type:'end'}` once; downlink stays live. */
|
|
32
|
+
end(): void;
|
|
33
|
+
/** Cancel the logical stream: one `{type:'cancel'}` frame, local retirement. */
|
|
34
|
+
cancel(): void;
|
|
35
|
+
}
|
|
36
|
+
/** Options. */
|
|
37
|
+
export interface MuxClientOptions {
|
|
38
|
+
/** Server ping cadence used for the silence window (2 × this); upstream default 2s. */
|
|
39
|
+
heartbeatIntervalMs?: number;
|
|
40
|
+
}
|
|
41
|
+
/** A stream/transport failure carrying wire failure fields (or a local cut code). */
|
|
42
|
+
export declare class MuxClientError extends Error {
|
|
43
|
+
readonly code: string;
|
|
44
|
+
readonly details: object;
|
|
45
|
+
constructor(code: string, message: string, details?: object);
|
|
46
|
+
}
|
|
47
|
+
/** The mux client half: pure protocol, socket injected. */
|
|
48
|
+
export declare class MuxClient {
|
|
49
|
+
private readonly socket;
|
|
50
|
+
private readonly heartbeatIntervalMs;
|
|
51
|
+
private nextId;
|
|
52
|
+
private readonly streams;
|
|
53
|
+
private state;
|
|
54
|
+
private watchdog;
|
|
55
|
+
constructor(socket: MuxClientSocket, options?: MuxClientOptions);
|
|
56
|
+
/** Open one logical stream; the handle's lanes are documented in the header. */
|
|
57
|
+
openStream(endpoint: string, payload: unknown): MuxStreamHandle;
|
|
58
|
+
/** Close every stream and the socket. Idempotent; no cancel frames (the server unwinds on socket close). */
|
|
59
|
+
dispose(): void;
|
|
60
|
+
private endUplink;
|
|
61
|
+
private retire;
|
|
62
|
+
/** Serialize one frame; the wire id is the stringified numeric counter. */
|
|
63
|
+
private sendFrame;
|
|
64
|
+
private onData;
|
|
65
|
+
/** The 1008 mirror: an unusable server frame closes the socket. */
|
|
66
|
+
private protocolViolation;
|
|
67
|
+
/** Fail every live stream, drop the table, optionally close the socket. */
|
|
68
|
+
private teardown;
|
|
69
|
+
private armWatchdog;
|
|
70
|
+
private clearWatchdog;
|
|
71
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/** A stream/transport failure carrying wire failure fields (or a local cut code). */
|
|
2
|
+
export class MuxClientError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'MuxClientError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const DEFAULT_HEARTBEAT_MS = 2_000;
|
|
13
|
+
const MAX_SILENT_PERIODS = 2;
|
|
14
|
+
const DONE = { value: undefined, done: true };
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function exactKeys(value, keys) {
|
|
19
|
+
const own = Object.keys(value);
|
|
20
|
+
return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
21
|
+
}
|
|
22
|
+
function validId(value) {
|
|
23
|
+
return typeof value === 'string' && value.length > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Downlink lane of one stream: server items until a terminal frame or a local
|
|
27
|
+
* cut. Mirrors the upstream client's `StreamInbox` semantics — a wire `error`
|
|
28
|
+
* drains queued items first and then throws; local cuts drop the queue.
|
|
29
|
+
*/
|
|
30
|
+
class DownlinkQueue {
|
|
31
|
+
queue = [];
|
|
32
|
+
failure;
|
|
33
|
+
thrown = false;
|
|
34
|
+
done = false;
|
|
35
|
+
waiters = [];
|
|
36
|
+
push(value) {
|
|
37
|
+
if (this.done || this.failure !== undefined)
|
|
38
|
+
return;
|
|
39
|
+
this.queue.push(value);
|
|
40
|
+
this.signal();
|
|
41
|
+
}
|
|
42
|
+
/** Terminal server `end` frame: queued items drain, then the iterable finishes. */
|
|
43
|
+
settleEnd() {
|
|
44
|
+
if (this.done || this.failure !== undefined)
|
|
45
|
+
return;
|
|
46
|
+
this.done = true;
|
|
47
|
+
this.signal();
|
|
48
|
+
}
|
|
49
|
+
/** Server `error` frame: queued items drain, then the iterable throws it. */
|
|
50
|
+
settleFailure(error) {
|
|
51
|
+
if (this.done || this.failure !== undefined)
|
|
52
|
+
return;
|
|
53
|
+
this.failure = error;
|
|
54
|
+
this.signal();
|
|
55
|
+
}
|
|
56
|
+
/** Local cancellation: queued items are dropped, the iterable finishes. */
|
|
57
|
+
abort() {
|
|
58
|
+
if (this.done || this.failure !== undefined)
|
|
59
|
+
return;
|
|
60
|
+
this.queue.length = 0;
|
|
61
|
+
this.done = true;
|
|
62
|
+
this.signal();
|
|
63
|
+
}
|
|
64
|
+
/** Local cut (socket close, silence, dispose): drop the queue, throw now. */
|
|
65
|
+
fail(error) {
|
|
66
|
+
if (this.failure !== undefined)
|
|
67
|
+
return;
|
|
68
|
+
this.queue.length = 0;
|
|
69
|
+
this.done = true;
|
|
70
|
+
this.failure = error;
|
|
71
|
+
this.signal();
|
|
72
|
+
}
|
|
73
|
+
[Symbol.asyncIterator]() {
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
async next() {
|
|
77
|
+
while (true) {
|
|
78
|
+
if (this.queue.length > 0)
|
|
79
|
+
return { value: this.queue.shift(), done: false };
|
|
80
|
+
if (this.failure !== undefined && !this.thrown) {
|
|
81
|
+
this.thrown = true;
|
|
82
|
+
throw this.failure;
|
|
83
|
+
}
|
|
84
|
+
if (this.done || this.thrown)
|
|
85
|
+
return DONE;
|
|
86
|
+
await new Promise((resolve) => { this.waiters.push(resolve); });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
signal() {
|
|
90
|
+
const waiters = [...this.waiters];
|
|
91
|
+
this.waiters.length = 0;
|
|
92
|
+
for (const wake of waiters)
|
|
93
|
+
wake();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** The mux client half: pure protocol, socket injected. */
|
|
97
|
+
export class MuxClient {
|
|
98
|
+
socket;
|
|
99
|
+
heartbeatIntervalMs;
|
|
100
|
+
nextId = 1;
|
|
101
|
+
streams = new Map();
|
|
102
|
+
state = 'open';
|
|
103
|
+
watchdog;
|
|
104
|
+
constructor(socket, options = {}) {
|
|
105
|
+
this.socket = socket;
|
|
106
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
107
|
+
socket.onMessage((data) => { this.onData(data); });
|
|
108
|
+
socket.onClose(() => {
|
|
109
|
+
this.teardown(new MuxClientError('gateway/socket-closed', 'mux-client: the server closed the socket'));
|
|
110
|
+
});
|
|
111
|
+
this.armWatchdog();
|
|
112
|
+
}
|
|
113
|
+
/** Open one logical stream; the handle's lanes are documented in the header. */
|
|
114
|
+
openStream(endpoint, payload) {
|
|
115
|
+
if (this.state !== 'open')
|
|
116
|
+
throw new MuxClientError('gateway/disposed', 'mux-client: disposed');
|
|
117
|
+
const id = this.nextId;
|
|
118
|
+
this.nextId += 1;
|
|
119
|
+
const stream = { downlink: new DownlinkQueue(), ended: false, retired: false };
|
|
120
|
+
this.streams.set(String(id), stream);
|
|
121
|
+
this.sendFrame({ type: 'open', streamId: id, endpoint, payload });
|
|
122
|
+
const key = String(id);
|
|
123
|
+
const live = () => this.state === 'open' && !stream.retired;
|
|
124
|
+
const upstream = {
|
|
125
|
+
[Symbol.asyncIterator]: () => ({
|
|
126
|
+
next: (value) => {
|
|
127
|
+
if (!live() || stream.ended)
|
|
128
|
+
return Promise.resolve(DONE);
|
|
129
|
+
this.sendFrame(value === undefined
|
|
130
|
+
? { type: 'item', streamId: id }
|
|
131
|
+
: { type: 'item', streamId: id, value });
|
|
132
|
+
return Promise.resolve({ value: undefined, done: false });
|
|
133
|
+
},
|
|
134
|
+
return: () => {
|
|
135
|
+
this.endUplink(id, stream);
|
|
136
|
+
return Promise.resolve(DONE);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
items: stream.downlink,
|
|
142
|
+
upstream,
|
|
143
|
+
end: () => { this.endUplink(id, stream); },
|
|
144
|
+
cancel: () => {
|
|
145
|
+
if (!live())
|
|
146
|
+
return;
|
|
147
|
+
this.retire(key, stream);
|
|
148
|
+
stream.downlink.abort();
|
|
149
|
+
this.sendFrame({ type: 'cancel', streamId: id });
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Close every stream and the socket. Idempotent; no cancel frames (the server unwinds on socket close). */
|
|
154
|
+
dispose() {
|
|
155
|
+
this.teardown(new MuxClientError('gateway/disposed', 'mux-client: disposed'), true);
|
|
156
|
+
}
|
|
157
|
+
endUplink(id, stream) {
|
|
158
|
+
if (this.state !== 'open' || stream.retired || stream.ended)
|
|
159
|
+
return;
|
|
160
|
+
stream.ended = true;
|
|
161
|
+
this.sendFrame({ type: 'end', streamId: id });
|
|
162
|
+
}
|
|
163
|
+
retire(key, stream) {
|
|
164
|
+
stream.retired = true;
|
|
165
|
+
this.streams.delete(key);
|
|
166
|
+
}
|
|
167
|
+
/** Serialize one frame; the wire id is the stringified numeric counter. */
|
|
168
|
+
sendFrame(frame) {
|
|
169
|
+
const wire = { type: frame.type, streamId: String(frame.streamId) };
|
|
170
|
+
if (frame.type === 'open') {
|
|
171
|
+
wire['endpoint'] = frame.endpoint;
|
|
172
|
+
wire['payload'] = frame.payload;
|
|
173
|
+
}
|
|
174
|
+
if (frame.type === 'item' && frame.value !== undefined)
|
|
175
|
+
wire['value'] = frame.value;
|
|
176
|
+
this.socket.send(JSON.stringify(wire));
|
|
177
|
+
}
|
|
178
|
+
onData(data) {
|
|
179
|
+
if (this.state !== 'open')
|
|
180
|
+
return;
|
|
181
|
+
this.armWatchdog(); // any received frame is liveness
|
|
182
|
+
let raw;
|
|
183
|
+
try {
|
|
184
|
+
raw = JSON.parse(data);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
this.protocolViolation('malformed server frame');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!isRecord(raw)) {
|
|
191
|
+
this.protocolViolation('malformed server frame');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (raw['type'] === 'ping') {
|
|
195
|
+
if (!exactKeys(raw, ['type'])) {
|
|
196
|
+
this.protocolViolation('malformed ping frame');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.socket.send('{"type":"pong"}');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (raw['type'] === 'item') {
|
|
203
|
+
if (!(exactKeys(raw, ['type', 'streamId']) || exactKeys(raw, ['type', 'streamId', 'value']))
|
|
204
|
+
|| !validId(raw['streamId'])) {
|
|
205
|
+
this.protocolViolation('malformed item frame');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const stream = this.streams.get(raw['streamId']);
|
|
209
|
+
if (stream === undefined)
|
|
210
|
+
return; // late frame: stream retired, drop
|
|
211
|
+
stream.downlink.push(Object.hasOwn(raw, 'value') ? raw['value'] : undefined);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (raw['type'] === 'end') {
|
|
215
|
+
if (!exactKeys(raw, ['type', 'streamId']) || !validId(raw['streamId'])) {
|
|
216
|
+
this.protocolViolation('malformed end frame');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const stream = this.streams.get(raw['streamId']);
|
|
220
|
+
if (stream === undefined)
|
|
221
|
+
return;
|
|
222
|
+
this.retire(raw['streamId'], stream);
|
|
223
|
+
stream.downlink.settleEnd();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (raw['type'] === 'error') {
|
|
227
|
+
const failure = failureOf(raw['error']);
|
|
228
|
+
if (failure === undefined || !exactKeys(raw, ['type', 'streamId', 'error']) || !validId(raw['streamId'])) {
|
|
229
|
+
this.protocolViolation('malformed error frame');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const stream = this.streams.get(raw['streamId']);
|
|
233
|
+
if (stream === undefined)
|
|
234
|
+
return;
|
|
235
|
+
this.retire(raw['streamId'], stream);
|
|
236
|
+
stream.downlink.settleFailure(new MuxClientError(failure.code, failure.message, failure.details));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.protocolViolation(`out-of-protocol server frame (${String(raw['type'])})`);
|
|
240
|
+
}
|
|
241
|
+
/** The 1008 mirror: an unusable server frame closes the socket. */
|
|
242
|
+
protocolViolation(detail) {
|
|
243
|
+
this.teardown(new MuxClientError('gateway/protocol', `mux-client: ${detail} — closing the socket`), true);
|
|
244
|
+
}
|
|
245
|
+
/** Fail every live stream, drop the table, optionally close the socket. */
|
|
246
|
+
teardown(error, close = false) {
|
|
247
|
+
if (this.state !== 'open')
|
|
248
|
+
return;
|
|
249
|
+
this.state = 'closed';
|
|
250
|
+
this.clearWatchdog();
|
|
251
|
+
for (const stream of this.streams.values()) {
|
|
252
|
+
stream.retired = true;
|
|
253
|
+
stream.downlink.fail(error);
|
|
254
|
+
}
|
|
255
|
+
this.streams.clear();
|
|
256
|
+
if (close)
|
|
257
|
+
this.socket.close();
|
|
258
|
+
}
|
|
259
|
+
armWatchdog() {
|
|
260
|
+
this.clearWatchdog();
|
|
261
|
+
const timer = setTimeout(() => {
|
|
262
|
+
this.teardown(new MuxClientError('gateway/heartbeat-silence', `mux-client: no frames for ${String(this.heartbeatIntervalMs * MAX_SILENT_PERIODS)}ms`), true);
|
|
263
|
+
}, this.heartbeatIntervalMs * MAX_SILENT_PERIODS);
|
|
264
|
+
this.watchdog = timer;
|
|
265
|
+
}
|
|
266
|
+
clearWatchdog() {
|
|
267
|
+
if (this.watchdog === undefined)
|
|
268
|
+
return;
|
|
269
|
+
clearTimeout(this.watchdog);
|
|
270
|
+
this.watchdog = undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** Validate a wire failure body (`MuxFailure` shape); undefined when malformed. */
|
|
274
|
+
function failureOf(value) {
|
|
275
|
+
if (!isRecord(value))
|
|
276
|
+
return undefined;
|
|
277
|
+
if (typeof value['code'] !== 'string' || typeof value['message'] !== 'string')
|
|
278
|
+
return undefined;
|
|
279
|
+
const details = value['details'];
|
|
280
|
+
return { code: value['code'], message: value['message'], details: isRecord(details) ? details : {} };
|
|
281
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The super-dsh PACK version (D6 fix): `super-dsh/package.json`'s version —
|
|
3
|
+
* the number `dsh plugin add` actually versions and the only one that moves
|
|
4
|
+
* with releases. The hub SUBPACKAGE's own manifest is a constant 0.1.0, which
|
|
5
|
+
* made both the served `hub.version` and the I2 comparator dead code exactly
|
|
6
|
+
* in the case they exist for (a stale peer pack).
|
|
7
|
+
*
|
|
8
|
+
* Depth contract: this module compiles to `<pack root>/superd-hub/dist/`, so
|
|
9
|
+
* the pack manifest is exactly two hops up — `../../package.json`. Verified
|
|
10
|
+
* against the built tree in BOTH shapes: repo (`super-dsh/superd-hub/dist/`
|
|
11
|
+
* → `super-dsh/package.json`) and the packed tarball (`package/superd-hub/dist/`
|
|
12
|
+
* → `package/package.json`; npm always includes the root manifest).
|
|
13
|
+
*
|
|
14
|
+
* Unparsable or missing → 'unknown' (warn honestly: the I2 comparator treats
|
|
15
|
+
* an unreadable version as a compatibility fact, which is the truth).
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
function readPackVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = createRequire(import.meta.url)('../../package.json');
|
|
21
|
+
if (typeof pkg.version === 'string' && pkg.version !== '')
|
|
22
|
+
return pkg.version;
|
|
23
|
+
return 'unknown';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 'unknown';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The super-dsh pack version, or 'unknown' when the manifest cannot be read. */
|
|
30
|
+
export const PACK_VERSION = readPackVersion();
|
|
@@ -15,6 +15,12 @@ export interface RosterEntry {
|
|
|
15
15
|
label: string;
|
|
16
16
|
/** True once the adapter's world context is spawned and its target registered. */
|
|
17
17
|
ready: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Owning machine id (m4) for FEDERATED rows (mesh P2 Task 3); absent = this
|
|
20
|
+
* machine's local world. The a6-keyed row's owner; `agentLinks` projects it
|
|
21
|
+
* onto AgentLink.machine and derives remote grouping/ordering from it.
|
|
22
|
+
*/
|
|
23
|
+
machine?: string;
|
|
18
24
|
/** True once the world's spawn REJECTED (world-join catch) — stays mounted for diagnosis. */
|
|
19
25
|
failed?: boolean;
|
|
20
26
|
}
|
package/superd-hub/dist/rpc.d.ts
CHANGED
|
@@ -23,16 +23,32 @@ export interface RuntimeTargetSource {
|
|
|
23
23
|
}
|
|
24
24
|
/** The live registry view: native plus every registered foreign context. */
|
|
25
25
|
export declare const registryTargets: RuntimeTargetSource;
|
|
26
|
+
/** Injection seam for the roster/hub identity: the hub home backing the a6
|
|
27
|
+
* id store and the machines registry. Defaults to `resolveDshHome()`
|
|
28
|
+
* (registerSelectorRpc passes no deps — the same home the rest of this
|
|
29
|
+
* process uses); unit tests inject a tmpdir so nothing ever touches an
|
|
30
|
+
* ambient/prod DSH_HOME. */
|
|
31
|
+
export interface AgentRuntimeDeps {
|
|
32
|
+
home?: string;
|
|
33
|
+
}
|
|
26
34
|
/**
|
|
27
35
|
* The control-plane handler: world inventory only.
|
|
36
|
+
*
|
|
37
|
+
* ADDITIVE contract (mesh P1): the GET body gained `hub: {machineId, label,
|
|
38
|
+
* version}` and every `agents` row gained `id` (a6); `available` and all
|
|
39
|
+
* pre-existing agents fields are unchanged — old clients ignore the new
|
|
40
|
+
* fields, new clients read them.
|
|
28
41
|
* @param request - the incoming `/api/agent-runtime` request.
|
|
29
42
|
* @param source - live runtime registry view.
|
|
43
|
+
* @param deps - optional home override; defaults to `resolveDshHome()`.
|
|
30
44
|
* @returns the JSON response.
|
|
31
45
|
*/
|
|
32
|
-
export declare function handleAgentRuntime(request: Request, source: RuntimeTargetSource): Promise<Response>;
|
|
46
|
+
export declare function handleAgentRuntime(request: Request, source: RuntimeTargetSource, deps?: AgentRuntimeDeps): Promise<Response>;
|
|
33
47
|
/**
|
|
34
48
|
* Register the control-plane RPC face on the shared `/api` connection channel
|
|
35
49
|
* (behind the standard browser-trust + auth fence). No-op when the
|
|
36
|
-
* `connection` service is absent (e.g. fake-ctx unit tests).
|
|
50
|
+
* `connection` service is absent (e.g. fake-ctx unit tests). The route answers
|
|
51
|
+
* with the DEFAULT home (`resolveDshHome()` at request time); callers needing
|
|
52
|
+
* a pinned home invoke handleAgentRuntime directly with deps.
|
|
37
53
|
*/
|
|
38
54
|
export declare function registerSelectorRpc(ctx: Context, source: RuntimeTargetSource): void;
|