@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.
- 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 +384 -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 +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manager-service-contract.d.ts +61 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +225 -4
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +106 -3
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +526 -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/remote-authority.d.ts +4 -2
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +39 -0
- package/dist/remote-authority.js.map +1 -1
- package/dist/run-hosting.d.ts +34 -1
- package/dist/run-hosting.d.ts.map +1 -1
- package/dist/run-hosting.js +369 -15
- package/dist/run-hosting.js.map +1 -1
- 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/dist/vitals.d.ts +37 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +117 -0
- package/dist/vitals.js.map +1 -0
- 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
|
|
@@ -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("✗
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
c.
|
|
510
|
-
|
|
511
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
599
|
-
const
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
endpointServeExecutor: { endpoint, instanceId }
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
612
|
-
kv, space, endpoint, instanceId,
|
|
613
|
-
probeHolder: makeManagerHolderLivenessProbe({ space, servers, auth, log: (
|
|
614
|
-
evictHolders: makeManagerEndpointHolderEvictor({ space, servers, auth, log: (
|
|
615
|
-
log: (
|
|
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
|
-
|
|
619
|
-
|
|
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 (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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 });
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
726
|
-
|
|
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: "
|
|
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
|
|
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: "
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|