@serfab/cadre-host 0.13.0 → 1.0.0

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 (62) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/host.js +9 -1
  3. package/dist/bin/host.js.map +1 -1
  4. package/dist/donation/__tests__/fake-orchestrator.js +1 -1
  5. package/dist/donation/__tests__/fake-orchestrator.js.map +1 -1
  6. package/dist/donation/donation-service.d.ts +7 -2
  7. package/dist/donation/donation-service.js +16 -2
  8. package/dist/donation/donation-service.js.map +1 -1
  9. package/dist/donation/donation-supervisor.d.ts +12 -2
  10. package/dist/donation/donation-supervisor.js +25 -2
  11. package/dist/donation/donation-supervisor.js.map +1 -1
  12. package/dist/donation/types.d.ts +2 -1
  13. package/dist/donation/types.js.map +1 -1
  14. package/dist/installer/index.d.ts +9 -1
  15. package/dist/installer/index.js +5 -1
  16. package/dist/installer/index.js.map +1 -1
  17. package/dist/orchestrator/host-process-orchestrator.d.ts +71 -11
  18. package/dist/orchestrator/host-process-orchestrator.js +173 -80
  19. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  20. package/dist/orchestrator/port-allocator.d.ts +44 -2
  21. package/dist/orchestrator/port-allocator.js +63 -5
  22. package/dist/orchestrator/port-allocator.js.map +1 -1
  23. package/dist/orchestrator/port-probe.d.ts +15 -0
  24. package/dist/orchestrator/port-probe.js +22 -0
  25. package/dist/orchestrator/port-probe.js.map +1 -0
  26. package/dist/orchestrator/types.d.ts +13 -1
  27. package/dist/orchestrator/types.js.map +1 -1
  28. package/dist/server/routes/bootstrap-node-validation.d.ts +30 -11
  29. package/dist/server/routes/bootstrap-node-validation.js +32 -13
  30. package/dist/server/routes/bootstrap-node-validation.js.map +1 -1
  31. package/dist/server/server.d.ts +4 -1
  32. package/dist/server/server.js +36 -2
  33. package/dist/server/server.js.map +1 -1
  34. package/dist/ui/assets/{index-C-iks41M.js → index-CEoNRwM_.js} +3 -3
  35. package/dist/ui/assets/{index-C-iks41M.js.map → index-CEoNRwM_.js.map} +1 -1
  36. package/dist/ui/index.html +1 -1
  37. package/package.json +4 -4
  38. package/src/__tests__/orchestrator-env-scrub.test.ts +22 -0
  39. package/src/__tests__/orchestrator-node-identity.test.ts +4 -4
  40. package/src/__tests__/orchestrator-owner.test.ts +45 -2
  41. package/src/__tests__/orchestrator-ports.test.ts +172 -0
  42. package/src/__tests__/orchestrator.test.ts +190 -79
  43. package/src/bin/host.ts +9 -1
  44. package/src/donation/__tests__/donation-service.test.ts +1155 -1068
  45. package/src/donation/__tests__/donation-supervisor.test.ts +548 -510
  46. package/src/donation/__tests__/fake-orchestrator.ts +1 -1
  47. package/src/donation/donation-service.ts +22 -3
  48. package/src/donation/donation-supervisor.ts +25 -2
  49. package/src/donation/types.ts +2 -1
  50. package/src/installer/__tests__/installer.smoke.test.ts +263 -218
  51. package/src/installer/index.ts +327 -314
  52. package/src/orchestrator/host-process-orchestrator.ts +1250 -1143
  53. package/src/orchestrator/port-allocator.ts +148 -91
  54. package/src/orchestrator/port-probe.ts +31 -0
  55. package/src/orchestrator/types.ts +13 -1
  56. package/src/server/__tests__/bootstrap-node-validation.test.ts +12 -8
  57. package/src/server/__tests__/grants-route.test.ts +72 -4
  58. package/src/server/__tests__/nodes-route.test.ts +2 -2
  59. package/src/server/__tests__/publishers.test.ts +1 -1
  60. package/src/server/__tests__/status-route.test.ts +1 -1
  61. package/src/server/routes/bootstrap-node-validation.ts +32 -13
  62. package/src/server/server.ts +157 -122
@@ -63,7 +63,12 @@ export class Installer {
63
63
  // 4. Seed nat.json with the chosen libp2p port + UPnP flag so the
64
64
  // NatService picks them up on first start.
65
65
  seedNatSettings(answers.dataDir, answers.libp2pPort, answers.upnpEnabled);
66
+ const uiUrl = `http://127.0.0.1:${answers.uiPort}/`;
66
67
  // 5. Service-host registration.
68
+ if (opts.noService) {
69
+ log('--no-service: skipping service registration, browser open and enrollment invite');
70
+ return { dataDir: answers.dataDir, uiUrl, configPath: cfgPath };
71
+ }
67
72
  const serviceHost = opts.serviceHost ?? createServiceHost(this.platform);
68
73
  const ctx = {
69
74
  nodePath: opts.nodePath ?? process.execPath,
@@ -73,7 +78,6 @@ export class Installer {
73
78
  };
74
79
  await serviceHost.install(ctx);
75
80
  // 6. Browser open (best-effort).
76
- const uiUrl = `http://127.0.0.1:${answers.uiPort}/`;
77
81
  const shouldOpenBrowser = opts.openBrowser !== false && !opts.nonInteractive && process.stdout.isTTY;
78
82
  if (shouldOpenBrowser) {
79
83
  openBrowser(uiUrl);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installer/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installer/index.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,18 @@
1
1
  import type { Orchestrator, OrchestratorCreateRequest, OrchestratorCreateResult, OrchestratorStats } from '@serfab/cadre-provider';
2
- import { type OwnerSpawnConfig, type HostProcessConfig, type ManagedNodeInfo, type NodeStateListener } from './types.js';
2
+ import { type OwnerSpawnConfig, type HostProcessConfig, type ManagedNodeInfo, type NodePorts, type NodeStateListener } from './types.js';
3
+ /**
4
+ * The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
5
+ * entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
6
+ * every interface.
7
+ *
8
+ * The WebSocket entry is what lets a phone reach the child. A phone's node carries
9
+ * WebSocket, circuit-relay and WebRTC transports but no TCP
10
+ * (`reference-app-rn/src/phone-node-config.ts`), and it listens on nothing, so the
11
+ * phone has to be the one that dials. Naming a `/ws` address is all the child needs:
12
+ * cadre-core adds the WebSocket transport for any listen entry that names one
13
+ * (`resolveTransportOptions` in `cadre-core/src/relay-addrs.ts`).
14
+ */
15
+ export declare function childListenAddrs(ports: Pick<NodePorts, 'p2p' | 'ws'>): string[];
3
16
  /** Fixed friendly id for the admin's owner node. */
4
17
  export declare const OWNER_CONTAINER_ID = "owner";
5
18
  /** Endpoint + bearer for the owner node's loopback admin channel (6.6). */
@@ -168,11 +181,51 @@ export declare class HostProcessOrchestrator implements Orchestrator {
168
181
  * `await` here.
169
182
  */
170
183
  private launchChild;
184
+ /**
185
+ * Refuse to re-spawn `containerId` while a previous child of it may still be
186
+ * running. Both spawn paths call this as their last `await` before the handle
187
+ * drop, so a refusal happens while nothing has been released, no token rotated
188
+ * and no child launched — the caller sees an ordinary failed spawn.
189
+ *
190
+ * Why it exists: the drop hands the old handle's ports back and the launch takes
191
+ * those same ports (`reusedNodePorts`), so a child started over a live one dies
192
+ * on a port clash — but only after `launchChild` has returned success with a
193
+ * fresh seed token. The caller then records a dead child, and the live one
194
+ * refuses the new token.
195
+ *
196
+ * - A handle this process spawned is checked exactly ({@link isHandleLive}); a
197
+ * live one refuses outright.
198
+ * - A handle re-attached from `state.json` whose pid is alive but whose token
199
+ * does not match is either our child still starting or an unrelated process
200
+ * that inherited the pid. The ports decide: if any the re-spawn would take back
201
+ * is already bound, refuse. The pid is never killed on that evidence — it may
202
+ * not be ours.
203
+ *
204
+ * `overrides` are the ports the caller will override on top of the reused ones
205
+ * (the owner node's configured p2p port); `owner` adds the admin channel.
206
+ *
207
+ * NOTE: the probe cannot see a re-attached child still loading its modules —
208
+ * `cadre-cli start` writes its token and only then binds, so a child in that
209
+ * window shows neither, and a re-spawn launched then races it for the ports.
210
+ * Reachable only when the host restarts within a child's first seconds. If it
211
+ * is ever seen, treat a re-attached handle with a live pid as starting for a
212
+ * grace period after its `spawnedAt`.
213
+ *
214
+ * NOTE: the probe holds each port for an instant, so a child binding that port
215
+ * in the same instant fails. It would have had to write its token between the
216
+ * token check above and the probe, against the several awaited steps
217
+ * `cadre-cli start` takes between writing its token and binding; if a re-attached
218
+ * child is ever seen dying on its own port right after a host restart, probe by
219
+ * connecting instead of binding.
220
+ */
221
+ private refuseRespawnOverLiveChild;
222
+ /** Every handle for `containerId`, in map order — the order `dropStaleHandle` drops them in. */
223
+ private handlesFor;
171
224
  /**
172
225
  * Drop any handle left over from a previous spawn of the same `containerId`
173
226
  * and release its ports. Handles are keyed by the per-spawn `dockerId`, so
174
227
  * without this a re-spawn (donated-node respawn) would strand the prior
175
- * handle in the map forever, leaking four ports from a bounded range each
228
+ * handle in the map forever, leaking its ports from a bounded range each
176
229
  * time. Mirrors the same cleanup in `ensureOwnerNode`.
177
230
  *
178
231
  * DO NOT delete the workdir: the identity key and node-local stores
@@ -180,11 +233,12 @@ export declare class HostProcessOrchestrator implements Orchestrator {
180
233
  * the whole reason a respawn comes back as the same node. `launchChild`
181
234
  * reuses the same workdir.
182
235
  *
183
- * NOTE: releasing the ports hands them straight back to the allocator, so a
184
- * re-spawn while the *previous* child is still listening can bind-clash. Every
185
- * caller today re-spawns only a container it has established is not running
186
- * (`DonationService.respawn`, `ensureOwnerNode`). If a caller ever needs to
187
- * replace a live child, stop it first — or hold the ports until its exit.
236
+ * Releasing the ports hands them straight back to the allocator, and the
237
+ * re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
238
+ * re-spawn while the *previous* child is still listening would bind-clash.
239
+ * Callers must therefore never drop a live child, and both do so only after
240
+ * {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
241
+ * a live child must stop it first.
188
242
  *
189
243
  * **The drop is reversible.** It happens before the launch can fail, so the
190
244
  * dropped handles are returned and every caller restores them via
@@ -197,7 +251,7 @@ export declare class HostProcessOrchestrator implements Orchestrator {
197
251
  private dropStaleHandle;
198
252
  /**
199
253
  * Inverse of {@link dropStaleHandle}, for a spawn that then failed: put each
200
- * handle back in the map and re-reserve its four ports, leaving host state
254
+ * handle back in the map and re-reserve its ports, leaving host state
201
255
  * exactly as the failed attempt found it.
202
256
  *
203
257
  * **Precondition: the drop → launch window is synchronous.** Restoring is
@@ -215,9 +269,10 @@ export declare class HostProcessOrchestrator implements Orchestrator {
215
269
  *
216
270
  * NOTE: a restored handle keeps `alive: true` even if its child exited while
217
271
  * it was out of the map (`launchChild`'s exit listener looks the handle up by
218
- * dockerId and finds nothing). Only reachable by re-spawning a container
219
- * whose child is still live, which `dropStaleHandle`'s note above already
220
- * forbids.
272
+ * dockerId and finds nothing). Only reachable by dropping a handle whose child
273
+ * is still live, which {@link refuseRespawnOverLiveChild} refuses before any
274
+ * drop. (Its start-up blind spot concerns re-attached handles only, and those
275
+ * have no exit listener to miss.)
221
276
  */
222
277
  private restoreDroppedHandles;
223
278
  /** A managed node's own directory under `rootDir` — config, log, storage, identity. */
@@ -233,6 +288,11 @@ export declare class HostProcessOrchestrator implements Orchestrator {
233
288
  private releasePorts;
234
289
  stopContainer(dockerId: string): Promise<void>;
235
290
  removeContainer(dockerId: string): Promise<void>;
291
+ /**
292
+ * Whether the child `dockerId` names is running — see {@link isHandleLive}. An
293
+ * id with no handle is not running: without the handle's workdir there is no
294
+ * token to verify a live pid against.
295
+ */
236
296
  isRunning(dockerId: string): Promise<boolean>;
237
297
  getStats(dockerId: string): Promise<OrchestratorStats>;
238
298
  getLogs(dockerId: string, tail?: number): Promise<string>;
@@ -7,10 +7,11 @@ import debug from 'debug';
7
7
  import pidusage from 'pidusage';
8
8
  import { defaultLogPath, rotateOnDisk } from './log-rotator.js';
9
9
  import { ensureNodeIdentity } from './node-identity.js';
10
- import { allocateNodePorts, PortAllocator } from './port-allocator.js';
10
+ import { allocateNodePorts, PortAllocator, releaseNodePorts, reserveNodePorts, reusedNodePorts, } from './port-allocator.js';
11
11
  import { StateStore } from './state-store.js';
12
12
  import { isPidAlive } from './pid-liveness.js';
13
- import { decodeDockerId, encodeDockerId, } from './types.js';
13
+ import { assertPortFree } from './port-probe.js';
14
+ import { encodeDockerId, } from './types.js';
14
15
  const log = debug('cadre:host:orchestrator');
15
16
  const DEFAULTS = {
16
17
  portRangeStart: 10000,
@@ -42,6 +43,38 @@ function scrubbedParentEnv() {
42
43
  }
43
44
  return env;
44
45
  }
46
+ /**
47
+ * The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
48
+ * entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
49
+ * every interface.
50
+ *
51
+ * The WebSocket entry is what lets a phone reach the child. A phone's node carries
52
+ * WebSocket, circuit-relay and WebRTC transports but no TCP
53
+ * (`reference-app-rn/src/phone-node-config.ts`), and it listens on nothing, so the
54
+ * phone has to be the one that dials. Naming a `/ws` address is all the child needs:
55
+ * cadre-core adds the WebSocket transport for any listen entry that names one
56
+ * (`resolveTransportOptions` in `cadre-core/src/relay-addrs.ts`).
57
+ */
58
+ export function childListenAddrs(ports) {
59
+ return [`/ip4/0.0.0.0/tcp/${ports.p2p}`, `/ip4/0.0.0.0/tcp/${ports.ws}/ws`];
60
+ }
61
+ /**
62
+ * Every TCP port a managed child binds, on the address it binds it to: health and
63
+ * metrics (`cadre-cli`'s `HealthServer`) and the libp2p TCP and WebSocket listeners
64
+ * ({@link childListenAddrs}) on every interface, plus the loopback admin channel —
65
+ * for the owner node only, the one child started with `--admin-port`. A key the
66
+ * set lacks (a handle persisted by an older build) is skipped.
67
+ */
68
+ function childPortBindings(ports, owner) {
69
+ const bindings = [];
70
+ for (const port of [ports.health, ports.metrics, ports.p2p, ports.ws]) {
71
+ if (port !== undefined)
72
+ bindings.push({ port, host: '0.0.0.0' });
73
+ }
74
+ if (owner && ports.admin !== undefined)
75
+ bindings.push({ port: ports.admin, host: '127.0.0.1' });
76
+ return bindings;
77
+ }
45
78
  /** Fixed friendly id for the admin's owner node. */
46
79
  export const OWNER_CONTAINER_ID = 'owner';
47
80
  /**
@@ -101,7 +134,6 @@ export class HostProcessOrchestrator {
101
134
  const state = this.stateStore.load();
102
135
  this.ownerConfig = state.ownerConfig;
103
136
  for (const persisted of state.handles) {
104
- const alive = isPidAlive(persisted.pid) && tokenMatches(persisted.workdir, persisted.startupToken);
105
137
  const handle = {
106
138
  containerId: persisted.containerId,
107
139
  dockerId: persisted.dockerId,
@@ -113,8 +145,9 @@ export class HostProcessOrchestrator {
113
145
  partyId: persisted.partyId,
114
146
  profile: persisted.profile,
115
147
  ...(persisted.owner ? { owner: true } : {}),
116
- alive,
148
+ alive: false,
117
149
  };
150
+ handle.alive = isHandleLive(handle);
118
151
  this.handles.set(handle.dockerId, handle);
119
152
  // Always reserve ports — even for dead handles, until the caller cleans them up
120
153
  this.reservePorts(persisted.ports);
@@ -191,8 +224,8 @@ export class HostProcessOrchestrator {
191
224
  // Identity BEFORE any port is reserved: it is the one step here that can
192
225
  // fail on its own (an unreadable or undecodable identity.key throws rather
193
226
  // than silently re-keying), and everything past the allocation is either
194
- // infallible or releases the ports itself. Allocating first would leak four
195
- // ports from a bounded range on every failed provision attempt.
227
+ // infallible or releases the ports itself. Allocating first would leak a
228
+ // node's worth of ports from a bounded range on every failed provision attempt.
196
229
  const identity = await ensureNodeIdentity(workdir);
197
230
  log('container %s identity peerId=%s', request.containerId, identity.peerId);
198
231
  // A node started with pinned owner keys belongs to a FOREIGN cadre (the
@@ -203,8 +236,8 @@ export class HostProcessOrchestrator {
203
236
  const foreignParty = (request.pinnedOwnerKeys?.length ?? 0) > 0;
204
237
  // Only storage-profile nodes participate in strands and thus fan out push
205
238
  // wakes — a transaction-only node need not carry credentials. Resolved
206
- // BEFORE the drop below: it is the last `await` on this path, which is what
207
- // keeps the drop → launch window synchronous (see restoreDroppedHandles).
239
+ // BEFORE the drop below, which keeps the drop → launch window synchronous
240
+ // (see restoreDroppedHandles).
208
241
  const push = request.profile === 'storage' && !foreignParty ? await this.resolvePush() : undefined;
209
242
  // Pinned owner key(s) reach the child via CADRE_OWNER_KEYS (comma-separated);
210
243
  // `cadre-cli start` unions it into its cold-start pinnedKeyTrustPolicy so the
@@ -212,16 +245,17 @@ export class HostProcessOrchestrator {
212
245
  const extraEnv = request.pinnedOwnerKeys?.length
213
246
  ? { CADRE_OWNER_KEYS: request.pinnedOwnerKeys.join(',') }
214
247
  : undefined;
248
+ // The last `await` on this path: checked as close to the drop as it can be.
249
+ await this.refuseRespawnOverLiveChild(request.containerId, {}, false);
215
250
  // Nothing from here to the launch is `await`ed — which is what keeps
216
251
  // `restoreDroppedHandles`'s documented precondition true even though the
217
252
  // `try` now opens further up. Do not add an `await` past this line.
218
253
  dropped = this.dropStaleHandle(request.containerId);
219
- // NOTE: the p2p port is re-allocated per spawn, so a re-spawned donated
220
- // node keeps its peer id but may announce a different port. Recoverable —
221
- // it dials out to its retained bootstrap peers and republishes its own
222
- // CadrePeer row once connected. If that reconnect ever proves too slow,
223
- // pin the p2p port per containerId (see donated-node-respawn-core).
224
- ports = allocateNodePorts(this.portAllocator);
254
+ // A re-spawn comes back on the ports the dropped handle held, not merely the
255
+ // lowest free ones: a requester that dials the node itself (a phone) knows it
256
+ // only by the address it was given. The drop above just released them, and
257
+ // nothing can have taken them since — see reusedNodePorts.
258
+ ports = allocateNodePorts(this.portAllocator, reusedNodePorts(dropped));
225
259
  return this.launchChild({
226
260
  containerId: request.containerId,
227
261
  partyId: request.partyId,
@@ -355,7 +389,7 @@ export class HostProcessOrchestrator {
355
389
  }
356
390
  this.ownerConfig = config;
357
391
  const existing = this.findOwnerHandle();
358
- if (existing && isPidAlive(existing.pid) && tokenMatches(existing.workdir, existing.startupToken)) {
392
+ if (existing && isHandleLive(existing)) {
359
393
  this.persist();
360
394
  return toNodeInfo(existing);
361
395
  }
@@ -365,6 +399,8 @@ export class HostProcessOrchestrator {
365
399
  // Resolved BEFORE the drop below so the drop → launch window stays
366
400
  // synchronous (see restoreDroppedHandles).
367
401
  const push = await this.resolvePush();
402
+ // The last `await` before the drop — same guard as in createContainer.
403
+ await this.refuseRespawnOverLiveChild(OWNER_CONTAINER_ID, { p2p: config.libp2pPort }, true);
368
404
  // NOTE: the short-circuit above matches via `findOwnerHandle` (owner flag OR
369
405
  // containerId), this drop only by containerId. Equivalent today — every
370
406
  // owner handle is launched with OWNER_CONTAINER_ID. If an owner spawn ever
@@ -374,8 +410,10 @@ export class HostProcessOrchestrator {
374
410
  let ports;
375
411
  try {
376
412
  // The owner node must listen on the configured libp2p port so the NAT
377
- // mapping (external → internal) lands on it — hence the p2p override.
378
- ports = allocateNodePorts(this.portAllocator, { p2p: config.libp2pPort });
413
+ // mapping (external → internal) lands on it — hence the p2p override, which
414
+ // wins over the dropped handle's. Every other port comes back as it was, for
415
+ // the same reason as in createContainer.
416
+ ports = allocateNodePorts(this.portAllocator, { ...reusedNodePorts(dropped), p2p: config.libp2pPort });
379
417
  const result = this.launchChild({
380
418
  containerId: OWNER_CONTAINER_ID,
381
419
  partyId: config.partyId,
@@ -493,17 +531,19 @@ export class HostProcessOrchestrator {
493
531
  CADRE_STARTUP_TOKEN: token,
494
532
  CADRE_HEALTH_PORT: String(ports.health),
495
533
  CADRE_METRICS_PORT: String(ports.metrics),
496
- CADRE_LISTEN_ADDRS: `/ip4/0.0.0.0/tcp/${ports.p2p}`,
534
+ CADRE_LISTEN_ADDRS: childListenAddrs(ports).join(','),
497
535
  // NOTE: the scrub above drops every CADRE_* var and `extraEnv` is built only from
498
- // pinnedOwnerKeys, so a managed child advertises only the port assigned to it here
536
+ // pinnedOwnerKeys, so a managed child advertises only the ports assigned to it here
499
537
  // — `CADRE_ANNOUNCE_ADDRS`/`CADRE_APPEND_ANNOUNCE_ADDRS` cannot reach it. Fine while
500
- // children are reached at that port or through a relay; if a host is ever fronted by
501
- // a proxy or DNS name, plumb an announce var through from host config. The port is
502
- // the CHILD'S CONTROL NODE's alone: each strand node the child runs binds the same
503
- // entry with an OS-assigned port instead, since one port cannot be held twice
504
- // (`cadre-core/src/strand-network-config.ts`). A NAT forward of this port therefore
505
- // reaches the control node only, and strand nodes are reached through observed
506
- // addresses or a relay.
538
+ // children are reached at those ports or through a relay; if a host is ever fronted
539
+ // by a proxy or DNS name, plumb an announce var through from host config. The TCP
540
+ // and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand node the
541
+ // child runs binds the same two entries with OS-assigned ports instead, since one
542
+ // port cannot be held twice (`cadre-core/src/strand-network-config.ts`). A NAT
543
+ // forward (the owner node's, via `NatService`) covers the TCP port only, so it
544
+ // reaches the control node over TCP alone; a phone outside the LAN, which needs the
545
+ // WebSocket port, and every strand node are reached through observed addresses or
546
+ // a relay.
507
547
  CADRE_SEED_TOKEN: seedToken,
508
548
  // Pin each child's node-local state (trusted-owner anchor, retained
509
549
  // cold-start dial targets) to its OWN workdir. This is the same value the
@@ -604,11 +644,67 @@ export class HostProcessOrchestrator {
604
644
  p2pPort: ports.p2p,
605
645
  };
606
646
  }
647
+ /**
648
+ * Refuse to re-spawn `containerId` while a previous child of it may still be
649
+ * running. Both spawn paths call this as their last `await` before the handle
650
+ * drop, so a refusal happens while nothing has been released, no token rotated
651
+ * and no child launched — the caller sees an ordinary failed spawn.
652
+ *
653
+ * Why it exists: the drop hands the old handle's ports back and the launch takes
654
+ * those same ports (`reusedNodePorts`), so a child started over a live one dies
655
+ * on a port clash — but only after `launchChild` has returned success with a
656
+ * fresh seed token. The caller then records a dead child, and the live one
657
+ * refuses the new token.
658
+ *
659
+ * - A handle this process spawned is checked exactly ({@link isHandleLive}); a
660
+ * live one refuses outright.
661
+ * - A handle re-attached from `state.json` whose pid is alive but whose token
662
+ * does not match is either our child still starting or an unrelated process
663
+ * that inherited the pid. The ports decide: if any the re-spawn would take back
664
+ * is already bound, refuse. The pid is never killed on that evidence — it may
665
+ * not be ours.
666
+ *
667
+ * `overrides` are the ports the caller will override on top of the reused ones
668
+ * (the owner node's configured p2p port); `owner` adds the admin channel.
669
+ *
670
+ * NOTE: the probe cannot see a re-attached child still loading its modules —
671
+ * `cadre-cli start` writes its token and only then binds, so a child in that
672
+ * window shows neither, and a re-spawn launched then races it for the ports.
673
+ * Reachable only when the host restarts within a child's first seconds. If it
674
+ * is ever seen, treat a re-attached handle with a live pid as starting for a
675
+ * grace period after its `spawnedAt`.
676
+ *
677
+ * NOTE: the probe holds each port for an instant, so a child binding that port
678
+ * in the same instant fails. It would have had to write its token between the
679
+ * token check above and the probe, against the several awaited steps
680
+ * `cadre-cli start` takes between writing its token and binding; if a re-attached
681
+ * child is ever seen dying on its own port right after a host restart, probe by
682
+ * connecting instead of binding.
683
+ */
684
+ async refuseRespawnOverLiveChild(containerId, overrides, owner) {
685
+ const previous = this.handlesFor(containerId);
686
+ if (previous.some(isHandleLive)) {
687
+ throw new Error(`container ${containerId} is still running`);
688
+ }
689
+ if (!previous.some((h) => isPidAlive(h.pid)))
690
+ return;
691
+ const bindings = childPortBindings({ ...reusedNodePorts(previous), ...overrides }, owner);
692
+ try {
693
+ await Promise.all(bindings.map(assertPortFree));
694
+ }
695
+ catch (err) {
696
+ throw new Error(`container ${containerId} may still be running: ${err.message}`, { cause: err });
697
+ }
698
+ }
699
+ /** Every handle for `containerId`, in map order — the order `dropStaleHandle` drops them in. */
700
+ handlesFor(containerId) {
701
+ return [...this.handles.values()].filter((h) => h.containerId === containerId);
702
+ }
607
703
  /**
608
704
  * Drop any handle left over from a previous spawn of the same `containerId`
609
705
  * and release its ports. Handles are keyed by the per-spawn `dockerId`, so
610
706
  * without this a re-spawn (donated-node respawn) would strand the prior
611
- * handle in the map forever, leaking four ports from a bounded range each
707
+ * handle in the map forever, leaking its ports from a bounded range each
612
708
  * time. Mirrors the same cleanup in `ensureOwnerNode`.
613
709
  *
614
710
  * DO NOT delete the workdir: the identity key and node-local stores
@@ -616,11 +712,12 @@ export class HostProcessOrchestrator {
616
712
  * the whole reason a respawn comes back as the same node. `launchChild`
617
713
  * reuses the same workdir.
618
714
  *
619
- * NOTE: releasing the ports hands them straight back to the allocator, so a
620
- * re-spawn while the *previous* child is still listening can bind-clash. Every
621
- * caller today re-spawns only a container it has established is not running
622
- * (`DonationService.respawn`, `ensureOwnerNode`). If a caller ever needs to
623
- * replace a live child, stop it first — or hold the ports until its exit.
715
+ * Releasing the ports hands them straight back to the allocator, and the
716
+ * re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
717
+ * re-spawn while the *previous* child is still listening would bind-clash.
718
+ * Callers must therefore never drop a live child, and both do so only after
719
+ * {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
720
+ * a live child must stop it first.
624
721
  *
625
722
  * **The drop is reversible.** It happens before the launch can fail, so the
626
723
  * dropped handles are returned and every caller restores them via
@@ -631,20 +728,17 @@ export class HostProcessOrchestrator {
631
728
  * gone by then.
632
729
  */
633
730
  dropStaleHandle(containerId) {
634
- const dropped = [];
635
- for (const h of this.handles.values()) {
636
- if (h.containerId !== containerId)
637
- continue;
731
+ const dropped = this.handlesFor(containerId);
732
+ for (const h of dropped) {
638
733
  log('dropping stale handle %s for container %s', h.dockerId, containerId);
639
734
  this.releasePorts(h.ports);
640
735
  this.handles.delete(h.dockerId);
641
- dropped.push(h);
642
736
  }
643
737
  return dropped;
644
738
  }
645
739
  /**
646
740
  * Inverse of {@link dropStaleHandle}, for a spawn that then failed: put each
647
- * handle back in the map and re-reserve its four ports, leaving host state
741
+ * handle back in the map and re-reserve its ports, leaving host state
648
742
  * exactly as the failed attempt found it.
649
743
  *
650
744
  * **Precondition: the drop → launch window is synchronous.** Restoring is
@@ -662,9 +756,10 @@ export class HostProcessOrchestrator {
662
756
  *
663
757
  * NOTE: a restored handle keeps `alive: true` even if its child exited while
664
758
  * it was out of the map (`launchChild`'s exit listener looks the handle up by
665
- * dockerId and finds nothing). Only reachable by re-spawning a container
666
- * whose child is still live, which `dropStaleHandle`'s note above already
667
- * forbids.
759
+ * dockerId and finds nothing). Only reachable by dropping a handle whose child
760
+ * is still live, which {@link refuseRespawnOverLiveChild} refuses before any
761
+ * drop. (Its start-up blind spot concerns re-attached handles only, and those
762
+ * have no exit listener to miss.)
668
763
  */
669
764
  restoreDroppedHandles(dropped) {
670
765
  for (const h of dropped) {
@@ -691,19 +786,10 @@ export class HostProcessOrchestrator {
691
786
  * launch then failed.
692
787
  */
693
788
  reservePorts(ports) {
694
- this.portAllocator.markUsed(ports.health);
695
- this.portAllocator.markUsed(ports.metrics);
696
- this.portAllocator.markUsed(ports.p2p);
697
- // `admin` is absent on handles persisted before this build; markUsed would
698
- // otherwise put `undefined` in the used-set.
699
- if (typeof ports.admin === 'number')
700
- this.portAllocator.markUsed(ports.admin);
789
+ reserveNodePorts(this.portAllocator, ports);
701
790
  }
702
791
  releasePorts(ports) {
703
- this.portAllocator.release(ports.health);
704
- this.portAllocator.release(ports.metrics);
705
- this.portAllocator.release(ports.p2p);
706
- this.portAllocator.release(ports.admin);
792
+ releaseNodePorts(this.portAllocator, ports);
707
793
  }
708
794
  async stopContainer(dockerId) {
709
795
  const handle = this.requireHandle(dockerId);
@@ -770,37 +856,20 @@ export class HostProcessOrchestrator {
770
856
  this.handles.delete(dockerId);
771
857
  this.persist();
772
858
  }
859
+ /**
860
+ * Whether the child `dockerId` names is running — see {@link isHandleLive}. An
861
+ * id with no handle is not running: without the handle's workdir there is no
862
+ * token to verify a live pid against.
863
+ */
773
864
  async isRunning(dockerId) {
774
865
  const handle = this.handles.get(dockerId);
775
- let pid;
776
- let token;
777
- let workdir;
778
- if (handle) {
779
- pid = handle.pid;
780
- token = handle.startupToken;
781
- workdir = handle.workdir;
782
- }
783
- else {
784
- try {
785
- const decoded = decodeDockerId(dockerId);
786
- pid = decoded.pid;
787
- token = decoded.token;
788
- }
789
- catch {
790
- return false;
791
- }
792
- }
793
- if (!isPidAlive(pid)) {
794
- if (handle)
795
- handle.alive = false;
796
- return false;
797
- }
798
- if (workdir === undefined) {
799
- // We have no record — without a workdir we can't verify the token,
800
- // so we must treat this as unknown rather than running.
866
+ if (!handle)
801
867
  return false;
802
- }
803
- return tokenMatches(workdir, token);
868
+ if (isHandleLive(handle))
869
+ return true;
870
+ if (!isPidAlive(handle.pid))
871
+ handle.alive = false;
872
+ return false;
804
873
  }
805
874
  async getStats(dockerId) {
806
875
  const handle = this.requireHandle(dockerId);
@@ -958,6 +1027,30 @@ function resolveCadreCliBin() {
958
1027
  `and listed as a dependency. Underlying error: ${err.message}`, { cause: err });
959
1028
  }
960
1029
  }
1030
+ /**
1031
+ * Whether the process a handle names is still the child it was launched as — the
1032
+ * one liveness rule behind `init`, `isRunning`, `ensureOwnerNode`'s short-circuit
1033
+ * and the pre-spawn guard, so they cannot drift apart.
1034
+ *
1035
+ * A handle this process spawned carries its `ChildProcess`, and that answer is
1036
+ * exact from the moment of spawn: no exit seen, and the pid still answering. (The
1037
+ * pid check covers the moment between the OS ending the process and Node
1038
+ * delivering its exit; the pid cannot have been reused in that moment, since the
1039
+ * OS holds it until Node has reaped the child.) The startup-token file is no use
1040
+ * here — a child writes it only once it is running its own code, so reading it
1041
+ * would report every child dead for its first moments.
1042
+ *
1043
+ * A handle re-attached from `state.json` after a host restart has no
1044
+ * `ChildProcess`, so it falls back to the pid plus that token file, which tells our
1045
+ * child apart from an unrelated process that inherited its pid.
1046
+ */
1047
+ function isHandleLive(handle) {
1048
+ const { child } = handle;
1049
+ if (child) {
1050
+ return child.exitCode === null && child.signalCode === null && isPidAlive(handle.pid);
1051
+ }
1052
+ return isPidAlive(handle.pid) && tokenMatches(handle.workdir, handle.startupToken);
1053
+ }
961
1054
  function tokenMatches(workdir, expected) {
962
1055
  const tokenPath = join(workdir, STARTUP_TOKEN_FILE);
963
1056
  try {