@cotal-ai/manager 0.90.1 → 0.91.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.
- package/dist/chat-session.d.ts +165 -0
- package/dist/chat-session.d.ts.map +1 -0
- package/dist/chat-session.js +281 -0
- package/dist/chat-session.js.map +1 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +374 -163
- package/dist/commands.js.map +1 -1
- package/dist/endpoint-evict.d.ts +2 -0
- package/dist/endpoint-evict.d.ts.map +1 -1
- package/dist/endpoint-evict.js +1 -0
- package/dist/endpoint-evict.js.map +1 -1
- package/dist/holder-liveness.d.ts +1 -0
- package/dist/holder-liveness.d.ts.map +1 -1
- package/dist/holder-liveness.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manager-service-contract.d.ts +52 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +138 -4
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +105 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +515 -49
- package/dist/manager.js.map +1 -1
- package/dist/operator-deregister.d.ts +4 -0
- package/dist/operator-deregister.d.ts.map +1 -0
- package/dist/operator-deregister.js +75 -0
- package/dist/operator-deregister.js.map +1 -0
- package/dist/service-adapter.d.ts +12 -0
- package/dist/service-adapter.d.ts.map +1 -0
- package/dist/service-adapter.js +174 -0
- package/dist/service-adapter.js.map +1 -0
- package/dist/service-child.d.ts +37 -0
- package/dist/service-child.d.ts.map +1 -0
- package/dist/service-child.js +195 -0
- package/dist/service-child.js.map +1 -0
- package/dist/service-observation.d.ts +19 -0
- package/dist/service-observation.d.ts.map +1 -0
- package/dist/service-observation.js +97 -0
- package/dist/service-observation.js.map +1 -0
- package/dist/static-lifecycle.d.ts +8 -6
- package/dist/static-lifecycle.d.ts.map +1 -1
- package/dist/static-lifecycle.js +19 -8
- package/dist/static-lifecycle.js.map +1 -1
- package/package.json +13 -12
package/dist/commands.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
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
|
|
5
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -369,6 +418,8 @@ async function runManager(args, defaultRuntime) {
|
|
|
369
418
|
};
|
|
370
419
|
}
|
|
371
420
|
catch (e) {
|
|
421
|
+
if (service)
|
|
422
|
+
throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm hosted authority preparation; it was not retried");
|
|
372
423
|
console.error(c.red(`✗ remote supervision for space "${space}" was refused: ${e.message}. ` +
|
|
373
424
|
`Foreground \`cotal spawn\` remains available; detached/managed agents require a live host-approved manager-service authority.`));
|
|
374
425
|
process.exit(1);
|
|
@@ -376,14 +427,24 @@ async function runManager(args, defaultRuntime) {
|
|
|
376
427
|
}
|
|
377
428
|
// Parse the roster + launch spec before touching the network — a malformed file should fail fast,
|
|
378
429
|
// 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;
|
|
430
|
+
const roster = service ? service.roster ?? [] : v.roster ? loadRoster(v.roster) : [];
|
|
431
|
+
const launchSpec = service ? service.launchSpec : v.launch ? loadLaunchSpec(v.launch) : undefined;
|
|
381
432
|
if (v["resume-attempt"] && (v.roster || v.launch || v.spawn)) {
|
|
382
433
|
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
434
|
process.exit(1);
|
|
384
435
|
}
|
|
385
|
-
if (v["resume-commit-token"] && !v["resume-attempt"]) {
|
|
386
|
-
console.error(c.red("✗
|
|
436
|
+
if ((v["resume-commit-token"] || v["resume-commit-file"] || v["resume-commit-sha256"]) && !v["resume-attempt"]) {
|
|
437
|
+
console.error(c.red("✗ a resume commit token requires --resume-attempt"));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
let resumeDurableCommitToken;
|
|
441
|
+
try {
|
|
442
|
+
resumeDurableCommitToken = resumeCommitToken(v, !service);
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
if (service)
|
|
446
|
+
throw e;
|
|
447
|
+
console.error(c.red(`✗ ${e.message}`));
|
|
387
448
|
process.exit(1);
|
|
388
449
|
}
|
|
389
450
|
// The registration policy is read AFTER every local refusal above and before the first dial: a
|
|
@@ -394,6 +455,8 @@ async function runManager(args, defaultRuntime) {
|
|
|
394
455
|
eventsRequired = (await refreshRegistrationPolicy(target))?.events === "required";
|
|
395
456
|
}
|
|
396
457
|
catch (e) {
|
|
458
|
+
if (service)
|
|
459
|
+
throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
|
|
397
460
|
console.error(c.red(`✗ ${e.message}`));
|
|
398
461
|
process.exit(1);
|
|
399
462
|
}
|
|
@@ -414,6 +477,8 @@ async function runManager(args, defaultRuntime) {
|
|
|
414
477
|
maxSessions = parsePositiveIntegerFlag("--max-sessions", v["max-sessions"]);
|
|
415
478
|
}
|
|
416
479
|
catch (e) {
|
|
480
|
+
if (service)
|
|
481
|
+
throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
|
|
417
482
|
console.error(c.red(`✗ ${e.message}`));
|
|
418
483
|
process.exit(1);
|
|
419
484
|
}
|
|
@@ -430,40 +495,66 @@ async function runManager(args, defaultRuntime) {
|
|
|
430
495
|
servers: server,
|
|
431
496
|
eventsRequired,
|
|
432
497
|
runtime,
|
|
498
|
+
execution,
|
|
433
499
|
consolePort,
|
|
434
500
|
wsPort,
|
|
435
501
|
attachHost,
|
|
436
502
|
maxSessions,
|
|
437
503
|
installedExtensions: true,
|
|
438
504
|
resumeAttemptId: v["resume-attempt"],
|
|
439
|
-
resumeDurableCommitToken
|
|
505
|
+
resumeDurableCommitToken,
|
|
440
506
|
remoteAuthority,
|
|
507
|
+
...(service ? { workspaceRoot: managerRoot, onServiceIdentity: bindManagerServiceIdentity(service) } : {}),
|
|
441
508
|
});
|
|
442
509
|
}
|
|
443
510
|
catch (e) {
|
|
511
|
+
if (service)
|
|
512
|
+
throw new CommandFailure("unknown", "OPERATION_UNKNOWN", "the native manager child did not confirm startup; inspect the original protected launch records before another start");
|
|
444
513
|
console.error(c.red(`✗ ${e.message}`));
|
|
445
514
|
process.exit(1);
|
|
446
515
|
}
|
|
447
|
-
const managerRoot = findCotalRoot();
|
|
448
516
|
const managerContext = { root: managerRoot, space };
|
|
449
|
-
|
|
517
|
+
// Until the settled record exists, a release only drops a managed child's exact starting slot.
|
|
518
|
+
// A failed start or a stop that wins the start leaves no settled legacy record behind.
|
|
519
|
+
let releaseSettledRecord;
|
|
520
|
+
const releasePidRecord = () => { releaseSettledRecord?.(); releaseStartingCustody?.(); };
|
|
450
521
|
// Signals and cooperative requests share the sparing default. Only the process-bound
|
|
451
|
-
// `cotal down --with-agents` intent authorizes an agent reap
|
|
522
|
+
// `cotal down --with-agents` intent authorizes an agent reap; a managed child accepts only its
|
|
523
|
+
// request-bound native stop handoff.
|
|
452
524
|
let shuttingDown = false;
|
|
453
525
|
// A start that failed under a pending stop leaves the exit to its own error once that stop settles.
|
|
454
526
|
let startFailed = false;
|
|
455
527
|
let stopped;
|
|
528
|
+
let disposeObserver;
|
|
456
529
|
const shutdown = () => {
|
|
457
530
|
if (shuttingDown)
|
|
458
531
|
return;
|
|
532
|
+
let request;
|
|
533
|
+
let withAgents;
|
|
534
|
+
if (service) {
|
|
535
|
+
try {
|
|
536
|
+
request = acceptManagerServiceStop(mgr, service);
|
|
537
|
+
withAgents = request.withAgents;
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
console.error(c.yellow("! the managed manager stop handoff was not accepted; inspect its original request and phase records"));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const decision = consumeManagerShutdownIntent({ root: managerRoot, space });
|
|
546
|
+
if (decision.warning)
|
|
547
|
+
console.error(c.yellow(`! ${decision.warning}`));
|
|
548
|
+
withAgents = decision.withAgents;
|
|
549
|
+
}
|
|
459
550
|
shuttingDown = true;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
console.error(c.yellow(`! ${decision.warning}`));
|
|
463
|
-
stopped = mgr.stop({ withAgents: decision.withAgents })
|
|
551
|
+
disposeObserver?.();
|
|
552
|
+
stopped = mgr.stop({ withAgents })
|
|
464
553
|
.then(() => {
|
|
465
554
|
publishManagerSpareCapability(managerContext, false);
|
|
466
|
-
releasePidRecord
|
|
555
|
+
releasePidRecord();
|
|
556
|
+
if (service && request)
|
|
557
|
+
publishServiceStopReceipt(service.context, request, "completed");
|
|
467
558
|
if (!startFailed)
|
|
468
559
|
process.exit(0);
|
|
469
560
|
})
|
|
@@ -472,7 +563,17 @@ async function runManager(args, defaultRuntime) {
|
|
|
472
563
|
// under this instance's lease. A record removed under a failed stop is the orphan defect the
|
|
473
564
|
// pidfile contract exists to prevent. The process still exits: stop() has closed what it could,
|
|
474
565
|
// and the shuttingDown latch would otherwise leave it deaf to every later signal (#2290).
|
|
475
|
-
|
|
566
|
+
if (service) {
|
|
567
|
+
if (request) {
|
|
568
|
+
try {
|
|
569
|
+
publishServiceStopReceipt(service.context, request, "failed");
|
|
570
|
+
}
|
|
571
|
+
catch { /* Missing terminal evidence stays unknown to the original controller. */ }
|
|
572
|
+
}
|
|
573
|
+
console.error(c.red("✗ the native managed manager stop did not confirm completion; its phase evidence was retained"));
|
|
574
|
+
}
|
|
575
|
+
else
|
|
576
|
+
console.error(c.red(`✗ ${e.message}`));
|
|
476
577
|
if (!startFailed)
|
|
477
578
|
process.exit(1);
|
|
478
579
|
});
|
|
@@ -502,13 +603,22 @@ async function runManager(args, defaultRuntime) {
|
|
|
502
603
|
if (shuttingDown)
|
|
503
604
|
return;
|
|
504
605
|
// 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
|
-
|
|
606
|
+
// before its cooperative endpoint exists. A failed start still publishes no pid record. A
|
|
607
|
+
// managed child already owns its exact starting PID slot; it settles that same process here.
|
|
608
|
+
releaseSettledRecord = recordManagerPid(managerRoot, space);
|
|
507
609
|
publishManagerSpareCapability(managerContext, mgr.spareSeats);
|
|
508
|
-
|
|
509
|
-
c.
|
|
510
|
-
|
|
511
|
-
|
|
610
|
+
if (!service)
|
|
611
|
+
console.log(c.green("✓ manager up") +
|
|
612
|
+
c.dim(` (space ${space} · ${mgr.runtimeKind})`) +
|
|
613
|
+
`\n console: ${mgr.consoleUrl}` +
|
|
614
|
+
c.dim("\n spawn: cotal spawn --detach <persona> · stop: cotal stop --name <n> (Ctrl-C to shut down)"));
|
|
615
|
+
if (service) {
|
|
616
|
+
// The served-contract binding precedes the serving receipt, so a serving child always has it.
|
|
617
|
+
const served = managerClusterArtifacts();
|
|
618
|
+
publishServedContract(service.context, { endpoint: MANAGER_ENDPOINT, clusterDigest: served.closureDigest, document: served.document });
|
|
619
|
+
publishServiceStartup(serviceStartupPath(service.context), { phase: "manager-serving", ...service.context });
|
|
620
|
+
disposeObserver = armManagerServiceObserver(mgr, service);
|
|
621
|
+
}
|
|
512
622
|
// Declarative boot: bring up each rostered agent through the same spawn path as a detached spawn.
|
|
513
623
|
// A failed entry is logged but non-fatal — healthy agents stay up and the operator can
|
|
514
624
|
// fix the roster without the supervisor crash-looping.
|
|
@@ -548,7 +658,7 @@ async function runManager(args, defaultRuntime) {
|
|
|
548
658
|
// transient persona, then spawn it with its resolved ACLs/identity. Staggered like `--spawn` so
|
|
549
659
|
// heavy cold-starts don't pile up. A failed entry is logged, non-fatal — healthy agents stay up.
|
|
550
660
|
if (launchSpec) {
|
|
551
|
-
const root =
|
|
661
|
+
const root = managerRoot;
|
|
552
662
|
for (let i = 0; i < launchSpec.agents.length; i++) {
|
|
553
663
|
const la = launchSpec.agents[i];
|
|
554
664
|
let configPath;
|
|
@@ -574,8 +684,33 @@ async function runManager(args, defaultRuntime) {
|
|
|
574
684
|
}
|
|
575
685
|
await new Promise(() => { });
|
|
576
686
|
}
|
|
687
|
+
function validateMaintenanceArguments(args, mode) {
|
|
688
|
+
presentationOf(args.values);
|
|
689
|
+
if (args.positionals.length)
|
|
690
|
+
throw new CommandFailure("refused", "USAGE_INVALID", `manager ${mode} takes named options, not additional positional targets`);
|
|
691
|
+
if (mode === "seats" && !args.values.drain && (args.values.yes !== undefined || args.values["dry-run"] !== undefined))
|
|
692
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "manager seats --yes and --dry-run require --drain");
|
|
693
|
+
for (const key of mode === "reconcile" ? ["endpoint", "instance"] : []) {
|
|
694
|
+
const value = args.values[key];
|
|
695
|
+
if (value !== undefined && (typeof value !== "string" || !value || value.length > 200 || token(value) !== value))
|
|
696
|
+
throw new CommandFailure("refused", "USAGE_INVALID", `--${key} needs one full native token; prefixes and normalized replacements are not used`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function confirmMaintenance(args, target, review, question) {
|
|
700
|
+
if (args.values.yes === true)
|
|
701
|
+
return true;
|
|
702
|
+
const presentation = presentationOf(args.values);
|
|
703
|
+
if (presentation === "json" || !process.stdin.isTTY || !process.stdout.isTTY || inputDisabledByFlag() || process.env.COTAL_NO_PROMPT === "1")
|
|
704
|
+
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" });
|
|
705
|
+
console.log(renderReport(review, presentation, terminalWidth(process.stdout)));
|
|
706
|
+
const answer = await untilInputEnds((signal) => prompts.confirm({ message: question, initialValue: false, signal }));
|
|
707
|
+
if (!prompts.isCancel(answer) && answer)
|
|
708
|
+
return true;
|
|
709
|
+
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."] }] }] }));
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
577
712
|
/**
|
|
578
|
-
* `cotal reconcile
|
|
713
|
+
* `cotal manager reconcile` — the guarded exit from an issuance gate left FROZEN by a crashed
|
|
579
714
|
* re-registration whose freeze-holder is dead (#391).
|
|
580
715
|
*
|
|
581
716
|
* WHY THIS IS A CLI COMMAND AND NOT AN ADMIN VERB ON THE MANAGER ENDPOINT: the wedged state IS
|
|
@@ -586,161 +721,163 @@ async function runManager(args, defaultRuntime) {
|
|
|
586
721
|
* Read-only until the guard passes: it observes and probes before it constructs the barrier.
|
|
587
722
|
*/
|
|
588
723
|
async function runReconcileGate(args) {
|
|
724
|
+
validateMaintenanceArguments(args, "reconcile");
|
|
589
725
|
const v = args.values;
|
|
590
|
-
const space = spaceFor(v);
|
|
591
|
-
const
|
|
726
|
+
const presentation = presentationOf(args.values), root = findCotalRoot(), space = spaceFor(v, root);
|
|
727
|
+
const selected = resolveNativeServiceTarget(root, space, v.server);
|
|
592
728
|
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
729
|
const instanceId = v.instance ?? loadManagerInstanceIdentity(root, space)?.instanceId;
|
|
597
|
-
if (!instanceId)
|
|
598
|
-
throw new
|
|
599
|
-
const
|
|
730
|
+
if (!instanceId || instanceId.length > 200 || token(instanceId) !== instanceId)
|
|
731
|
+
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" });
|
|
732
|
+
const target = { root: selected.root, space: selected.space, endpoint, instanceId };
|
|
733
|
+
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."] }] }] };
|
|
734
|
+
if (args.values["dry-run"]) {
|
|
735
|
+
writeResult(presentation, { command: "manager", target, result: { kind: "manager-gate-repair-preview", dryRun: true, submitted: false, guardEvidence: "not-checked", credentialIssued: false } }, () => review);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (!await confirmMaintenance(args, target, review, "Attempt the displayed gate repair under its native liveness guard?"))
|
|
739
|
+
return;
|
|
740
|
+
let auth;
|
|
741
|
+
try {
|
|
742
|
+
auth = await getSpaceAuth(workspaceSecretStore(root), space);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (isCommandFailure(error))
|
|
746
|
+
throw error;
|
|
747
|
+
throw new CommandFailure("refused", "DEPENDENCY_UNAVAILABLE", "this root's space signing material could not be read; no gate repair was submitted", { target });
|
|
748
|
+
}
|
|
600
749
|
if (!auth)
|
|
601
|
-
throw new
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
endpointServeExecutor: { endpoint, instanceId }
|
|
606
|
-
|
|
607
|
-
|
|
750
|
+
throw new CommandFailure("refused", "PERMISSION_DENIED", "gate repair requires this root's native space signing material; no participant or broader caller was substituted", { target });
|
|
751
|
+
let nc;
|
|
752
|
+
try {
|
|
753
|
+
const identity = newIdentity();
|
|
754
|
+
const creds = await mintCreds(auth, identity, "endpoint-serve-executor", { endpointServeExecutor: { endpoint, instanceId } });
|
|
755
|
+
nc = await dialerFor(selected.server)({ servers: selected.server, ...standaloneConnectOpts({ creds, tls: selected.tlsRequired }), maxReconnectAttempts: 0 });
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
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 });
|
|
759
|
+
}
|
|
608
760
|
try {
|
|
609
761
|
const kv = await new Kvm(nc).open(epAuthBucket(space));
|
|
610
762
|
const recordsKv = await new Kvm(nc).open(recordsBucket(space));
|
|
611
|
-
const
|
|
612
|
-
kv, space, endpoint, instanceId,
|
|
613
|
-
probeHolder: makeManagerHolderLivenessProbe({ space, servers, auth, log: (
|
|
614
|
-
evictHolders: makeManagerEndpointHolderEvictor({ space, servers, auth, log: (
|
|
615
|
-
log: (
|
|
616
|
-
recordsKv,
|
|
763
|
+
const native = await reconcileEndpointGate({
|
|
764
|
+
kv, space, endpoint, instanceId, recordsKv,
|
|
765
|
+
probeHolder: makeManagerHolderLivenessProbe({ space, servers: selected.server, tlsRequired: selected.tlsRequired, auth, log: () => { } }),
|
|
766
|
+
evictHolders: makeManagerEndpointHolderEvictor({ space, servers: selected.server, tlsRequired: selected.tlsRequired, auth, log: () => { } }),
|
|
767
|
+
log: () => { },
|
|
617
768
|
});
|
|
618
|
-
|
|
619
|
-
|
|
769
|
+
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 };
|
|
770
|
+
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
771
|
}
|
|
621
|
-
catch (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
throw e;
|
|
772
|
+
catch (error) {
|
|
773
|
+
if (isCommandFailure(error))
|
|
774
|
+
throw error;
|
|
775
|
+
const beforeEffects = error instanceof GateReconcileRefused && ["no-gate", "not-frozen", "wrong-op-kind", "holder-alive", "holder-unknown", "liveness-unestablishable"].includes(error.condition);
|
|
776
|
+
// The guard's own sentence names the freeze-holder and the next step for this condition.
|
|
777
|
+
if (beforeEffects)
|
|
778
|
+
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" });
|
|
779
|
+
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
780
|
}
|
|
631
781
|
finally {
|
|
632
782
|
await nc.drain().catch(() => nc.close());
|
|
633
783
|
}
|
|
634
784
|
}
|
|
635
|
-
/**
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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 });
|
|
785
|
+
/** List local custodian records; drain only through the native creation-identity guards. */
|
|
786
|
+
async function runSeats(args) {
|
|
787
|
+
validateMaintenanceArguments(args, "seats");
|
|
788
|
+
const root = custodyRoot(), presentation = presentationOf(args.values), drain = args.values.drain === true;
|
|
789
|
+
const target = { scope: "local-custody-store", root };
|
|
790
|
+
const view = (result) => ({ sections: [
|
|
791
|
+
{ 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."] }] },
|
|
792
|
+
{ 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."] }] },
|
|
793
|
+
] });
|
|
794
|
+
const project = (entries) => entries.map((entry) => ({ id: entry.id, name: entry.name ?? null, state: entry.state, custodianPid: entry.custodianPid ?? null, childPid: entry.childPid ?? null }));
|
|
795
|
+
let before;
|
|
687
796
|
try {
|
|
688
|
-
|
|
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.`);
|
|
797
|
+
before = project(await drainSeats(root));
|
|
696
798
|
}
|
|
697
|
-
catch
|
|
698
|
-
|
|
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;
|
|
799
|
+
catch {
|
|
800
|
+
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
801
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
802
|
+
const refused = before.some((entry) => entry.state === "refused");
|
|
803
|
+
if (!drain || args.values["dry-run"] || refused) {
|
|
804
|
+
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);
|
|
805
|
+
return;
|
|
710
806
|
}
|
|
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}`);
|
|
807
|
+
if (!before.some((entry) => entry.state === "childless")) {
|
|
808
|
+
writeResult(presentation, { command: "manager", target, result: { kind: "manager-seat-drain", entries: before, preview: false, drainSubmitted: false }, noOp: true }, view);
|
|
723
809
|
return;
|
|
724
810
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
811
|
+
if (!await confirmMaintenance(args, target, view({ entries: before, preview: true }), "Drain all safely childless records in the displayed local store? Live children stay."))
|
|
812
|
+
return;
|
|
813
|
+
let entries;
|
|
814
|
+
try {
|
|
815
|
+
entries = project(await drainSeats(root, { drain: true }));
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
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" });
|
|
819
|
+
}
|
|
820
|
+
const incomplete = entries.some((entry) => entry.state === "refused"), changed = entries.filter((entry) => entry.state === "drained").map((entry) => ({ kind: "seat-record-drained", id: entry.id }));
|
|
821
|
+
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
822
|
}
|
|
823
|
+
const maintenanceOutputFlags = [jsonFlag, plainFlag,
|
|
824
|
+
{ name: "yes", short: "y", type: "boolean", description: "confirm the selected guarded gate repair or childless-seat drain" },
|
|
825
|
+
{ name: "dry-run", type: "boolean", description: "review the selected maintenance scope without submitting repair or drain effects" },
|
|
826
|
+
];
|
|
732
827
|
/** The manager's commands: the `supervise` daemon runner, and the guarded `reconcile-gate` repair.
|
|
733
828
|
* Self-registered on import; the `cotal` binary resolves them from the registry. */
|
|
734
829
|
const managerCommands = [
|
|
735
830
|
{
|
|
736
831
|
kind: "command",
|
|
737
|
-
name: "
|
|
832
|
+
name: "__service-manager",
|
|
833
|
+
hidden: true,
|
|
834
|
+
prepareMeshTarget: false,
|
|
835
|
+
group: "Manager",
|
|
836
|
+
summary: "native manager child with protected launch identity and request-bound stop receipts",
|
|
837
|
+
flags: [
|
|
838
|
+
{ name: "root", type: "string", description: "explicit registered mesh root" },
|
|
839
|
+
{ name: "space", type: "string", description: "explicit registered space" },
|
|
840
|
+
{ name: "server", type: "string", description: "registered broker URL without credentials" },
|
|
841
|
+
{ name: "runtime", type: "string", description: "explicit native seat runtime" },
|
|
842
|
+
{ name: "console-port", type: "string", description: "explicit native console port; zero allocates a port" },
|
|
843
|
+
{ name: "console-host", type: "string", description: "explicit native console bind host" },
|
|
844
|
+
{ name: "ws-port", type: "string", description: "the prepared broker websocket port" },
|
|
845
|
+
{ name: "max-sessions", type: "string", description: "explicit positive native session ceiling" },
|
|
846
|
+
{ name: "launch", type: "string", description: "private native launch spec for this selected space" },
|
|
847
|
+
{ name: "launch-sha256", type: "string", description: "full digest pinned by the saved service definition" },
|
|
848
|
+
{ name: "roster", type: "string", description: "absolute roster file whose agents start at launch" },
|
|
849
|
+
{ name: "service-instance", type: "string", description: "full controller-assigned instance ID" },
|
|
850
|
+
{ name: "launch-request", type: "string", description: "full original launch request ID" },
|
|
851
|
+
{ name: "state-directory", type: "string", description: "owned private launch state directory" },
|
|
852
|
+
{ name: "resume-attempt", type: "string", description: "maintenance resume attempt this launch serves" },
|
|
853
|
+
{ name: "resume-commit-file", type: "string", description: "protected one-read resume commit handoff file" },
|
|
854
|
+
{ name: "resume-commit-sha256", type: "string", description: "full digest of the resume commit handoff" },
|
|
855
|
+
],
|
|
856
|
+
validate: (args) => { managerServiceChild(args); },
|
|
857
|
+
requiredExtensions: (args) => typeof args.values.runtime === "string" && args.values.runtime !== "pty" ? [{ kind: "runtime", name: args.values.runtime }] : [],
|
|
858
|
+
run: async (args) => {
|
|
859
|
+
const service = managerServiceChild(args);
|
|
860
|
+
try {
|
|
861
|
+
await runManager(args, "auto", service);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
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");
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
kind: "command",
|
|
870
|
+
// The manager daemon `up` launches. `supervise` is the same entry for persisted callers, plus the
|
|
871
|
+
// deprecation warning; help and completion list neither.
|
|
872
|
+
name: "__daemon-manager",
|
|
873
|
+
hidden: true,
|
|
738
874
|
group: "Manager",
|
|
739
875
|
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
876
|
flags: [
|
|
741
877
|
{ name: "space", type: "string", value: "<s>", description: "space to supervise (default: this folder's auth space; env COTAL_SPACE when unset)" },
|
|
742
878
|
{ name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh; env COTAL_SERVER when unset)" },
|
|
743
879
|
{ name: "runtime", type: "string", value: "<name>", description: "agent runtime (default pty; others come from installed extensions)" },
|
|
880
|
+
{ 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
881
|
{ name: "console-port", type: "string", value: "<n>", description: "protocol-console port" },
|
|
745
882
|
{ name: "console-host", type: "string", value: "<host>", description: "bind host for the console endpoint (default: loopback)" },
|
|
746
883
|
{ name: "ws-port", type: "string", value: "<n>", description: "broker ws listener port for the console session client" },
|
|
@@ -748,10 +885,15 @@ const managerCommands = [
|
|
|
748
885
|
{ name: "roster", type: "string", value: "<file>", description: "declarative roster to boot at startup" },
|
|
749
886
|
{ name: "launch", type: "string", value: "<spec>", description: "resolved mesh-manifest launch spec (cotal up -f / spawn -f)" },
|
|
750
887
|
{ 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
|
|
888
|
+
{ name: "resume-commit-token", type: "string", value: "<token>", description: "deprecated: durable resume commit evidence on argv; prefer --resume-commit-file" },
|
|
889
|
+
{ name: "resume-commit-file", type: "string", value: "<path>", description: "protected one-read resume commit handoff file" },
|
|
890
|
+
{ name: "resume-commit-sha256", type: "string", value: "<hex>", description: "full digest of the resume commit handoff" },
|
|
752
891
|
{ name: "spawn", type: "string", value: "<names>", description: "comma-separated personas to pre-spawn at startup" },
|
|
753
892
|
],
|
|
893
|
+
validate: (args) => { superviseExecution(args.values); },
|
|
754
894
|
requiredExtensions: (args) => {
|
|
895
|
+
if (superviseExecution(args.values) === "none")
|
|
896
|
+
return [];
|
|
755
897
|
const runtime = args.values.runtime;
|
|
756
898
|
return typeof runtime === "string" && runtime !== "pty" ? [{ kind: "runtime", name: runtime }] : [];
|
|
757
899
|
},
|
|
@@ -761,8 +903,11 @@ const managerCommands = [
|
|
|
761
903
|
kind: "command",
|
|
762
904
|
name: "reconcile-gate",
|
|
763
905
|
group: "Manager",
|
|
906
|
+
outputs: ["json", "plain"],
|
|
907
|
+
prepareMeshTarget: false,
|
|
908
|
+
validate: (args) => validateMaintenanceArguments(args, "reconcile"),
|
|
764
909
|
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: [
|
|
910
|
+
flags: [...maintenanceOutputFlags,
|
|
766
911
|
{ name: "space", type: "string", value: "<s>", description: "space to reconcile in (default: this folder's auth space)" },
|
|
767
912
|
{ name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh)" },
|
|
768
913
|
{ name: "endpoint", type: "string", value: "<e>", description: `endpoint whose gate is frozen (default: ${MANAGER_ENDPOINT})` },
|
|
@@ -773,24 +918,90 @@ const managerCommands = [
|
|
|
773
918
|
{
|
|
774
919
|
kind: "command",
|
|
775
920
|
name: "deregister-instance",
|
|
776
|
-
group: "Manager",
|
|
777
|
-
summary: "
|
|
778
|
-
|
|
921
|
+
group: "Manager", hidden: true, reads: true, prepareMeshTarget: false,
|
|
922
|
+
summary: "legacy spelling; use endpoint deregister <class> <full-instance-id>",
|
|
923
|
+
outputs: ["json", "plain"],
|
|
924
|
+
flags: [jsonFlag, plainFlag,
|
|
779
925
|
{ name: "space", type: "string", value: "<s>", description: "space the instance is registered in (default: this folder's auth space)" },
|
|
780
926
|
{ name: "server", type: "string", value: "<url>", description: "broker URL (default: the local mesh)" },
|
|
781
927
|
{ name: "endpoint", type: "string", value: "<e>", description: `endpoint the instance serves (default: ${MANAGER_ENDPOINT})` },
|
|
782
928
|
{ 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
929
|
],
|
|
784
|
-
|
|
930
|
+
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]" }); },
|
|
931
|
+
run: async () => { throw new CommandFailure("refused", "USAGE_INVALID", "use cotal endpoint deregister <class> <full-instance-id> instead"); },
|
|
785
932
|
},
|
|
786
933
|
{
|
|
787
934
|
kind: "command",
|
|
788
935
|
name: "seats",
|
|
789
936
|
group: "Manager",
|
|
790
937
|
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
|
-
|
|
938
|
+
outputs: ["json", "plain"],
|
|
939
|
+
reads: true,
|
|
940
|
+
prepareMeshTarget: false,
|
|
941
|
+
flags: [...maintenanceOutputFlags,
|
|
942
|
+
{ name: "drain", type: "boolean", description: "retire safely childless records in this local store; live children stay" },
|
|
943
|
+
],
|
|
944
|
+
validate: (args) => validateMaintenanceArguments(args, "seats"),
|
|
792
945
|
run: runSeats,
|
|
793
946
|
},
|
|
794
947
|
];
|
|
795
|
-
|
|
948
|
+
const maintenance = [
|
|
949
|
+
{ name: "reconcile", source: "reconcile-gate", summary: "repair an owned manager issuance gate from its mesh root" },
|
|
950
|
+
{ name: "seats", source: "seats", summary: "inspect retained local seats; --drain removes only childless records" },
|
|
951
|
+
].map((mode) => {
|
|
952
|
+
const source = managerCommands.find((command) => command.name === mode.source);
|
|
953
|
+
if (!source)
|
|
954
|
+
throw new Error(`manager command ${mode.source} is not registered`);
|
|
955
|
+
const command = { ...source, flags: source.flags?.map((flag) => flag.name === "space" ? { ...flag, short: "s" } : flag) };
|
|
956
|
+
return { ...mode, command };
|
|
957
|
+
});
|
|
958
|
+
const maintenanceFlags = new Map();
|
|
959
|
+
for (const mode of maintenance)
|
|
960
|
+
for (const flag of mode.command.flags ?? []) {
|
|
961
|
+
const shared = maintenanceOutputFlags.some((common) => common.name === flag.name);
|
|
962
|
+
maintenanceFlags.set(flag.name, { ...flag, description: shared ? flag.description : `${mode.name}: ${flag.description ?? ""}` });
|
|
963
|
+
}
|
|
964
|
+
function managerInvocation(args) {
|
|
965
|
+
const parsed = parseCommandArgs(managerGroup, [...args.raw]);
|
|
966
|
+
const mode = parsed.positionals[0];
|
|
967
|
+
if (mode === undefined) {
|
|
968
|
+
if (Object.keys(parsed.values).some((key) => !["json", "plain"].includes(key)))
|
|
969
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "choose manager reconcile or manager seats before its options");
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
const child = maintenance.find((item) => item.name === mode);
|
|
973
|
+
if (!child)
|
|
974
|
+
throw new CommandFailure("refused", "USAGE_INVALID", "manager takes reconcile or seats");
|
|
975
|
+
const index = parsed.positionalIndices?.[0];
|
|
976
|
+
if (index === undefined)
|
|
977
|
+
throw new Error("the manager subcommand has no parsed argument index");
|
|
978
|
+
const words = args.raw.filter((_, position) => position !== index);
|
|
979
|
+
return { command: child.command, args: parseCommandArgs(child.command, words) };
|
|
980
|
+
}
|
|
981
|
+
const managerGroup = {
|
|
982
|
+
kind: "command",
|
|
983
|
+
name: "manager",
|
|
984
|
+
aliases: ["mgr"],
|
|
985
|
+
group: "Manager",
|
|
986
|
+
summary: "inspect retained local seats or repair an owned manager gate",
|
|
987
|
+
usage: "cotal manager [reconcile [--yes|--dry-run] | seats [--drain [--yes|--dry-run]]] [--json|--plain]",
|
|
988
|
+
positionals: "[reconcile | seats]",
|
|
989
|
+
outputs: ["json", "plain"],
|
|
990
|
+
flags: [...maintenanceFlags.values()],
|
|
991
|
+
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]" })),
|
|
992
|
+
reads: true,
|
|
993
|
+
prepareMeshTarget: false,
|
|
994
|
+
validate: (args) => { presentationOf(args.values); const invocation = managerInvocation(args); return invocation?.command.validate?.(invocation.args); },
|
|
995
|
+
async run(args) {
|
|
996
|
+
const invocation = managerInvocation(args);
|
|
997
|
+
if (invocation)
|
|
998
|
+
return invocation.command.run(invocation.args);
|
|
999
|
+
writeResult(presentationOf(args.values), { command: "manager", target: null, result: { kind: "manager-maintenance-commands", operations: maintenance.map(({ name, summary }) => ({ name, summary })), managerStarted: false } }, () => ({ sections: [
|
|
1000
|
+
{ title: "Manager maintenance", blocks: [{ kind: "commands", items: maintenance.map((mode) => ({ command: `cotal mgr ${mode.name}`, description: mode.summary })) }] },
|
|
1001
|
+
{ 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" }] }] },
|
|
1002
|
+
] }));
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
const daemonManager = managerCommands.find((command) => command.name === "__daemon-manager");
|
|
1006
|
+
registry.register(...managerCommands.map((command) => command.name === "seats" || command.name === "reconcile-gate" ? { ...command, hidden: true } : command), deprecatedDaemonEntry("supervise", daemonManager), managerGroup, endpointDeregisterAction);
|
|
796
1007
|
//# sourceMappingURL=commands.js.map
|