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.
Files changed (87) hide show
  1. package/client/index.js +741 -256
  2. package/cordis.patch.yml +16 -0
  3. package/package.json +7 -2
  4. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  5. package/superd-hub/dist/a2a-delivery.js +240 -0
  6. package/superd-hub/dist/a2a-host.d.ts +76 -0
  7. package/superd-hub/dist/a2a-host.js +145 -0
  8. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  9. package/superd-hub/dist/a2a-ingest.js +636 -0
  10. package/superd-hub/dist/a2a-log.d.ts +17 -0
  11. package/superd-hub/dist/a2a-log.js +42 -0
  12. package/superd-hub/dist/a2a-row.d.ts +65 -0
  13. package/superd-hub/dist/a2a-row.js +162 -0
  14. package/superd-hub/dist/a2a-service.d.ts +357 -0
  15. package/superd-hub/dist/a2a-service.js +980 -0
  16. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  17. package/superd-hub/dist/a2a-wire.js +305 -0
  18. package/superd-hub/dist/agent-ids.d.ts +10 -0
  19. package/superd-hub/dist/agent-ids.js +68 -0
  20. package/superd-hub/dist/agent-roster.d.ts +29 -18
  21. package/superd-hub/dist/agent-roster.js +54 -15
  22. package/superd-hub/dist/agent-status.d.ts +119 -0
  23. package/superd-hub/dist/agent-status.js +386 -0
  24. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  25. package/superd-hub/dist/client/mesh-api.js +219 -0
  26. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  27. package/superd-hub/dist/client/mesh-groups.js +66 -0
  28. package/superd-hub/dist/client/roster.d.ts +48 -0
  29. package/superd-hub/dist/client/roster.js +96 -0
  30. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  31. package/superd-hub/dist/exec-pty-channel.js +162 -0
  32. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  33. package/superd-hub/dist/exec-pty-framing.js +81 -0
  34. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  35. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  36. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  37. package/superd-hub/dist/exec-pty-session.js +234 -0
  38. package/superd-hub/dist/exec.d.ts +56 -0
  39. package/superd-hub/dist/exec.js +225 -0
  40. package/superd-hub/dist/gateway.d.ts +39 -2
  41. package/superd-hub/dist/gateway.js +62 -3
  42. package/superd-hub/dist/hub-tool.d.ts +174 -0
  43. package/superd-hub/dist/hub-tool.js +464 -0
  44. package/superd-hub/dist/index.d.ts +15 -1
  45. package/superd-hub/dist/index.js +16 -0
  46. package/superd-hub/dist/machines.d.ts +45 -0
  47. package/superd-hub/dist/machines.js +125 -0
  48. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  49. package/superd-hub/dist/mesh-federate.js +223 -0
  50. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  51. package/superd-hub/dist/mesh-gateway.js +408 -0
  52. package/superd-hub/dist/mesh-link.d.ts +274 -0
  53. package/superd-hub/dist/mesh-link.js +441 -0
  54. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  55. package/superd-hub/dist/mesh-rpc.js +250 -0
  56. package/superd-hub/dist/mux-client.d.ts +71 -0
  57. package/superd-hub/dist/mux-client.js +281 -0
  58. package/superd-hub/dist/pack-version.d.ts +2 -0
  59. package/superd-hub/dist/pack-version.js +30 -0
  60. package/superd-hub/dist/roster.d.ts +6 -0
  61. package/superd-hub/dist/rpc.d.ts +18 -2
  62. package/superd-hub/dist/rpc.js +34 -4
  63. package/superd-hub/dist/spawn-world.js +3 -1
  64. package/superd-hub/dist/switch-core.d.ts +62 -25
  65. package/superd-hub/dist/switch-core.js +53 -30
  66. package/superd-hub/dist/system-head.d.ts +22 -12
  67. package/superd-hub/dist/system-head.js +100 -84
  68. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  69. package/superd-hub/dist/wire-endpoints.js +180 -0
  70. package/superd-hub/lib/client/index.js +741 -256
  71. package/superd-hub/package.json +11 -2
  72. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  73. package/agent-agy/dist/agent-preset-projection.js +0 -33
  74. package/agent-claude/dist/agent-preset-projection.js +0 -30
  75. package/agent-codex/dist/agent-preset-projection.js +0 -37
  76. package/agent-codex/dist/app-home.js +0 -65
  77. package/agent-codex/dist/spike.js +0 -46
  78. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  79. package/agent-omp/dist/agent-preset-projection.js +0 -36
  80. package/agent-omp/dist/app-home.js +0 -86
  81. package/agent-pi/dist/agent-preset-projection.js +0 -29
  82. package/superd-hub/dist/client-shim.d.ts +0 -28
  83. package/superd-hub/dist/client-shim.js +0 -251
  84. package/superd-hub/dist/index-pass.d.ts +0 -22
  85. package/superd-hub/dist/index-pass.js +0 -47
  86. package/superd-hub/dist/labels.d.ts +0 -45
  87. package/superd-hub/dist/labels.js +0 -83
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Mesh link (mesh P1, spec §4/§6.3): remote connection management —
3
+ * token-URL parsing, cookie mint, roster probe, and the pairing /
4
+ * reprobe orchestration that adopts the remote machine id (D4) into
5
+ * the machine registry from `./machines.js`.
6
+ *
7
+ * Pure module — no cordis dependencies. Outbound calls use the global
8
+ * fetch (injectable via `fetchImpl` / `deps.fetchImpl` for tests) with
9
+ * a 10s AbortSignal timeout. The minted cookie is persisted on the machine
10
+ * row (`MachineRow.cookie`, D1 sensitivity, §3.3/§4) so hub restarts keep the
11
+ * remote's 30-day session; an in-process jar fronts the row as a write-through
12
+ * cache of the same value. Probe outcomes land
13
+ * in the module-level status table exported read-only via
14
+ * `machineStatus()` (mesh API consumes; `resetMachineStatus()` is the
15
+ * test hook). P1 was probe-on-demand only; the mesh P3 Task 5a user
16
+ * mandate ("状态更新走单向 API + 5 分钟周期轮询") adds the PERIODIC sweep
17
+ * (`startReprobeSweep`) — a timer-discipline layer over the same
18
+ * `reprobeMachine`, never a second probing path. I2 also lands here: every
19
+ * successful probe compares the peer's `hub.version` against ours and
20
+ * records a `versionWarning` on the status entry (diff major/minor or
21
+ * unparsable → warn; patch-only drift → clean).
22
+ */
23
+ import { loadMachines, saveMachines, upsertRemote, listRemotes } from './machines.js';
24
+ import { PACK_VERSION } from './pack-version.js';
25
+ /**
26
+ * The `ours` side of the I2 comparison — the super-dsh PACK version (D6: the
27
+ * hub subpackage's constant 0.1.0 made the comparator dead; the pack number
28
+ * is what `dsh plugin add` versions and the only one that moves). Exported so
29
+ * tests can pin the comparator's production input to the real pack manifest.
30
+ */
31
+ export const OWN_VERSION = PACK_VERSION;
32
+ /**
33
+ * Parse a version's major.minor core (I2): tolerant of a leading `v`, a
34
+ * missing patch segment, and a prerelease/build suffix — the mismatch rule
35
+ * ignores everything past `major.minor` by design (patch drift between
36
+ * paired hubs is routine and harmless).
37
+ */
38
+ function parseVersionCore(version) {
39
+ const m = /^\s*v?(\d+)\.(\d+)/.exec(version);
40
+ if (m === null)
41
+ return undefined;
42
+ return { major: Number(m[1]), minor: Number(m[2]) };
43
+ }
44
+ /**
45
+ * The I2 comparator: when to warn about a peer's superd version.
46
+ * - different major OR minor → a warning naming both versions;
47
+ * - same major+minor (patch-only drift) → `undefined` (clean);
48
+ * - unparsable or missing on EITHER side → a warning carrying 'unknown'
49
+ * (an unreadable peer version is itself a compatibility fact).
50
+ * Pure and exported for direct unit testing; the pair/reprobe paths feed it
51
+ * `probe.version` and the module's {@link OWN_VERSION}.
52
+ */
53
+ export function versionWarningFor(peer, ours) {
54
+ const peerCore = peer !== undefined ? parseVersionCore(peer) : undefined;
55
+ const ourCore = parseVersionCore(ours);
56
+ if (peerCore === undefined || ourCore === undefined) {
57
+ if (peer === undefined)
58
+ return 'version unknown (peer carries none)';
59
+ if (peerCore === undefined)
60
+ return `version unknown ("${peer}")`;
61
+ return `version unknown (ours "${ours}")`;
62
+ }
63
+ if (peerCore.major !== ourCore.major || peerCore.minor !== ourCore.minor) {
64
+ return `version ${peer} ≠ ours ${ours}`;
65
+ }
66
+ return undefined;
67
+ }
68
+ const FETCH_TIMEOUT_MS = 10_000;
69
+ /** <machineId, cookie> — in-process jar, a front for the persisted `MachineRow.cookie` (same value; write-through on every mint). */
70
+ const cookieJar = new Map();
71
+ const machineStatusTable = new Map();
72
+ function errMsg(err) {
73
+ return err instanceof Error ? err.message : String(err);
74
+ }
75
+ /** Validate a pasted token URL: absolute http(s) origin + non-empty `?token=`.
76
+ * Error copy never echoes the raw input — a typo'd paste may still carry a live
77
+ * token, so messages carry at most the (userinfo-free) origin. */
78
+ export function parseTokenUrl(tokenUrl) {
79
+ let url;
80
+ try {
81
+ url = new URL(tokenUrl);
82
+ }
83
+ catch {
84
+ throw new Error('mesh-link: invalid token url — not a parseable absolute URL');
85
+ }
86
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
87
+ throw new Error(`mesh-link: invalid token url — origin must be http(s), got "${url.origin}"`);
88
+ }
89
+ const token = url.searchParams.get('token') ?? '';
90
+ if (token === '') {
91
+ throw new Error(`mesh-link: invalid token url at "${url.origin}" — missing ?token= query parameter`);
92
+ }
93
+ return { origin: url.origin, token };
94
+ }
95
+ /** Fallback Set-Cookie splitter for header environments without `getSetCookie()`. */
96
+ function splitSetCookieHeader(joined) {
97
+ if (!joined)
98
+ return [];
99
+ return joined.split(/,(?=\s*[^;=\s]+\s*=)/);
100
+ }
101
+ /** First `name=value` pair of a Set-Cookie value (attributes stripped). */
102
+ function firstCookiePair(raw) {
103
+ const m = /^\s*([^=;\s]+=[^;]*)/.exec(raw);
104
+ return m ? m[1].trim() : null;
105
+ }
106
+ /**
107
+ * Mint a request cookie from a launch token: `GET <origin>/?token=<t>` with
108
+ * `redirect: 'manual'`, taking the first `dsh-auth-*` name=value pair from
109
+ * Set-Cookie. Non-303/2xx, a missing cookie, or a network failure throws
110
+ * with the status/cause attached.
111
+ */
112
+ export async function mintCookie(origin, token, fetchImpl = globalThis.fetch) {
113
+ let res;
114
+ try {
115
+ res = await fetchImpl(`${origin}/?token=${encodeURIComponent(token)}`, {
116
+ redirect: 'manual',
117
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
118
+ });
119
+ }
120
+ catch (err) {
121
+ throw new Error(`mesh-link: token mint to ${origin} failed — ${errMsg(err)}`);
122
+ }
123
+ if (res.status !== 303 && !(res.status >= 200 && res.status < 300)) {
124
+ throw new Error(`mesh-link: token mint failed — unexpected status ${res.status} from ${origin}`);
125
+ }
126
+ const rawCookies = typeof res.headers.getSetCookie === 'function'
127
+ ? res.headers.getSetCookie()
128
+ : splitSetCookieHeader(res.headers.get('set-cookie'));
129
+ for (const raw of rawCookies) {
130
+ const pair = firstCookiePair(raw);
131
+ if (pair !== null && pair.startsWith('dsh-auth-'))
132
+ return pair;
133
+ }
134
+ throw new Error(`mesh-link: token mint failed — no dsh-auth-* Set-Cookie in response (status ${res.status}) from ${origin}`);
135
+ }
136
+ function coerceAgent(v) {
137
+ const a = (typeof v === 'object' && v !== null ? v : {});
138
+ return {
139
+ id: typeof a.id === 'string' ? a.id : '',
140
+ key: typeof a.key === 'string' ? a.key : '',
141
+ label: typeof a.label === 'string' ? a.label : '',
142
+ ready: a.ready === true,
143
+ failed: a.failed === true,
144
+ machine: typeof a.machine === 'string' && a.machine !== '' ? a.machine : 'local',
145
+ };
146
+ }
147
+ /**
148
+ * Probe a remote hub's roster: `GET <origin>/api/agent-runtime` with the
149
+ * cookie riding along. 401 → `auth-expired`; network error → `unreachable`;
150
+ * 200 without a `hub.machineId` block is a non-superdsh 0.1.6+ peer →
151
+ * `unreachable` with `peer roster has no hub block`.
152
+ */
153
+ export async function probeMachine(origin, cookie, fetchImpl = globalThis.fetch) {
154
+ let res;
155
+ try {
156
+ // `?mesh=1` asks the peer for ITS OWN rows only (self-description
157
+ // discipline): a hub never re-broadcasts other remotes' agents. The
158
+ // adoption filter below is the belt-and-braces leg for older peers.
159
+ res = await fetchImpl(`${origin}/api/agent-runtime?mesh=1`, {
160
+ headers: cookie === '' ? {} : { cookie },
161
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
162
+ });
163
+ }
164
+ catch (err) {
165
+ return { ok: false, status: 'unreachable', error: `probe fetch to ${origin} failed — ${errMsg(err)}` };
166
+ }
167
+ if (res.status === 401) {
168
+ return { ok: false, status: 'auth-expired', error: `probe got 401 from ${origin} — cookie rejected` };
169
+ }
170
+ if (res.status !== 200) {
171
+ return { ok: false, status: 'unreachable', error: `probe got status ${res.status} from ${origin}` };
172
+ }
173
+ let body;
174
+ try {
175
+ body = await res.json();
176
+ }
177
+ catch (err) {
178
+ return { ok: false, status: 'unreachable', error: `probe roster from ${origin} is not JSON — ${errMsg(err)}` };
179
+ }
180
+ const o = (typeof body === 'object' && body !== null ? body : {});
181
+ const hub = (typeof o.hub === 'object' && o.hub !== null ? o.hub : {});
182
+ if (typeof hub.machineId !== 'string' || hub.machineId === '') {
183
+ return { ok: false, status: 'unreachable', error: 'peer roster has no hub block' };
184
+ }
185
+ // P2P (2026-09-28 user ruling): adopt ONLY the responder's own rows
186
+ // (machine === 'local'). Whatever else its roster carries — mirrors of US,
187
+ // third-party remotes it federates — is its own business; the requester
188
+ // merges its N paired machines itself, one probe each.
189
+ const agents = (Array.isArray(o.agents) ? o.agents.map(coerceAgent) : [])
190
+ .filter((a) => a.machine === 'local');
191
+ return {
192
+ ok: true,
193
+ machineId: hub.machineId,
194
+ version: typeof hub.version === 'string' ? hub.version : undefined,
195
+ agents,
196
+ };
197
+ }
198
+ /** Read-only snapshot copy of the status table — callers can hold/mutate it freely. */
199
+ export function machineStatus() {
200
+ return new Map(Array.from(machineStatusTable, ([id, e]) => [id, { ...e }]));
201
+ }
202
+ /** Test hook: drop all status entries. */
203
+ export function resetMachineStatus() {
204
+ machineStatusTable.clear();
205
+ }
206
+ const federationListeners = new Set();
207
+ /** Subscribe to federation lifecycle events; returns an unsubscribe function. */
208
+ export function onMeshFederationEvent(fn) {
209
+ federationListeners.add(fn);
210
+ return () => {
211
+ federationListeners.delete(fn);
212
+ };
213
+ }
214
+ function emitFederation(event) {
215
+ for (const fn of federationListeners)
216
+ fn(event);
217
+ }
218
+ /**
219
+ * Announce a machine removal on the federation bus (`{type: 'defederated'}`).
220
+ * Called by mesh-rpc's DELETE path AFTER a successful `removeRemote` — the
221
+ * route module stays free of federation logic (one-way imports: mesh-rpc →
222
+ * mesh-link, the registrar subscribes at wiring time).
223
+ */
224
+ export function emitMachineRemoved(machineId) {
225
+ emitFederation({ type: 'defederated', machineId });
226
+ }
227
+ /**
228
+ * The machine's CURRENT pairing cookie: the in-process jar first (warm after
229
+ * every mint this process — pair and reprobe both write it before any
230
+ * federation event fires), then the persisted `MachineRow.cookie` (only when
231
+ * `home` is given; the defensive cold path for gateways built without a probe
232
+ * in this process), else `''` (wire calls then simply omit the Cookie header).
233
+ */
234
+ export function cookieOf(machineId, home) {
235
+ const jarred = cookieJar.get(machineId);
236
+ if (jarred !== undefined)
237
+ return jarred;
238
+ if (home === undefined)
239
+ return '';
240
+ return listRemotes(home).find((r) => r.id === machineId)?.cookie ?? '';
241
+ }
242
+ /** Record one probe outcome; the I2 versionWarning (4th arg) lands only on
243
+ * reachable probes — every entry is written FRESH, so a re-probe against a
244
+ * converged peer replaces a stale warning with a clean entry. */
245
+ function recordStatus(id, status, error, versionWarning) {
246
+ const entry = error === undefined
247
+ ? { status, lastProbeAt: Date.now() }
248
+ : { status, lastProbeAt: Date.now(), error };
249
+ if (versionWarning !== undefined)
250
+ entry.versionWarning = versionWarning;
251
+ machineStatusTable.set(id, entry);
252
+ }
253
+ /** Persist one remote row; uses `upsertRemote` unless a load/save seam is injected (test isolation). */
254
+ function persistRow(home, row, deps) {
255
+ if (!deps?.load && !deps?.save) {
256
+ upsertRemote(home, row);
257
+ return;
258
+ }
259
+ const file = (deps.load ?? loadMachines)(home);
260
+ const idx = file.remotes.findIndex((r) => r.id === row.id);
261
+ if (idx === -1)
262
+ file.remotes.push({ ...row });
263
+ else
264
+ file.remotes[idx] = { ...row };
265
+ (deps.save ?? saveMachines)(home, file);
266
+ }
267
+ /**
268
+ * Full pairing: parse the pasted token URL → mint a cookie → probe the roster
269
+ * → adopt the remote machine id (fail-loud on collision with self or an
270
+ * existing remote) → persist the row with the retained token and minted
271
+ * cookie → jar the cookie and mark the machine reachable. A failed mint/probe throws before
272
+ * anything is persisted.
273
+ */
274
+ export async function pairMachine(home, input, deps) {
275
+ const fetchImpl = deps?.fetchImpl ?? globalThis.fetch;
276
+ const load = deps?.load ?? loadMachines;
277
+ const { origin, token } = parseTokenUrl(input.tokenUrl);
278
+ const cookie = await mintCookie(origin, token, fetchImpl);
279
+ const probe = await probeMachine(origin, cookie, fetchImpl);
280
+ if (!probe.ok) {
281
+ throw new Error(`mesh-link: pairing ${origin} failed — ${probe.status}: ${probe.error}`);
282
+ }
283
+ const file = load(home);
284
+ if (probe.machineId === file.self.id || file.remotes.some((r) => r.id === probe.machineId)) {
285
+ throw new Error(`mesh-link: duplicate-machine-id ${probe.machineId} — collides with self or an existing remote row; remove the stale row before re-pairing`);
286
+ }
287
+ const row = {
288
+ id: probe.machineId,
289
+ name: input.name,
290
+ label: input.label ?? input.name,
291
+ origin,
292
+ token,
293
+ cookie,
294
+ enabled: true,
295
+ addedAt: Date.now(),
296
+ };
297
+ persistRow(home, row, deps);
298
+ cookieJar.set(row.id, cookie);
299
+ recordStatus(row.id, 'reachable', undefined, versionWarningFor(probe.version, OWN_VERSION));
300
+ emitFederation({ type: 'federated', machineId: row.id, machineName: row.name, origin, agents: probe.agents });
301
+ return { machineId: row.id, row, probe };
302
+ }
303
+ /**
304
+ * Re-probe a paired machine: the jar's cookie, falling back to the cookie
305
+ * persisted on the row (cold jar after a hub restart), rides the probe. On
306
+ * `auth-expired`, if the row retains a token, re-mint once and probe again; a
307
+ * successful re-mint writes the new cookie back into the jar AND the row. Every
308
+ * outcome lands in the status table
309
+ * (`reachable` / `auth-expired` / `unreachable`, the latter two with the error).
310
+ */
311
+ export async function reprobeMachine(home, id, deps) {
312
+ const fetchImpl = deps?.fetchImpl ?? globalThis.fetch;
313
+ const remotes = deps?.load ? deps.load(home).remotes : listRemotes(home);
314
+ const row = remotes.find((r) => r.id === id);
315
+ if (!row) {
316
+ throw new Error(`mesh-link: unknown machine id "${id}" — no remote row in the registry`);
317
+ }
318
+ const origin = row.origin;
319
+ let cookie = cookieJar.get(id) ?? row.cookie ?? '';
320
+ let probe = await probeMachine(origin, cookie, fetchImpl);
321
+ if (!probe.ok && probe.status === 'auth-expired' && row.token) {
322
+ try {
323
+ cookie = await mintCookie(origin, row.token, fetchImpl);
324
+ cookieJar.set(id, cookie);
325
+ persistRow(home, { ...row, cookie }, deps);
326
+ probe = await probeMachine(origin, cookie, fetchImpl);
327
+ }
328
+ catch (err) {
329
+ probe = { ok: false, status: 'auth-expired', error: `re-mint failed — ${errMsg(err)}` };
330
+ }
331
+ }
332
+ if (probe.ok) {
333
+ cookieJar.set(id, cookie);
334
+ recordStatus(id, 'reachable', undefined, versionWarningFor(probe.version, OWN_VERSION));
335
+ emitFederation({ type: 'federated', machineId: id, machineName: row.name, origin, agents: probe.agents });
336
+ }
337
+ else if (probe.status === 'auth-expired') {
338
+ recordStatus(id, 'auth-expired', probe.error);
339
+ emitFederation({ type: 'degraded', machineId: id, reason: 'auth-expired', error: probe.error });
340
+ }
341
+ else {
342
+ recordStatus(id, 'unreachable', probe.error);
343
+ emitFederation({ type: 'degraded', machineId: id, reason: probe.status, error: probe.error });
344
+ }
345
+ return probe;
346
+ }
347
+ // ---- I1(a): the periodic reprobe sweep (mesh P3 Task 5a) ----------------------
348
+ /** Default sweep period — the user mandate's 5 minutes (5a I1-a). */
349
+ export const REPROBE_SWEEP_DEFAULT_SEC = 300;
350
+ /** Floor for any sweep period: below this a reprobe loop is a self-inflicted storm. */
351
+ export const REPROBE_SWEEP_MIN_SEC = 60;
352
+ let registeredCollectors = {};
353
+ /** Register/unregister the process-level collectors (the a2a row's assembly; set-once semantics, ctx0 only). */
354
+ export function setSweepCollectors(collectors) {
355
+ registeredCollectors = collectors ?? {};
356
+ }
357
+ /** One full sweep — the timer's entire payload. Reads the registry FRESH at
358
+ * start (rows paired/removed since the last tick are honored), probes every
359
+ * ENABLED remote through the real {@link reprobeMachine} (success converges
360
+ * federation via the existing event hook; failure degrades — no second
361
+ * probing path), and swallows every per-machine rejection so one dead peer
362
+ * (or a row deleted mid-sweep → unknown-id throw) never rejects the sweep.
363
+ * Since R5 the collectors run inside the same swallow discipline.
364
+ * `localCollect: false` (O1b, 0.1.8-e) skips ONLY the local leg: the boot tick
365
+ * uses it because the local session plane is not mounted yet the moment the
366
+ * hub's rows come up — sampling there yields an all-error collection that
367
+ * would then read as a fresh, empty window (see `startReprobeSweep`). */
368
+ export async function runReprobeSweep(deps) {
369
+ const collectors = deps.collectors ?? registeredCollectors;
370
+ if (deps.localCollect !== false && collectors.onLocalCollect !== undefined) {
371
+ try {
372
+ await collectors.onLocalCollect();
373
+ }
374
+ catch {
375
+ // the local leg degrades — remotes still get their probe
376
+ }
377
+ }
378
+ for (const row of listRemotes(deps.home)) {
379
+ if (!row.enabled)
380
+ continue;
381
+ try {
382
+ const probe = await reprobeMachine(deps.home, row.id, { fetchImpl: deps.fetchImpl });
383
+ if (probe.ok && collectors.onRemoteProbed !== undefined) {
384
+ try {
385
+ await collectors.onRemoteProbed({ remote: row, probe });
386
+ }
387
+ catch {
388
+ // a leg failure never breaks probe accounting or the sweep loop
389
+ }
390
+ }
391
+ }
392
+ catch {
393
+ // Per-machine errors end here — the sweep itself always settles clean.
394
+ }
395
+ }
396
+ }
397
+ /**
398
+ * Start the periodic reprobe sweep (I1-a). Discipline, all of it load-bearing:
399
+ * - the interval handle is `unref()`d — a freshness sweep never holds the
400
+ * hub process (or a test runner) open;
401
+ * - a tick while the previous sweep is still in flight is SKIPPED (no
402
+ * overlap: a slow peer slows the cadence, it never stacks sweeps);
403
+ * - the returned disposer clears the timer AND kills any late tick — the
404
+ * hub host wiring registers it as a ctx effect, so the sweep dies with
405
+ * the row (dispose-clean); the disposer is idempotent.
406
+ * The first sweep runs after one full period, except when `immediate` asks
407
+ * for one at boot (the mesh UI's own reprobe stays available too).
408
+ */
409
+ export function startReprobeSweep(deps) {
410
+ const seconds = Math.max(REPROBE_SWEEP_MIN_SEC, deps.intervalSec ?? REPROBE_SWEEP_DEFAULT_SEC);
411
+ const setInt = deps.setIntervalFn ?? ((fn, ms) => globalThis.setInterval(fn, ms));
412
+ const clearInt = deps.clearIntervalFn ?? ((handle) => globalThis.clearInterval(handle));
413
+ let inFlight = false;
414
+ let disposed = false;
415
+ /** One tick. The boot tick passes `localCollect=false` (O1b, 0.1.8-e). */
416
+ const tick = (localCollect) => {
417
+ if (disposed || inFlight)
418
+ return;
419
+ inFlight = true;
420
+ void runReprobeSweep({
421
+ home: deps.home,
422
+ ...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
423
+ ...(deps.collectors === undefined ? {} : { collectors: deps.collectors }),
424
+ ...(localCollect ? {} : { localCollect: false }),
425
+ }).finally(() => { inFlight = false; });
426
+ };
427
+ const handle = setInt(() => tick(true), seconds * 1_000);
428
+ handle.unref?.();
429
+ // O1 (0.1.8-c): fill the roster at BOOT, not one full period later.
430
+ // O1b (0.1.8-e): the local leg is deferred to the first regular tick — the
431
+ // local session plane is still mounting and an early sample would be cached
432
+ // as an empty window for a full interval.
433
+ if (deps.immediate === true)
434
+ tick(false);
435
+ return () => {
436
+ if (disposed)
437
+ return;
438
+ disposed = true;
439
+ clearInt(handle);
440
+ };
441
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Machine-mesh management API (mesh P1, spec §6.3): the add-machine face
3
+ * mounted behind `ctx.connection.fetch` — the client-connection service owns
4
+ * the single `/api` prefix webServer route and runs the Host/Origin
5
+ * browser-trust fence + persistent browser auth before dispatching (same
6
+ * auth conclusion as rpc.ts; registering our own webServer route would
7
+ * bypass that fence).
8
+ *
9
+ * Route + handler ONLY — pairing/probing business logic lives in
10
+ * `./mesh-link.js`, persistence in `./machines.js`. This module never writes
11
+ * the mesh-link status table: `machineStatus()` is imported READ-ONLY and
12
+ * joined into GET rows by id (absent → `'pending'`).
13
+ *
14
+ * One exact route, body/method dispatched:
15
+ * GET /api/superd/machines → `{self, machines}` (public row
16
+ * projection — NEVER token/cookie,
17
+ * D1 + Task 2 review's deferred
18
+ * finding: the row carries two
19
+ * secrets and the wire carries
20
+ * none; since mesh P3 5a (I2) a
21
+ * row may also carry
22
+ * `versionWarning` — additive,
23
+ * present only when the peer's
24
+ * superd version differs from
25
+ * ours by more than patch)
26
+ * POST /api/superd/machines → `{op?: 'reprobe', ...}`:
27
+ * op:'reprobe' + {id} → `{probe: ProbeResult}` (the
28
+ * brief's `/machines/reprobe` path
29
+ * variant is FOLDED here because
30
+ * connection.fetch matches by exact
31
+ * path — one registration, body
32
+ * dispatch; documented per Task 3
33
+ * controller amendment)
34
+ * otherwise `{name, label?, tokenUrl}` → pairMachine → 201 `{machine}`;
35
+ * any failure → 400 `{error}` (the
36
+ * message is mesh-link's own,
37
+ * already token-redacted)
38
+ * DELETE /api/superd/machines?id=<m4> → `{removed: true|false}`
39
+ * DELETE /api/superd/machines?id=<m4> → `{removed: true|false}`; a successful
40
+ * removal ALSO announces `emitMachineRemoved`
41
+ * on mesh-link's federation bus (mesh P2
42
+ * Task 3) — the registrar defederates the
43
+ * machine's rows; this module stays
44
+ * route+handler only (no mesh-federate import)
45
+ * `DELETE` note: upstream's declared `ConnectionFetchMethod` union is
46
+ * `GET | HEAD | POST`, but the runtime route table dispatches through an
47
+ * untyped `methods.has(method)` set and `assertFetchRoute` never inspects
48
+ * method names (packages/client/connection/src/rpc-host.ts, verified against
49
+ * dsh-v0.1.7-rc.1) — DELETE rides the same exact-path dispatch. If a future
50
+ * upstream tightens that, remove folds into a POST op like reprobe.
51
+ */
52
+ import type { Context } from '@deepseek-ai/cordis';
53
+ import { type A2aIngest } from './a2a-ingest.js';
54
+ import type { A2aLogFn } from './a2a-log.js';
55
+ /** The shared-channel Fetch route path this API owns. */
56
+ export declare const MESH_API_BASE = "/api/superd/machines";
57
+ /** Injection seam: the hub home (registry + pairing live under it) and an
58
+ * optional fetch override for tests. `home` is resolved by the caller
59
+ * (gateway.ts passes `resolveDshHome()` at registration time); this module
60
+ * stays free of home-resolution logic. */
61
+ export interface MeshDeps {
62
+ home: string;
63
+ fetchImpl?: typeof fetch;
64
+ }
65
+ /** The GET/POST row shape as it crosses the wire — the persisted MachineRow
66
+ * minus its two secrets (`token`, `cookie`), plus the joined status (and,
67
+ * additively since I2/mesh P3 5a, the version-mismatch warning when the
68
+ * mesh-link status entry carries one). */
69
+ export interface MachinePublicRow {
70
+ id: string;
71
+ name: string;
72
+ label: string;
73
+ origin: string;
74
+ enabled: boolean;
75
+ addedAt: number;
76
+ status: string;
77
+ lastProbeAt?: number;
78
+ error?: string;
79
+ versionWarning?: string;
80
+ }
81
+ /**
82
+ * The mesh-machines handler: GET list / POST pair-or-reprobe / DELETE remove.
83
+ * @param request - the incoming `/api/superd/machines` request.
84
+ * @param deps - home + optional fetch override (tests inject a fixture).
85
+ * @returns the JSON response.
86
+ */
87
+ export declare function handleMeshMachines(request: Request, deps: MeshDeps): Promise<Response>;
88
+ /**
89
+ * Register the mesh-machines API on the shared `/api` connection channel
90
+ * (behind the standard browser-trust + auth fence). No-op when the
91
+ * `connection` service is absent (e.g. fake-ctx unit tests).
92
+ */
93
+ export declare function registerMeshRpc(ctx: Context, deps: MeshDeps): void;
94
+ /**
95
+ * POST /api/superd/a2a — body = one v1 wire frame `{v: 1, envelope}` → the
96
+ * envelope's receipt (new/message: send receipt; stop: stop receipt;
97
+ * reply/notice: the local delivery outcome). Malformed frames are 400s with
98
+ * the decoder's named reason.
99
+ */
100
+ export declare function handleA2aIngestPost(request: Request, ingest: A2aIngest): Promise<Response>;
101
+ /**
102
+ * GET /api/superd/a2a/outbox?machine=<m4> — claim & clear the buffered
103
+ * envelopes addressed to the claiming machine (declarative claim, D1).
104
+ */
105
+ export declare function handleA2aOutboxGet(request: Request, ingest: A2aIngest): Response;
106
+ /**
107
+ * Register the a2a endpoints (POST /api/superd/a2a + GET .../outbox) on the
108
+ * shared `/api` connection channel — same fence as the machines route. The
109
+ * ingest instance is SHARED (R4): the initiator service's self-routing leg
110
+ * calls the very same handler these routes call. No-op when `connection` is
111
+ * absent.
112
+ */
113
+ export declare function registerA2aRpc(ctx: Context, ingest: A2aIngest): void;
114
+ /**
115
+ * POST /api/superd/exec — one-shot `bash -lc <cmd>` on THIS machine (spec
116
+ * §8.1): tail-windowed stdout/stderr, two-stage group kill on timeout. The
117
+ * auth trust boundary is the shared `/api` fence (same as machines/a2a);
118
+ * the initiator-side per-machine knob (`execEnabled`) lives on the CALLER's
119
+ * registry row, not here.
120
+ */
121
+ export declare function handleExecPost(request: Request, log?: A2aLogFn): Promise<Response>;
122
+ /** Register the exec endpoint on the shared `/api` connection channel. */
123
+ export declare function registerExecRpc(ctx: Context, log?: A2aLogFn): void;