@cotal-ai/manager 0.90.1 → 0.92.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 (60) hide show
  1. package/dist/chat-session.d.ts +165 -0
  2. package/dist/chat-session.d.ts.map +1 -0
  3. package/dist/chat-session.js +281 -0
  4. package/dist/chat-session.js.map +1 -0
  5. package/dist/commands.d.ts +3 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +384 -163
  8. package/dist/commands.js.map +1 -1
  9. package/dist/endpoint-evict.d.ts +2 -0
  10. package/dist/endpoint-evict.d.ts.map +1 -1
  11. package/dist/endpoint-evict.js +1 -0
  12. package/dist/endpoint-evict.js.map +1 -1
  13. package/dist/holder-liveness.d.ts +1 -0
  14. package/dist/holder-liveness.d.ts.map +1 -1
  15. package/dist/holder-liveness.js.map +1 -1
  16. package/dist/index.d.ts +6 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/manager-service-contract.d.ts +61 -1
  21. package/dist/manager-service-contract.d.ts.map +1 -1
  22. package/dist/manager-service-contract.js +225 -4
  23. package/dist/manager-service-contract.js.map +1 -1
  24. package/dist/manager.d.ts +106 -3
  25. package/dist/manager.d.ts.map +1 -1
  26. package/dist/manager.js +526 -49
  27. package/dist/manager.js.map +1 -1
  28. package/dist/operator-deregister.d.ts +4 -0
  29. package/dist/operator-deregister.d.ts.map +1 -0
  30. package/dist/operator-deregister.js +75 -0
  31. package/dist/operator-deregister.js.map +1 -0
  32. package/dist/remote-authority.d.ts +4 -2
  33. package/dist/remote-authority.d.ts.map +1 -1
  34. package/dist/remote-authority.js +39 -0
  35. package/dist/remote-authority.js.map +1 -1
  36. package/dist/run-hosting.d.ts +34 -1
  37. package/dist/run-hosting.d.ts.map +1 -1
  38. package/dist/run-hosting.js +369 -15
  39. package/dist/run-hosting.js.map +1 -1
  40. package/dist/service-adapter.d.ts +12 -0
  41. package/dist/service-adapter.d.ts.map +1 -0
  42. package/dist/service-adapter.js +174 -0
  43. package/dist/service-adapter.js.map +1 -0
  44. package/dist/service-child.d.ts +37 -0
  45. package/dist/service-child.d.ts.map +1 -0
  46. package/dist/service-child.js +195 -0
  47. package/dist/service-child.js.map +1 -0
  48. package/dist/service-observation.d.ts +19 -0
  49. package/dist/service-observation.d.ts.map +1 -0
  50. package/dist/service-observation.js +97 -0
  51. package/dist/service-observation.js.map +1 -0
  52. package/dist/static-lifecycle.d.ts +8 -6
  53. package/dist/static-lifecycle.d.ts.map +1 -1
  54. package/dist/static-lifecycle.js +19 -8
  55. package/dist/static-lifecycle.js.map +1 -1
  56. package/dist/vitals.d.ts +37 -0
  57. package/dist/vitals.d.ts.map +1 -0
  58. package/dist/vitals.js +117 -0
  59. package/dist/vitals.js.map +1 -0
  60. package/package.json +13 -12
package/dist/commands.js CHANGED
@@ -1,14 +1,17 @@
1
- import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { Kvm } from "@nats-io/kv";
4
- import { dialerFor, isReachable, epAuthBucket, recordsBucket, instancePinnedInstrumentCapabilities, mintCreds, mintLifecycleUid, newIdentity, remoteManagerActors, remoteManagerRegistrationProof, resolveAuthProvider, standaloneConnectOpts, DEFAULT_SERVER, DEFAULT_SPACE, DEV_OWNER, registry, } from "@cotal-ai/core";
5
- import { authDir, canonicalLocalProcessPath, consumeManagerShutdownIntent, findCotalRoot, getSpaceAuth, hasUserAuthState, isWorkspaceTargetError, listenForLocalStop, loadManagerInstanceIdentity, loadSpaceAccountAuth, parsePositiveIntegerFlag, publishManagerSpareCapability, reclaimDeadPreUpgradeRecord, removePidPair, resolveMeshTarget, soleSpaceOf, userAuthStateDir, workspaceSecretStore, writePidPair, c, MANAGER_DELIVERY_AWARE_MARKER, MANAGER_PIDFILE, refreshRegistrationPolicy, } from "@cotal-ai/workspace";
5
+ import * as prompts from "@clack/prompts";
6
+ import { dialerFor, isReachable, epAuthBucket, recordsBucket, mintCreds, newIdentity, remoteManagerActors, remoteManagerRegistrationProof, resolveAuthProvider, standaloneConnectOpts, DEFAULT_SERVER, DEFAULT_SPACE, DEV_OWNER, registry, parseCommandArgs, token, writePrivateRecord, } from "@cotal-ai/core";
7
+ import { authDir, canonicalLocalProcessPath, claimServicePid, consumeManagerShutdownIntent, findCotalRoot, getSpaceAuth, hasUserAuthState, isWorkspaceTargetError, listenForLocalStop, loadManagerInstanceIdentity, loadSpaceAccountAuth, parsePositiveIntegerFlag, publishManagerSpareCapability, reclaimDeadPreUpgradeRecord, removePidPair, resolveMeshTarget, soleSpaceOf, userAuthStateDir, workspaceSecretStore, writePidPair, c, MANAGER_DELIVERY_AWARE_MARKER, MANAGER_PIDFILE, untilInputEnds, brokerAddressForDisplay, CommandFailure, inputDisabledByFlag, isCommandFailure, jsonFlag, plainFlag, presentationOf, renderReport, resolveNativeServiceTarget, terminalWidth, writeResult, refreshRegistrationPolicy, publishServiceStartup, serviceStartupPath, publishServiceStopReceipt, publishServedContract, protectedCredentialReference, readLocalRecordSource, deprecatedDaemonEntry, } from "@cotal-ai/workspace";
6
8
  import { Manager } from "./manager.js";
9
+ import { acceptManagerServiceStop, armManagerServiceObserver, bindManagerServiceIdentity, managerServiceChild } from "./service-child.js";
7
10
  import { MANAGER_ENDPOINT } from "./manager-service-contract.js";
8
11
  import { makeManagerEndpointHolderEvictor } from "./endpoint-evict.js";
9
12
  import { makeManagerHolderLivenessProbe } from "./holder-liveness.js";
10
13
  import { GateReconcileRefused, reconcileEndpointGate } from "./reconcile-gate.js";
11
- import { InstanceDeregisterRefused, deregisterEndpointInstance, makeInstanceProbe } from "./deregister-instance.js";
14
+ import { endpointDeregisterAction } from "./operator-deregister.js";
12
15
  import { loadRoster } from "./roster.js";
13
16
  import { loadLaunchSpec, materializePersona, launchAgentToStartOpts } from "./launch.js";
14
17
  import {} from "./runtime/index.js";
@@ -17,6 +20,35 @@ import { drainSeats } from "@cotal-ai/seat";
17
20
  import { currentRegistrationProof, loadOrCreateRemoteManagerIdentity, materialCredential, remoteStandingBundleRenewal, remoteManagedAgentEnrollmentMaterial, remoteManagedAgentEnrollmentRequest, remoteManagedAgentPrepareRetirementRequest, remoteManagedAgentRetirementPrepared, remoteManagerAdminAuthorizationRequest, remoteManagerAdminAuthorized, remoteManagerAuthorityRequest, remoteManagerGoalIndexEntries, remoteManagerGoalIndexScanRequest, remoteManagerMaintenanceRequest, remoteManagerMaintenanceResult, remoteRetainedAgentValidationRequest, retainedAgentAuthority, remoteRunHosting } from "./remote-authority.js";
18
21
  import { registerRemoteManagerAuthority } from "./remote-register.js";
19
22
  import { managerClusterArtifacts } from "./manager-service-contract.js";
23
+ /** Read the resume commit token from its protected handoff file once, check it against the
24
+ * digest named on argv, then unlink it. Errors name the field only; the value is never echoed. */
25
+ export function readResumeCommitFile(path, sha256) {
26
+ if (!path.startsWith("/") || path.length > 4096 || /[\u0000-\u001f\u007f]/u.test(path) || !/^[0-9a-f]{64}$/.test(sha256))
27
+ throw new CommandFailure("refused", "USAGE_INVALID", "--resume-commit-file needs an absolute protected path and --resume-commit-sha256 its full digest");
28
+ protectedCredentialReference(path);
29
+ const bytes = readLocalRecordSource(path, 128, true);
30
+ if (bytes === undefined)
31
+ throw new CommandFailure("refused", "TARGET_UNAVAILABLE", "the --resume-commit-file handoff is absent; it was not regenerated");
32
+ const value = bytes.toString("utf8");
33
+ if (!/^[a-f0-9]{64}$/.test(value) || createHash("sha256").update(value).digest("hex") !== sha256)
34
+ throw new CommandFailure("refused", "CONFLICT", "the --resume-commit-file handoff does not match --resume-commit-sha256; its contents were not echoed");
35
+ rmSync(path);
36
+ return value;
37
+ }
38
+ /** The resume commit token from one input form only. `supervise` keeps the deprecated argv
39
+ * token for older callers; the native child accepts only the file form. */
40
+ function resumeCommitToken(v, allowArgv) {
41
+ const file = v["resume-commit-file"] !== undefined || v["resume-commit-sha256"] !== undefined;
42
+ if (file && v["resume-commit-token"] !== undefined)
43
+ throw new CommandFailure("refused", "USAGE_INVALID", "use either --resume-commit-token or --resume-commit-file with --resume-commit-sha256, not both");
44
+ if (!allowArgv && v["resume-commit-token"] !== undefined)
45
+ throw new CommandFailure("refused", "USAGE_INVALID", "the native manager child takes the resume commit token only as a protected file");
46
+ if (!file)
47
+ return v["resume-commit-token"];
48
+ if (v["resume-commit-file"] === undefined || v["resume-commit-sha256"] === undefined)
49
+ throw new CommandFailure("refused", "USAGE_INVALID", "--resume-commit-file and --resume-commit-sha256 are required together");
50
+ return readResumeCommitFile(v["resume-commit-file"], v["resume-commit-sha256"]);
51
+ }
20
52
  /**
21
53
  * RECORD THIS PROCESS as the running manager, and un-record it on a clean exit.
22
54
  *
@@ -52,10 +84,11 @@ export function recordManagerPid(root, space) {
52
84
  const markerPath = canonicalLocalProcessPath(MANAGER_DELIVERY_AWARE_MARKER, ctx);
53
85
  const mine = String(process.pid);
54
86
  // #969/#1238: publish the pair by rename so a later teardown never sees a torn pairing.
87
+ // Both records are created 0o600 under fresh names and renamed into place (writePidPair).
55
88
  writePidPair(pidPath, process.pid);
56
89
  // Written together and removed together: the marker proves the LIVE pid is a non-hosting build,
57
- // and it is only meaningful while it names that same pid.
58
- writeFileSync(markerPath, mine);
90
+ // and it is only meaningful while it names that same pid. A symlink at the marker is refused.
91
+ writePrivateRecord(markerPath, mine);
59
92
  return () => {
60
93
  for (const p of [markerPath, pidPath]) {
61
94
  try {
@@ -160,6 +193,13 @@ function stampConsole() {
160
193
  console.log = (...args) => originalLog(new Date().toISOString(), ...args);
161
194
  console.error = (...args) => originalError(new Date().toISOString(), ...args);
162
195
  }
196
+ function superviseExecution(v) {
197
+ if (v.execution !== undefined && v.execution !== "runtime" && v.execution !== "none")
198
+ throw new CommandFailure("refused", "USAGE_INVALID", '--execution must be "runtime" or "none"');
199
+ if (v.execution === "none" && ["runtime", "spawn", "roster", "launch", "resume-attempt", "resume-commit-token", "resume-commit-file", "resume-commit-sha256"].some((key) => v[key] !== undefined))
200
+ throw new CommandFailure("refused", "USAGE_INVALID", "execution: none forbids runtime and startup workload flags");
201
+ return v.execution ?? "runtime";
202
+ }
163
203
  /** Run a manager daemon in this process (the long-lived supervisor), then block.
164
204
  * `pty` ships with the manager; every other runtime needs a registered provider. The published
165
205
  * CLI lazy-loads installed providers, while library roots import their integrations explicitly.
@@ -170,26 +210,35 @@ function stampConsole() {
170
210
  // `--runtime` forces the manager runtime; honored only on the `supervise` path (default
171
211
  // pty). `cmux` gives each teammate its own cmux tab — `cotal supervise --runtime cmux` is
172
212
  // the cmux-tab manager.
173
- async function runManager(args, defaultRuntime) {
213
+ async function runManager(args, defaultRuntime, service) {
174
214
  stampConsole();
175
215
  const v = args.values;
176
- let runtime = defaultRuntime;
216
+ const managerRoot = service?.root ?? findCotalRoot();
217
+ const execution = superviseExecution(v);
218
+ let runtime = execution === "none" ? undefined : defaultRuntime;
177
219
  if (defaultRuntime === "auto" && v.runtime) {
178
220
  runtime = v.runtime;
179
221
  }
180
222
  let target;
181
223
  try {
182
- target = superviseTarget(v);
224
+ target = superviseTarget(v, managerRoot);
225
+ if (execution === "none" && target.remoteUser)
226
+ throw new Error("execution: none on supervise requires a genuine hosting user-auth root, not a participant registration");
183
227
  }
184
228
  catch (e) {
229
+ if (service)
230
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
185
231
  console.error(c.red(`✗ ${e.message}`));
186
232
  process.exit(1);
187
233
  }
188
234
  const { space, server } = target;
235
+ // Claim before hosted registration/issuance or local endpoint initialization. Starting
236
+ // custody proves only this process; no startup/serving phase exists and no contender is adopted.
237
+ const releaseStartingCustody = service ? claimServicePid({ root: managerRoot, space }, MANAGER_PIDFILE) : undefined;
189
238
  let remoteAuthority;
190
239
  if (target.remoteUser) {
191
240
  try {
192
- const root = findCotalRoot();
241
+ const root = managerRoot;
193
242
  const state = loadOrCreateRemoteManagerIdentity(root, space);
194
243
  const provider = resolveAuthProvider();
195
244
  // Every provider call shares this one context: a call that resolved the root again could land on
@@ -275,7 +324,17 @@ async function runManager(args, defaultRuntime) {
275
324
  state, owner: material.owner, registrationProof: retainedRegistrationProof,
276
325
  accountPublicKey: standing.accountPublicKey, processEpoch: registered.processEpoch,
277
326
  requestRunAdmission: (request) => provider.requestRemoteRunAdmission({ ...providerCall, request }),
327
+ requestRunSubmission: async (request) => {
328
+ if (!provider.requestRemoteRunSubmission)
329
+ throw new Error("auth provider does not implement keyed submission protocol");
330
+ return provider.requestRemoteRunSubmission({ ...providerCall, request });
331
+ },
278
332
  requestRunAttempt: (request) => provider.requestRemoteRunAttempt({ ...providerCall, request }),
333
+ requestRunCancel: async (request) => {
334
+ if (!provider.requestRemoteRunCancel)
335
+ throw new Error("auth provider does not implement the observed run-cancel protocol");
336
+ return provider.requestRemoteRunCancel({ ...providerCall, request });
337
+ },
279
338
  requestRunRevoke: async (request) => {
280
339
  if (provider.requestRemoteRunRevoke === undefined)
281
340
  throw new Error(`the registered auth provider "${provider.name}" does not implement closed hosted-run revoke`);
@@ -369,6 +428,8 @@ async function runManager(args, defaultRuntime) {
369
428
  };
370
429
  }
371
430
  catch (e) {
431
+ if (service)
432
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm hosted authority preparation; it was not retried");
372
433
  console.error(c.red(`✗ remote supervision for space "${space}" was refused: ${e.message}. ` +
373
434
  `Foreground \`cotal spawn\` remains available; detached/managed agents require a live host-approved manager-service authority.`));
374
435
  process.exit(1);
@@ -376,14 +437,24 @@ async function runManager(args, defaultRuntime) {
376
437
  }
377
438
  // Parse the roster + launch spec before touching the network — a malformed file should fail fast,
378
439
  // before the manager comes up or any agent is spawned.
379
- const roster = v.roster ? loadRoster(v.roster) : [];
380
- const launchSpec = v.launch ? loadLaunchSpec(v.launch) : undefined;
440
+ const roster = service ? service.roster ?? [] : v.roster ? loadRoster(v.roster) : [];
441
+ const launchSpec = service ? service.launchSpec : v.launch ? loadLaunchSpec(v.launch) : undefined;
381
442
  if (v["resume-attempt"] && (v.roster || v.launch || v.spawn)) {
382
443
  console.error(c.red("✗ --resume-attempt cannot be combined with --roster, --launch, or --spawn; retained agents resume only through the attempt-bound admin control op"));
383
444
  process.exit(1);
384
445
  }
385
- if (v["resume-commit-token"] && !v["resume-attempt"]) {
386
- console.error(c.red("✗ --resume-commit-token requires --resume-attempt"));
446
+ if ((v["resume-commit-token"] || v["resume-commit-file"] || v["resume-commit-sha256"]) && !v["resume-attempt"]) {
447
+ console.error(c.red("✗ a resume commit token requires --resume-attempt"));
448
+ process.exit(1);
449
+ }
450
+ let resumeDurableCommitToken;
451
+ try {
452
+ resumeDurableCommitToken = resumeCommitToken(v, !service);
453
+ }
454
+ catch (e) {
455
+ if (service)
456
+ throw e;
457
+ console.error(c.red(`✗ ${e.message}`));
387
458
  process.exit(1);
388
459
  }
389
460
  // The registration policy is read AFTER every local refusal above and before the first dial: a
@@ -394,6 +465,8 @@ async function runManager(args, defaultRuntime) {
394
465
  eventsRequired = (await refreshRegistrationPolicy(target))?.events === "required";
395
466
  }
396
467
  catch (e) {
468
+ if (service)
469
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
397
470
  console.error(c.red(`✗ ${e.message}`));
398
471
  process.exit(1);
399
472
  }
@@ -414,6 +487,8 @@ async function runManager(args, defaultRuntime) {
414
487
  maxSessions = parsePositiveIntegerFlag("--max-sessions", v["max-sessions"]);
415
488
  }
416
489
  catch (e) {
490
+ if (service)
491
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
417
492
  console.error(c.red(`✗ ${e.message}`));
418
493
  process.exit(1);
419
494
  }
@@ -430,40 +505,66 @@ async function runManager(args, defaultRuntime) {
430
505
  servers: server,
431
506
  eventsRequired,
432
507
  runtime,
508
+ execution,
433
509
  consolePort,
434
510
  wsPort,
435
511
  attachHost,
436
512
  maxSessions,
437
513
  installedExtensions: true,
438
514
  resumeAttemptId: v["resume-attempt"],
439
- resumeDurableCommitToken: v["resume-commit-token"],
515
+ resumeDurableCommitToken,
440
516
  remoteAuthority,
517
+ ...(service ? { workspaceRoot: managerRoot, onServiceIdentity: bindManagerServiceIdentity(service) } : {}),
441
518
  });
442
519
  }
443
520
  catch (e) {
521
+ if (service)
522
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
444
523
  console.error(c.red(`✗ ${e.message}`));
445
524
  process.exit(1);
446
525
  }
447
- const managerRoot = findCotalRoot();
448
526
  const managerContext = { root: managerRoot, space };
449
- let releasePidRecord;
527
+ // Until the settled record exists, a release only drops a managed child's exact starting slot.
528
+ // A failed start or a stop that wins the start leaves no settled legacy record behind.
529
+ let releaseSettledRecord;
530
+ const releasePidRecord = () => { releaseSettledRecord?.(); releaseStartingCustody?.(); };
450
531
  // Signals and cooperative requests share the sparing default. Only the process-bound
451
- // `cotal down --with-agents` intent authorizes an agent reap.
532
+ // `cotal down --with-agents` intent authorizes an agent reap; a managed child accepts only its
533
+ // request-bound native stop handoff.
452
534
  let shuttingDown = false;
453
535
  // A start that failed under a pending stop leaves the exit to its own error once that stop settles.
454
536
  let startFailed = false;
455
537
  let stopped;
538
+ let disposeObserver;
456
539
  const shutdown = () => {
457
540
  if (shuttingDown)
458
541
  return;
542
+ let request;
543
+ let withAgents;
544
+ if (service) {
545
+ try {
546
+ request = acceptManagerServiceStop(mgr, service);
547
+ withAgents = request.withAgents;
548
+ }
549
+ catch {
550
+ console.error(c.yellow("! the managed manager stop handoff was not accepted; inspect its original request and phase records"));
551
+ return;
552
+ }
553
+ }
554
+ else {
555
+ const decision = consumeManagerShutdownIntent({ root: managerRoot, space });
556
+ if (decision.warning)
557
+ console.error(c.yellow(`! ${decision.warning}`));
558
+ withAgents = decision.withAgents;
559
+ }
459
560
  shuttingDown = true;
460
- const decision = consumeManagerShutdownIntent({ root: managerRoot, space });
461
- if (decision.warning)
462
- console.error(c.yellow(`! ${decision.warning}`));
463
- stopped = mgr.stop({ withAgents: decision.withAgents })
561
+ disposeObserver?.();
562
+ stopped = mgr.stop({ withAgents })
464
563
  .then(() => {
465
564
  publishManagerSpareCapability(managerContext, false);
466
- releasePidRecord?.();
565
+ releasePidRecord();
566
+ if (service && request)
567
+ publishServiceStopReceipt(service.context, request, "completed");
467
568
  if (!startFailed)
468
569
  process.exit(0);
469
570
  })
@@ -472,7 +573,17 @@ async function runManager(args, defaultRuntime) {
472
573
  // under this instance's lease. A record removed under a failed stop is the orphan defect the
473
574
  // pidfile contract exists to prevent. The process still exits: stop() has closed what it could,
474
575
  // and the shuttingDown latch would otherwise leave it deaf to every later signal (#2290).
475
- console.error(c.red(`✗ ${e.message}`));
576
+ if (service) {
577
+ if (request) {
578
+ try {
579
+ publishServiceStopReceipt(service.context, request, "failed");
580
+ }
581
+ catch { /* Missing terminal evidence stays unknown to the original controller. */ }
582
+ }
583
+ console.error(c.red("✗ the native managed manager stop did not confirm completion; its phase evidence was retained"));
584
+ }
585
+ else
586
+ console.error(c.red(`✗ ${e.message}`));
476
587
  if (!startFailed)
477
588
  process.exit(1);
478
589
  });
@@ -502,13 +613,22 @@ async function runManager(args, defaultRuntime) {
502
613
  if (shuttingDown)
503
614
  return;
504
615
  // The control capability precedes the pid record, so a stop cannot observe a ready manager
505
- // before its cooperative endpoint exists. A failed start still publishes no pid record.
506
- releasePidRecord = recordManagerPid(managerRoot, space);
616
+ // before its cooperative endpoint exists. A failed start still publishes no pid record. A
617
+ // managed child already owns its exact starting PID slot; it settles that same process here.
618
+ releaseSettledRecord = recordManagerPid(managerRoot, space);
507
619
  publishManagerSpareCapability(managerContext, mgr.spareSeats);
508
- console.log(c.green("✓ manager up") +
509
- c.dim(` (space ${space} · ${mgr.runtimeKind})`) +
510
- `\n console: ${mgr.consoleUrl}` +
511
- c.dim("\n spawn: cotal spawn --detach <persona> · stop: cotal stop --name <n> (Ctrl-C to shut down)"));
620
+ if (!service)
621
+ console.log(c.green("✓ manager up") +
622
+ c.dim(` (space ${space} · ${mgr.runtimeKind})`) +
623
+ `\n console: ${mgr.consoleUrl}` +
624
+ c.dim("\n spawn: cotal spawn --detach <persona> · stop: cotal stop --name <n> (Ctrl-C to shut down)"));
625
+ if (service) {
626
+ // The served-contract binding precedes the serving receipt, so a serving child always has it.
627
+ const served = managerClusterArtifacts();
628
+ publishServedContract(service.context, { endpoint: MANAGER_ENDPOINT, clusterDigest: served.closureDigest, document: served.document });
629
+ publishServiceStartup(serviceStartupPath(service.context), { phase: "manager-serving", ...service.context });
630
+ disposeObserver = armManagerServiceObserver(mgr, service);
631
+ }
512
632
  // Declarative boot: bring up each rostered agent through the same spawn path as a detached spawn.
513
633
  // A failed entry is logged but non-fatal — healthy agents stay up and the operator can
514
634
  // fix the roster without the supervisor crash-looping.
@@ -548,7 +668,7 @@ async function runManager(args, defaultRuntime) {
548
668
  // transient persona, then spawn it with its resolved ACLs/identity. Staggered like `--spawn` so
549
669
  // heavy cold-starts don't pile up. A failed entry is logged, non-fatal — healthy agents stay up.
550
670
  if (launchSpec) {
551
- const root = findCotalRoot();
671
+ const root = managerRoot;
552
672
  for (let i = 0; i < launchSpec.agents.length; i++) {
553
673
  const la = launchSpec.agents[i];
554
674
  let configPath;
@@ -574,8 +694,33 @@ async function runManager(args, defaultRuntime) {
574
694
  }
575
695
  await new Promise(() => { });
576
696
  }
697
+ function validateMaintenanceArguments(args, mode) {
698
+ presentationOf(args.values);
699
+ if (args.positionals.length)
700
+ throw new CommandFailure("refused", "USAGE_INVALID", `manager ${mode} takes named options, not additional positional targets`);
701
+ if (mode === "seats" && !args.values.drain && (args.values.yes !== undefined || args.values["dry-run"] !== undefined))
702
+ throw new CommandFailure("refused", "USAGE_INVALID", "manager seats --yes and --dry-run require --drain");
703
+ for (const key of mode === "reconcile" ? ["endpoint", "instance"] : []) {
704
+ const value = args.values[key];
705
+ if (value !== undefined && (typeof value !== "string" || !value || value.length > 200 || token(value) !== value))
706
+ throw new CommandFailure("refused", "USAGE_INVALID", `--${key} needs one full native token; prefixes and normalized replacements are not used`);
707
+ }
708
+ }
709
+ async function confirmMaintenance(args, target, review, question) {
710
+ if (args.values.yes === true)
711
+ return true;
712
+ const presentation = presentationOf(args.values);
713
+ if (presentation === "json" || !process.stdin.isTTY || !process.stdout.isTTY || inputDisabledByFlag() || process.env.COTAL_NO_PROMPT === "1")
714
+ throw new CommandFailure("refused", "USAGE_INVALID", "manager maintenance requires --yes outside an interactive terminal", { target, remedy: "inspect the same operation with --dry-run before confirming its scope" });
715
+ console.log(renderReport(review, presentation, terminalWidth(process.stdout)));
716
+ const answer = await untilInputEnds((signal) => prompts.confirm({ message: question, initialValue: false, signal }));
717
+ if (!prompts.isCancel(answer) && answer)
718
+ return true;
719
+ writeResult(presentation, { command: "manager", target, result: { kind: "manager-maintenance-cancelled", changed: false }, noOp: true }, () => ({ sections: [{ title: "Manager maintenance cancelled", blocks: [{ kind: "lines", lines: ["The maintenance action was not submitted. No repair or seat drain was performed."] }] }] }));
720
+ return false;
721
+ }
577
722
  /**
578
- * `cotal reconcile-gate` — the guarded exit from an issuance gate left FROZEN by a crashed
723
+ * `cotal manager reconcile` — the guarded exit from an issuance gate left FROZEN by a crashed
579
724
  * re-registration whose freeze-holder is dead (#391).
580
725
  *
581
726
  * WHY THIS IS A CLI COMMAND AND NOT AN ADMIN VERB ON THE MANAGER ENDPOINT: the wedged state IS
@@ -586,161 +731,163 @@ async function runManager(args, defaultRuntime) {
586
731
  * Read-only until the guard passes: it observes and probes before it constructs the barrier.
587
732
  */
588
733
  async function runReconcileGate(args) {
734
+ validateMaintenanceArguments(args, "reconcile");
589
735
  const v = args.values;
590
- const space = spaceFor(v);
591
- const servers = v.server ?? DEFAULT_SERVER;
736
+ const presentation = presentationOf(args.values), root = findCotalRoot(), space = spaceFor(v, root);
737
+ const selected = resolveNativeServiceTarget(root, space, v.server);
592
738
  const endpoint = v.endpoint ?? MANAGER_ENDPOINT;
593
- const root = findCotalRoot();
594
- // The instanceId is the manager's PERSISTED identity by default — the same one the crashed
595
- // incarnation registered under. An explicit --instance covers a non-manager endpoint.
596
739
  const instanceId = v.instance ?? loadManagerInstanceIdentity(root, space)?.instanceId;
597
- if (!instanceId)
598
- throw new Error(`no persisted manager instance identity under ${root} for space "${space}" — pass --instance <id> for a non-manager endpoint`);
599
- const auth = await getSpaceAuth(workspaceSecretStore(root), space);
740
+ if (!instanceId || instanceId.length > 200 || token(instanceId) !== instanceId)
741
+ throw new CommandFailure("refused", "TARGET_NOT_FOUND", "no full persisted manager instance resolves this repair target", { target: { root, space, endpoint }, remedy: "choose --instance with the full native service instance ID from this root" });
742
+ const target = { root: selected.root, space: selected.space, endpoint, instanceId };
743
+ const review = { space: selected.space, sections: [{ title: "Review issuance-gate repair", blocks: [{ kind: "fields", rows: [["Space", space], ["Signing root", root], ["Endpoint", endpoint], ["Full instance", instanceId], ["Broker", brokerAddressForDisplay(selected.server) ?? "private broker address"], ["Frozen registration", "not checked"], ["Holder liveness", "not checked"], ["Effects", "revoke and verify-evict the dead registration family, then reopen its pinned gate"]] }, { kind: "lines", lines: ["The native guard must prove the freeze-holder gone. Unknown liveness refuses. This command does not start a manager or discard the gate. A failed repair may retain partial progress in its native cursor."] }] }] };
744
+ if (args.values["dry-run"]) {
745
+ writeResult(presentation, { command: "manager", target, result: { kind: "manager-gate-repair-preview", dryRun: true, submitted: false, guardEvidence: "not-checked", credentialIssued: false } }, () => review);
746
+ return;
747
+ }
748
+ if (!await confirmMaintenance(args, target, review, "Attempt the displayed gate repair under its native liveness guard?"))
749
+ return;
750
+ let auth;
751
+ try {
752
+ auth = await getSpaceAuth(workspaceSecretStore(root), space);
753
+ }
754
+ catch (error) {
755
+ if (isCommandFailure(error))
756
+ throw error;
757
+ throw new CommandFailure("refused", "DEPENDENCY_UNAVAILABLE", "this root's space signing material could not be read; no gate repair was submitted", { target });
758
+ }
600
759
  if (!auth)
601
- throw new Error(`no space auth for "${space}" here — run this on the mesh root that holds the space's auth material`);
602
- console.error(c.dim(`• reconciling the ${endpoint} issuance gate for ${instanceId} on ${servers} (space ${space})`));
603
- const identity = newIdentity();
604
- const creds = await mintCreds(auth, identity, "endpoint-serve-executor", {
605
- endpointServeExecutor: { endpoint, instanceId },
606
- });
607
- const nc = await dialerFor(servers)({ servers, ...standaloneConnectOpts({ creds, tls: false }), maxReconnectAttempts: 0 });
760
+ throw new CommandFailure("refused", "PERMISSION_DENIED", "gate repair requires this root's native space signing material; no participant or broader caller was substituted", { target });
761
+ let nc;
762
+ try {
763
+ const identity = newIdentity();
764
+ const creds = await mintCreds(auth, identity, "endpoint-serve-executor", { endpointServeExecutor: { endpoint, instanceId } });
765
+ nc = await dialerFor(selected.server)({ servers: selected.server, ...standaloneConnectOpts({ creds, tls: selected.tlsRequired }), maxReconnectAttempts: 0 });
766
+ }
767
+ catch {
768
+ throw new CommandFailure("refused", "DEPENDENCY_UNAVAILABLE", "the scoped repair connection could not be established; no gate repair was submitted and no alternate broker was tried", { target });
769
+ }
608
770
  try {
609
771
  const kv = await new Kvm(nc).open(epAuthBucket(space));
610
772
  const recordsKv = await new Kvm(nc).open(recordsBucket(space));
611
- const report = await reconcileEndpointGate({
612
- kv, space, endpoint, instanceId,
613
- probeHolder: makeManagerHolderLivenessProbe({ space, servers, auth, log: (l) => console.error(c.dim(` ${l}`)) }),
614
- evictHolders: makeManagerEndpointHolderEvictor({ space, servers, auth, log: (l) => console.error(c.dim(` ${l}`)) }),
615
- log: (l) => console.error(` ${l}`),
616
- recordsKv,
773
+ const native = await reconcileEndpointGate({
774
+ kv, space, endpoint, instanceId, recordsKv,
775
+ probeHolder: makeManagerHolderLivenessProbe({ space, servers: selected.server, tlsRequired: selected.tlsRequired, auth, log: () => { } }),
776
+ evictHolders: makeManagerEndpointHolderEvictor({ space, servers: selected.server, tlsRequired: selected.tlsRequired, auth, log: () => { } }),
777
+ log: () => { },
617
778
  });
618
- console.log(c.green(`✓ ${endpoint}/${instanceId}: gate reopened at generation ${report.reopenedAtGeneration}`) +
619
- ` (processEpoch ${report.after.processEpoch}, registrationRevision ${report.after.registrationRevision}; ${report.revoked.length} credential(s) revoked this attempt, ${report.holdersVerifiedThisAttempt.length} holder(s) verify-evicted this attempt, ${report.holdersVerifiedBeforeAttempt.length} already verified, ${report.holdersRemaining.length} remaining; repair cursor ${report.repairCursorCleanup}). Start the manager to let its normal takeover run.`);
779
+ const result = { kind: "manager-gate-repair", opId: native.opId, freezeToken: native.freezeToken, holderPrincipal: native.holderPrincipal, liveness: native.liveness.state, before: native.before, after: native.after, familyRows: native.familyRows, revokedCredentialIds: native.revoked, holdersVerifiedBeforeAttempt: native.holdersVerifiedBeforeAttempt, holdersVerifiedThisAttempt: native.holdersVerifiedThisAttempt, holdersRemaining: native.holdersRemaining, reopenedAtGeneration: native.reopenedAtGeneration, repairCursorCleanup: native.repairCursorCleanup, managerStarted: false };
780
+ writeResult(presentation, { command: "manager", target, result, ...(native.repairCursorCleanup === "retained" ? { completeness: "partial", partial: { message: "The native gate reopened, but its freeze-bound repair cursor was retained. No cursor cleanup success was inferred." } } : {}) }, (row) => ({ sections: [{ title: "Issuance gate reopened", blocks: [{ kind: "fields", rows: [["Space", space], ["Endpoint", endpoint], ["Full instance", instanceId], ["Native operation", row.opId], ["Holder", row.holderPrincipal], ["Liveness", row.liveness], ["Gate generation", String(row.reopenedAtGeneration)], ["Process epoch", String(row.after.processEpoch)], ["Registration revision", String(row.after.registrationRevision)], ["Revoked this attempt", String(row.revokedCredentialIds.length)], ["Verified this attempt", String(row.holdersVerifiedThisAttempt.length)], ["Already verified", String(row.holdersVerifiedBeforeAttempt.length)], ["Unverified holders", String(row.holdersRemaining.length)], ["Repair cursor", row.repairCursorCleanup], ["Manager", "not started"]] }, { kind: "lines", lines: ["This receipt describes the native gate repair. It does not establish that a manager is now running or serving."] }] }] }));
620
781
  }
621
- catch (e) {
622
- // A refusal is the DESIGNED outcome for every state this does not repair, so it prints the
623
- // condition by name — an operator must never have to guess which guard fired.
624
- if (e instanceof GateReconcileRefused) {
625
- console.error(c.red(`✗ refused (${e.condition}): ${e.message}`));
626
- process.exitCode = 2;
627
- return;
628
- }
629
- throw e;
782
+ catch (error) {
783
+ if (isCommandFailure(error))
784
+ throw error;
785
+ const beforeEffects = error instanceof GateReconcileRefused && ["no-gate", "not-frozen", "wrong-op-kind", "holder-alive", "holder-unknown", "liveness-unestablishable"].includes(error.condition);
786
+ // The guard's own sentence names the freeze-holder and the next step for this condition.
787
+ if (beforeEffects)
788
+ throw new CommandFailure("refused", "LIFECYCLE_BLOCKED", `the native gate-repair guard refused (${error.condition}); the repair did not pass its pre-effect guard`, { target: { ...target, condition: error.condition }, native: { code: error.condition, detail: error.message }, remedy: "inspect the selected gate and the delivery liveness oracle; a timeout is not proof that its holder is gone" });
789
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native gate repair did not confirm completion; revocation, eviction or reopen work may have occurred. No mutation was retried", { target: { ...target, ...(error instanceof GateReconcileRefused ? { condition: error.condition } : {}) }, remedy: "re-observe this gate and its operation-bound native repair cursor before another explicit repair" });
630
790
  }
631
791
  finally {
632
792
  await nc.drain().catch(() => nc.close());
633
793
  }
634
794
  }
635
- /**
636
- * `cotal deregister-instance` — remove the service registration of an instance whose host is gone.
637
- *
638
- * WHY A LOCAL OPERATOR COMMAND AND NOT AN ADMIN VERB ON THE ENDPOINT: the record to remove belongs
639
- * to an instance that by definition answers nothing, so there is no one to ask. It runs against the
640
- * records KV with a credential minted from the space seed on this machine, needing nothing from the
641
- * dead instance.
642
- *
643
- * TWO CREDENTIALS, because they are two different authorities and neither implies the other:
644
- * - a `control-caller-privileged` instrument PINNED to the instance, which is what can ask the
645
- * instance's own rail whether anything is there (the guard);
646
- * - an `endpoint-serve-executor` pinned to the same instance, which carries that registration's
647
- * two records keys (the delete). NAMED RESIDUAL: this is the existing registration profile, so
648
- * it also carries the endpoint's governance head and the rest of the serve-mint footprint —
649
- * wider than the two keys used here. It is minted locally by an operator who already holds the
650
- * space seed, is one-shot, and never leaves this command; narrowing the profile is a
651
- * `provision.ts` change with its own review.
652
- */
653
- async function runDeregisterInstance(args) {
654
- const v = args.values;
655
- const space = spaceFor(v);
656
- const servers = v.server ?? DEFAULT_SERVER;
657
- const endpoint = v.endpoint ?? MANAGER_ENDPOINT;
658
- const root = findCotalRoot();
659
- // Same default as `reconcile-gate`: this folder's own persisted manager instance. A FOREIGN
660
- // corpse (another machine's instance, the case this exists for) is named explicitly, and `cotal
661
- // ps` prints the whole id for exactly that.
662
- const instanceId = v.instance ?? loadManagerInstanceIdentity(root, space)?.instanceId;
663
- if (!instanceId)
664
- throw new Error(`no persisted manager instance identity under ${root} for space "${space}" - pass --instance <id> (the whole id, as \`cotal ps\` prints it)`);
665
- const auth = await getSpaceAuth(workspaceSecretStore(root), space);
666
- if (!auth)
667
- throw new Error(`no space auth for "${space}" here - run this on the mesh root that holds the space's auth material`);
668
- console.error(c.dim(`• deregistering ${endpoint}/${instanceId} on ${servers} (space ${space})`));
669
- // The GUARD's credential: a one-shot privileged instrument pinned to this one instance, so its
670
- // describe and its interest probe ride that instance's own `inst` rails and nothing wider.
671
- const probeIdentity = newIdentity();
672
- const probeUid = mintLifecycleUid();
673
- const probeCreds = await mintCreds(auth, probeIdentity, "control-caller-privileged", {
674
- lifecycleUid: probeUid,
675
- endpointCapabilities: instancePinnedInstrumentCapabilities("privileged", instanceId),
676
- });
677
- const probeCaller = { owner: DEV_OWNER, actor: probeIdentity.id, uid: probeUid };
678
- const probeNc = await dialerFor(servers)({ servers, ...standaloneConnectOpts({ creds: probeCreds, tls: false }), maxReconnectAttempts: 0 });
679
- const probeInstance = makeInstanceProbe(probeNc, { space, endpoint, instanceId, caller: probeCaller });
680
- // The DELETE's credential: the executor whose grants name exactly this registration's two
681
- // records keys. Minted only after the connection above exists, and used only after the guard.
682
- const execIdentity = newIdentity();
683
- const execCreds = await mintCreds(auth, execIdentity, "endpoint-serve-executor", {
684
- endpointServeExecutor: { endpoint, instanceId },
685
- });
686
- const execNc = await dialerFor(servers)({ servers, ...standaloneConnectOpts({ creds: execCreds, tls: false }), maxReconnectAttempts: 0 });
795
+ /** List local custodian records; drain only through the native creation-identity guards. */
796
+ async function runSeats(args) {
797
+ validateMaintenanceArguments(args, "seats");
798
+ const root = custodyRoot(), presentation = presentationOf(args.values), drain = args.values.drain === true;
799
+ const target = { scope: "local-custody-store", root };
800
+ const view = (result) => ({ sections: [
801
+ { title: result.preview ? "Review local seat drain" : "Retained local seats", blocks: result.entries.length ? [{ kind: "table", columns: [{ key: "name", label: "Agent" }, { key: "state", label: "Observed state" }, { key: "id", label: "Full seat identity" }, { key: "custodian", label: "Custodian PID" }, { key: "child", label: "Child PID" }], rows: result.entries.map((entry) => ({ name: entry.name ?? "not reported", state: { text: entry.state, tone: entry.state === "refused" ? "failed" : entry.state === "childless" ? "waiting" : "label" }, id: entry.id, custodian: entry.custodianPid === null ? "not reported" : String(entry.custodianPid), child: entry.childPid === null ? "not reported" : String(entry.childPid) })) }] : [{ kind: "lines", lines: ["No retained seat records were found in this local store."] }] },
802
+ { blocks: [{ kind: "fields", rows: [["Store", root], ["Scope", "all safely childless records in this store when the native drain runs"], ["Live children", "kept"], ["Native diagnostic bodies", "omitted"]] }, { kind: "lines", lines: ["A custody record is not agent progress or mesh membership. The native drain re-reads creation identities; the review is not a reservation. A refused drain can include an unconfirmed custodian effect."] }] },
803
+ ] });
804
+ const project = (entries) => entries.map((entry) => ({ id: entry.id, name: entry.name ?? null, state: entry.state, custodianPid: entry.custodianPid ?? null, childPid: entry.childPid ?? null }));
805
+ let before;
687
806
  try {
688
- const kv = await new Kvm(execNc).open(recordsBucket(space));
689
- const authKv = await new Kvm(execNc).open(epAuthBucket(space));
690
- const report = await deregisterEndpointInstance({
691
- kv, authKv, endpoint, instanceId, probeInstance, log: (l) => console.error(` ${l}`),
692
- });
693
- console.log(c.green(`✓ ${endpoint}/${instanceId} deregistered`) +
694
- ` (spec revision ${report.removedSpecRevision}${report.removedStatusRevision !== undefined ? `, status revision ${report.removedStatusRevision}` : ""}).` +
695
- ` It is gone from every class scatter in space "${space}"; if that host ever comes back, its manager registers again on start.`);
807
+ before = project(await drainSeats(root));
696
808
  }
697
- catch (e) {
698
- // A refusal is the DESIGNED outcome for every state this does not repair, so it prints its
699
- // condition by name rather than leaving the operator to guess which guard fired.
700
- if (e instanceof InstanceDeregisterRefused) {
701
- console.error(c.red(`✗ refused (${e.condition}): ${e.message}`));
702
- process.exitCode = 2;
703
- return;
704
- }
705
- throw e;
809
+ catch {
810
+ throw new CommandFailure("refused", process.platform === "linux" ? "DEPENDENCY_UNAVAILABLE" : "UNSUPPORTED", "the native local custody inventory could not be read; no drain was submitted", { target });
706
811
  }
707
- finally {
708
- await execNc.drain().catch(() => execNc.close());
709
- await probeNc.drain().catch(() => probeNc.close());
812
+ const refused = before.some((entry) => entry.state === "refused");
813
+ if (!drain || args.values["dry-run"] || refused) {
814
+ writeResult(presentation, { command: "manager", target, result: { kind: drain ? "manager-seat-drain-preview" : "manager-seat-census", entries: before, preview: drain, drainSubmitted: false }, ...(refused ? { completeness: "partial", ...(drain ? { failure: new CommandFailure("refused", "LIFECYCLE_BLOCKED", "some native custody records are unresolved; no drain was submitted", { target, remedy: "inspect those full seat IDs before requesting a drain" }) } : { partial: { message: "Some native custody records are unresolved; this is an incomplete local observation, not proof that those seats are gone." } }) } : {}) }, view);
815
+ return;
710
816
  }
711
- }
712
- /**
713
- * `cotal seats`: the custody records an earlier Linux manager left when the pty runtime still
714
- * started a detached custodian per seat (#1391). Read-only unless `--drain`, which retires each seat
715
- * whose agent has exited and keeps every seat whose agent still runs. A refusal exits non-zero: it is
716
- * a record nothing could prove safe to remove, so an operator has to look at it.
717
- */
718
- async function runSeats(args) {
719
- const root = custodyRoot();
720
- const entries = await drainSeats(root, { drain: args.values.drain === true });
721
- if (entries.length === 0) {
722
- console.log(`no seat custody records under ${root}`);
817
+ if (!before.some((entry) => entry.state === "childless")) {
818
+ writeResult(presentation, { command: "manager", target, result: { kind: "manager-seat-drain", entries: before, preview: false, drainSubmitted: false }, noOp: true }, view);
819
+ return;
820
+ }
821
+ if (!await confirmMaintenance(args, target, view({ entries: before, preview: true }), "Drain all safely childless records in the displayed local store? Live children stay."))
723
822
  return;
823
+ let entries;
824
+ try {
825
+ entries = project(await drainSeats(root, { drain: true }));
826
+ }
827
+ catch {
828
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native seat drain did not return its complete census; effects were not retried", { target, remedy: "re-read the local seat census before another drain" });
724
829
  }
725
- for (const e of entries)
726
- console.log(`${e.id} ${e.state.padEnd(10)} ${e.name ?? "-"} ${e.detail}`);
727
- const count = (state) => entries.filter((e) => e.state === state).length;
728
- console.error(c.dim(`• ${entries.length} record(s) under ${root}: ${count("live-child")} live-child, ${count("childless")} childless, ${count("drained")} drained, ${count("refused")} refused`));
729
- if (count("refused") > 0)
730
- process.exitCode = 1;
830
+ const incomplete = entries.some((entry) => entry.state === "refused"), changed = entries.filter((entry) => entry.state === "drained").map((entry) => ({ kind: "seat-record-drained", id: entry.id }));
831
+ writeResult(presentation, { command: "manager", target, result: { kind: "manager-seat-drain", entries, preview: false, drainSubmitted: true }, ...(incomplete ? { completeness: "partial", failure: new CommandFailure("unknown", "OPERATION_UNKNOWN", "some native drains did not confirm completion; confirmed removals are retained, but an unresolved custodian effect was not treated as unchanged", { target, changed, remedy: "re-read the full seat IDs; no drain was retried" }) } : { noOp: changed.length === 0 }) }, view);
731
832
  }
833
+ const maintenanceOutputFlags = [jsonFlag, plainFlag,
834
+ { name: "yes", short: "y", type: "boolean", description: "confirm the selected guarded gate repair or childless-seat drain" },
835
+ { name: "dry-run", type: "boolean", description: "review the selected maintenance scope without submitting repair or drain effects" },
836
+ ];
732
837
  /** The manager's commands: the `supervise` daemon runner, and the guarded `reconcile-gate` repair.
733
838
  * Self-registered on import; the `cotal` binary resolves them from the registry. */
734
839
  const managerCommands = [
735
840
  {
736
841
  kind: "command",
737
- name: "supervise",
842
+ name: "__service-manager",
843
+ hidden: true,
844
+ prepareMeshTarget: false,
845
+ group: "Manager",
846
+ summary: "native manager child with protected launch identity and request-bound stop receipts",
847
+ flags: [
848
+ { name: "root", type: "string", description: "explicit registered mesh root" },
849
+ { name: "space", type: "string", description: "explicit registered space" },
850
+ { name: "server", type: "string", description: "registered broker URL without credentials" },
851
+ { name: "runtime", type: "string", description: "explicit native seat runtime" },
852
+ { name: "console-port", type: "string", description: "explicit native console port; zero allocates a port" },
853
+ { name: "console-host", type: "string", description: "explicit native console bind host" },
854
+ { name: "ws-port", type: "string", description: "the prepared broker websocket port" },
855
+ { name: "max-sessions", type: "string", description: "explicit positive native session ceiling" },
856
+ { name: "launch", type: "string", description: "private native launch spec for this selected space" },
857
+ { name: "launch-sha256", type: "string", description: "full digest pinned by the saved service definition" },
858
+ { name: "roster", type: "string", description: "absolute roster file whose agents start at launch" },
859
+ { name: "service-instance", type: "string", description: "full controller-assigned instance ID" },
860
+ { name: "launch-request", type: "string", description: "full original launch request ID" },
861
+ { name: "state-directory", type: "string", description: "owned private launch state directory" },
862
+ { name: "resume-attempt", type: "string", description: "maintenance resume attempt this launch serves" },
863
+ { name: "resume-commit-file", type: "string", description: "protected one-read resume commit handoff file" },
864
+ { name: "resume-commit-sha256", type: "string", description: "full digest of the resume commit handoff" },
865
+ ],
866
+ validate: (args) => { managerServiceChild(args); },
867
+ requiredExtensions: (args) => typeof args.values.runtime === "string" && args.values.runtime !== "pty" ? [{ kind: "runtime", name: args.values.runtime }] : [],
868
+ run: async (args) => {
869
+ const service = managerServiceChild(args);
870
+ try {
871
+ await runManager(args, "auto", service);
872
+ }
873
+ catch {
874
+ throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not establish its complete startup receipt; no replacement was launched and no diagnostic body was copied to stdout");
875
+ }
876
+ },
877
+ },
878
+ {
879
+ kind: "command",
880
+ // The manager daemon `up` launches. `supervise` is the same entry for persisted callers, plus the
881
+ // deprecation warning; help and completion list neither.
882
+ name: "__daemon-manager",
883
+ hidden: true,
738
884
  group: "Manager",
739
885
  summary: "run a manager - [--runtime <name>] (default pty; extension runtimes are explicit-only) [--space <s>] [--server <url>] [--console-port <n>] [--max-sessions <n>] [--roster <file>] [--launch <spec>] [--resume-attempt <id>]",
740
886
  flags: [
741
887
  { name: "space", type: "string", value: "<s>", description: "space to supervise (default: this folder's auth space; env COTAL_SPACE when unset)" },
742
888
  { name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh; env COTAL_SERVER when unset)" },
743
889
  { name: "runtime", type: "string", value: "<name>", description: "agent runtime (default pty; others come from installed extensions)" },
890
+ { name: "execution", type: "string", value: "<runtime|none>", description: "runtime by default; none requires a fresh hosting user-auth control instance and no runtime or startup workload" },
744
891
  { name: "console-port", type: "string", value: "<n>", description: "protocol-console port" },
745
892
  { name: "console-host", type: "string", value: "<host>", description: "bind host for the console endpoint (default: loopback)" },
746
893
  { name: "ws-port", type: "string", value: "<n>", description: "broker ws listener port for the console session client" },
@@ -748,10 +895,15 @@ const managerCommands = [
748
895
  { name: "roster", type: "string", value: "<file>", description: "declarative roster to boot at startup" },
749
896
  { name: "launch", type: "string", value: "<spec>", description: "resolved mesh-manifest launch spec (cotal up -f / spawn -f)" },
750
897
  { name: "resume-attempt", type: "string", value: "<id>", description: "maintenance restore attempt accepted by resumePreserved" },
751
- { name: "resume-commit-token", type: "string", value: "<token>", description: "durable resume commit evidence for crash recovery" },
898
+ { name: "resume-commit-token", type: "string", value: "<token>", description: "deprecated: durable resume commit evidence on argv; prefer --resume-commit-file" },
899
+ { name: "resume-commit-file", type: "string", value: "<path>", description: "protected one-read resume commit handoff file" },
900
+ { name: "resume-commit-sha256", type: "string", value: "<hex>", description: "full digest of the resume commit handoff" },
752
901
  { name: "spawn", type: "string", value: "<names>", description: "comma-separated personas to pre-spawn at startup" },
753
902
  ],
903
+ validate: (args) => { superviseExecution(args.values); },
754
904
  requiredExtensions: (args) => {
905
+ if (superviseExecution(args.values) === "none")
906
+ return [];
755
907
  const runtime = args.values.runtime;
756
908
  return typeof runtime === "string" && runtime !== "pty" ? [{ kind: "runtime", name: runtime }] : [];
757
909
  },
@@ -761,8 +913,11 @@ const managerCommands = [
761
913
  kind: "command",
762
914
  name: "reconcile-gate",
763
915
  group: "Manager",
916
+ outputs: ["json", "plain"],
917
+ prepareMeshTarget: false,
918
+ validate: (args) => validateMaintenanceArguments(args, "reconcile"),
764
919
  summary: "reconcile an issuance gate left frozen by a crashed re-registration when boot cannot self-heal it - verifies the freeze-holder is gone, then completes the dead op (refuses if it is alive or unprovable) [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]",
765
- flags: [
920
+ flags: [...maintenanceOutputFlags,
766
921
  { name: "space", type: "string", value: "<s>", description: "space to reconcile in (default: this folder's auth space)" },
767
922
  { name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh)" },
768
923
  { name: "endpoint", type: "string", value: "<e>", description: `endpoint whose gate is frozen (default: ${MANAGER_ENDPOINT})` },
@@ -773,24 +928,90 @@ const managerCommands = [
773
928
  {
774
929
  kind: "command",
775
930
  name: "deregister-instance",
776
- group: "Manager",
777
- summary: "remove the service registration of an instance whose host is gone - removes only what the broker affirms gone (nothing subscribed on its rail), and refuses if it answers or is merely quiet [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]",
778
- flags: [
931
+ group: "Manager", hidden: true, reads: true, prepareMeshTarget: false,
932
+ summary: "legacy spelling; use endpoint deregister <class> <full-instance-id>",
933
+ outputs: ["json", "plain"],
934
+ flags: [jsonFlag, plainFlag,
779
935
  { name: "space", type: "string", value: "<s>", description: "space the instance is registered in (default: this folder's auth space)" },
780
936
  { name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh)" },
781
937
  { name: "endpoint", type: "string", value: "<e>", description: `endpoint the instance serves (default: ${MANAGER_ENDPOINT})` },
782
938
  { name: "instance", type: "string", value: "<id>", description: "instance id to deregister, the whole id as `cotal ps` prints it (default: this folder's persisted manager instance)" },
783
939
  ],
784
- run: runDeregisterInstance,
940
+ validate: () => { throw new CommandFailure("refused", "USAGE_INVALID", "the legacy deregistration spelling no longer executes", { remedy: "use cotal endpoint deregister <class> <full-instance-id> [--space <space>] [--yes|--dry-run]" }); },
941
+ run: async () => { throw new CommandFailure("refused", "USAGE_INVALID", "use cotal endpoint deregister <class> <full-instance-id> instead"); },
785
942
  },
786
943
  {
787
944
  kind: "command",
788
945
  name: "seats",
789
946
  group: "Manager",
790
947
  summary: "list the pty seat custodians an earlier Linux manager left on this machine - [--drain] retires each seat whose agent has exited and never signals one whose agent still runs",
791
- flags: [{ name: "drain", type: "boolean", description: "retire every seat whose agent has exited; a seat whose agent still runs is kept" }],
948
+ outputs: ["json", "plain"],
949
+ reads: true,
950
+ prepareMeshTarget: false,
951
+ flags: [...maintenanceOutputFlags,
952
+ { name: "drain", type: "boolean", description: "retire safely childless records in this local store; live children stay" },
953
+ ],
954
+ validate: (args) => validateMaintenanceArguments(args, "seats"),
792
955
  run: runSeats,
793
956
  },
794
957
  ];
795
- registry.register(...managerCommands);
958
+ const maintenance = [
959
+ { name: "reconcile", source: "reconcile-gate", summary: "repair an owned manager issuance gate from its mesh root" },
960
+ { name: "seats", source: "seats", summary: "inspect retained local seats; --drain removes only childless records" },
961
+ ].map((mode) => {
962
+ const source = managerCommands.find((command) => command.name === mode.source);
963
+ if (!source)
964
+ throw new Error(`manager command ${mode.source} is not registered`);
965
+ const command = { ...source, flags: source.flags?.map((flag) => flag.name === "space" ? { ...flag, short: "s" } : flag) };
966
+ return { ...mode, command };
967
+ });
968
+ const maintenanceFlags = new Map();
969
+ for (const mode of maintenance)
970
+ for (const flag of mode.command.flags ?? []) {
971
+ const shared = maintenanceOutputFlags.some((common) => common.name === flag.name);
972
+ maintenanceFlags.set(flag.name, { ...flag, description: shared ? flag.description : `${mode.name}: ${flag.description ?? ""}` });
973
+ }
974
+ function managerInvocation(args) {
975
+ const parsed = parseCommandArgs(managerGroup, [...args.raw]);
976
+ const mode = parsed.positionals[0];
977
+ if (mode === undefined) {
978
+ if (Object.keys(parsed.values).some((key) => !["json", "plain"].includes(key)))
979
+ throw new CommandFailure("refused", "USAGE_INVALID", "choose manager reconcile or manager seats before its options");
980
+ return undefined;
981
+ }
982
+ const child = maintenance.find((item) => item.name === mode);
983
+ if (!child)
984
+ throw new CommandFailure("refused", "USAGE_INVALID", "manager takes reconcile or seats");
985
+ const index = parsed.positionalIndices?.[0];
986
+ if (index === undefined)
987
+ throw new Error("the manager subcommand has no parsed argument index");
988
+ const words = args.raw.filter((_, position) => position !== index);
989
+ return { command: child.command, args: parseCommandArgs(child.command, words) };
990
+ }
991
+ const managerGroup = {
992
+ kind: "command",
993
+ name: "manager",
994
+ aliases: ["mgr"],
995
+ group: "Manager",
996
+ summary: "inspect retained local seats or repair an owned manager gate",
997
+ usage: "cotal manager [reconcile [--yes|--dry-run] | seats [--drain [--yes|--dry-run]]] [--json|--plain]",
998
+ positionals: "[reconcile | seats]",
999
+ outputs: ["json", "plain"],
1000
+ flags: [...maintenanceFlags.values()],
1001
+ subcommands: maintenance.map((mode) => ({ name: mode.name, summary: mode.summary, flagNames: mode.command.flags?.map((flag) => flag.name), outputs: mode.command.outputs, usage: mode.name === "seats" ? "cotal manager seats [--drain [--yes|--dry-run]] [--json|--plain]" : "cotal manager reconcile [--space <space>] [--server <url>] [--instance <id>] [--yes|--dry-run] [--json|--plain]" })),
1002
+ reads: true,
1003
+ prepareMeshTarget: false,
1004
+ validate: (args) => { presentationOf(args.values); const invocation = managerInvocation(args); return invocation?.command.validate?.(invocation.args); },
1005
+ async run(args) {
1006
+ const invocation = managerInvocation(args);
1007
+ if (invocation)
1008
+ return invocation.command.run(invocation.args);
1009
+ writeResult(presentationOf(args.values), { command: "manager", target: null, result: { kind: "manager-maintenance-commands", operations: maintenance.map(({ name, summary }) => ({ name, summary })), managerStarted: false } }, () => ({ sections: [
1010
+ { title: "Manager maintenance", blocks: [{ kind: "commands", items: maintenance.map((mode) => ({ command: `cotal mgr ${mode.name}`, description: mode.summary })) }] },
1011
+ { blocks: [{ kind: "lines", lines: [{ text: "These commands start no manager. Repair and drain default to cancellation; scripted maintenance requires --yes. Preview the scope with --dry-run.", tone: "hint" }] }] },
1012
+ ] }));
1013
+ },
1014
+ };
1015
+ const daemonManager = managerCommands.find((command) => command.name === "__daemon-manager");
1016
+ registry.register(...managerCommands.map((command) => command.name === "seats" || command.name === "reconcile-gate" ? { ...command, hidden: true } : command), deprecatedDaemonEntry("supervise", daemonManager), managerGroup, endpointDeregisterAction);
796
1017
  //# sourceMappingURL=commands.js.map