@cotal-ai/cli 0.61.0 → 0.63.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/command.d.ts.map +1 -1
- package/dist/command.js +4 -0
- package/dist/command.js.map +1 -1
- package/dist/commands/describe.d.ts.map +1 -1
- package/dist/commands/describe.js +19 -11
- package/dist/commands/describe.js.map +1 -1
- package/dist/commands/down.d.ts +3 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +24 -18
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/meshes.d.ts +4 -0
- package/dist/commands/meshes.d.ts.map +1 -1
- package/dist/commands/meshes.js +45 -9
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn.d.ts +16 -1
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +206 -44
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +3 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +84 -20
- package/dist/commands/up.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/control.d.ts +13 -1
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/control.js +45 -9
- package/dist/lib/control.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts +10 -1
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +19 -0
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/teardown-spare.d.ts +7 -3
- package/dist/lib/teardown-spare.d.ts.map +1 -1
- package/dist/lib/teardown-spare.js +22 -10
- package/dist/lib/teardown-spare.js.map +1 -1
- package/dist/managed-handoff.d.ts +10 -0
- package/dist/managed-handoff.d.ts.map +1 -0
- package/dist/managed-handoff.js +42 -0
- package/dist/managed-handoff.js.map +1 -0
- package/dist/seed/reconcile.d.ts.map +1 -1
- package/dist/seed/reconcile.js +4 -1
- package/dist/seed/reconcile.js.map +1 -1
- package/dist/seed/store.d.ts.map +1 -1
- package/dist/seed/store.js +5 -7
- package/dist/seed/store.js.map +1 -1
- package/package.json +4 -4
package/dist/commands/up.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createConnection, createServer } from "node:net";
|
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
5
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, readdirSync, } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, ttlBuckets, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, presenceBucket, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
7
|
+
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, ttlBuckets, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, presenceBucket, seedChannelRegistry, ensureDefaultDeliveryClass, LEASE_TTL_MS, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
8
8
|
import { connect } from "@nats-io/transport-node";
|
|
9
9
|
import { jetstreamManager, StorageType } from "@nats-io/jetstream";
|
|
10
10
|
import { Kvm } from "@nats-io/kv";
|
|
@@ -17,7 +17,7 @@ import { resolveNatsServer } from "../lib/nats-bin.js";
|
|
|
17
17
|
import { cotalPath, cotalRoot } from "../lib/paths.js";
|
|
18
18
|
import { renderDetachedSummary } from "../lib/up-report.js";
|
|
19
19
|
import { detachedSystemdSupervisionWarning } from "../lib/systemd-supervision.js";
|
|
20
|
-
import { deliveryUp, ensureControlPlane, stopDelivery } from "../lib/delivery-proc.js";
|
|
20
|
+
import { deliveryStoppedByDown, deliveryUp, ensureControlPlane, ensureDelivery, stopDelivery } from "../lib/delivery-proc.js";
|
|
21
21
|
import { RESPONDER_UNBOUND_CONSEQUENCE } from "../lib/delivery-responder.js";
|
|
22
22
|
import { displayCmd } from "../lib/self-exec.js";
|
|
23
23
|
import { liveManagerWouldApplyMaxSessions, managerHasDeliveryMarker, managerLogDisplayPath, managerRecordState, managerUp, stopManager } from "../lib/manager-proc.js";
|
|
@@ -26,6 +26,7 @@ import { loadManifest } from "../lib/manifest/index.js";
|
|
|
26
26
|
import { buildLaunchSpec, genRunId, manifestToChannels, preflightConnectors, writeLaunchSpec } from "../lib/manifest/apply.js";
|
|
27
27
|
import { renderUpPlan, renderInherited, renderWarnings } from "../lib/manifest/render.js";
|
|
28
28
|
import { failManifest } from "./topology.js";
|
|
29
|
+
import { reserveStop } from "./down.js";
|
|
29
30
|
import { extensionNames, preflightRuntime } from "../ext-loader.js";
|
|
30
31
|
import { completingFlagValue } from "../lib/completion.js";
|
|
31
32
|
import { askManager } from "../lib/control.js";
|
|
@@ -808,7 +809,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
808
809
|
// failure copy says "restart it with `cotal up`"), so a refresh must re-ensure the service —
|
|
809
810
|
// never just reprint "already running" over a dead callout. No broker config is (re)written
|
|
810
811
|
// here, so healing on a bare `cotal up` is safe: the mode can't drift, only the daemon heals.
|
|
811
|
-
let userAuth
|
|
812
|
+
let userAuth;
|
|
812
813
|
if (held.mode === "user") {
|
|
813
814
|
const auth = await getSpaceAuth(workspaceSecretStore(root), held.space);
|
|
814
815
|
if (!auth) {
|
|
@@ -868,15 +869,11 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
868
869
|
if (!controlPlane)
|
|
869
870
|
process.exitCode = 1;
|
|
870
871
|
}
|
|
871
|
-
const heldAttachHost = attachHostFor(held.space, values.host);
|
|
872
|
-
const heldMaxSessions = maxSessionsFor(held.space, maxSessions);
|
|
873
872
|
// A broker was already answering here — this branch starts nothing, so it must not claim the
|
|
874
|
-
// record as ours (see `Provenance`).
|
|
875
|
-
// `
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
// `attachHost` would silently demote the mesh to loopback.
|
|
879
|
-
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ...(held.storeDir !== undefined ? { storeDir: held.storeDir } : {}), ts: new Date().toISOString() }, "refresh");
|
|
873
|
+
// record as ours (see `Provenance`), and it passes only what this invocation decided.
|
|
874
|
+
// `recordOurMesh` carries everything else (`tlsRequired`, the recorded host and session
|
|
875
|
+
// ceiling, the store) from the record as it stands at write time, not from `held`.
|
|
876
|
+
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(userAuth ? { userAuth } : {}), ...(values.host ? { attachHost: values.host } : {}), ...(maxSessions !== undefined ? { maxSessions } : {}), ts: new Date().toISOString() }, "refresh");
|
|
880
877
|
return;
|
|
881
878
|
}
|
|
882
879
|
const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
|
|
@@ -1047,8 +1044,11 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1047
1044
|
void (async () => {
|
|
1048
1045
|
const managerContext = { root: cotalRoot(), space };
|
|
1049
1046
|
let spared;
|
|
1047
|
+
let spareSeats;
|
|
1050
1048
|
let legacyManagerSpareUnverified = false;
|
|
1051
1049
|
const managerPidPath = localProcessPath(MANAGER_PIDFILE, managerContext);
|
|
1050
|
+
const stopMarker = `${managerPidPath}.stopping`;
|
|
1051
|
+
let reserved = false;
|
|
1052
1052
|
if (existsSync(managerPidPath)) {
|
|
1053
1053
|
const pin = verifyIdentityPin(managerPidPath);
|
|
1054
1054
|
if (pin.kind === "legacy")
|
|
@@ -1056,7 +1056,12 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1056
1056
|
else if (pin.kind === "match") {
|
|
1057
1057
|
spared = await listManagerSeatsForSpare(managerContext);
|
|
1058
1058
|
try {
|
|
1059
|
-
|
|
1059
|
+
// Taken before the capability read and held until the manager is gone, as bare `down`
|
|
1060
|
+
// holds it, so a concurrent `cotal down` can neither stop this manager nor arm a reap
|
|
1061
|
+
// while this stop is in flight.
|
|
1062
|
+
reserveStop("manager", stopMarker);
|
|
1063
|
+
reserved = true;
|
|
1064
|
+
spareSeats = assertManagerCanSpare(managerContext, undefined, pin.record);
|
|
1060
1065
|
}
|
|
1061
1066
|
catch (e) {
|
|
1062
1067
|
// THE CAPABILITY ASSERT IS THE SIGNAL GATE, the same rule bare `down` enforces inside
|
|
@@ -1065,6 +1070,8 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1065
1070
|
// release the latch so the stack keeps running in the foreground; the operator ends it
|
|
1066
1071
|
// with `cotal down --with-agents` from another terminal, and the broker-exit handler
|
|
1067
1072
|
// below already ends `up` when the broker goes.
|
|
1073
|
+
if (reserved)
|
|
1074
|
+
rmSync(stopMarker, { force: true });
|
|
1068
1075
|
console.error(c.red(`! teardown: ${e.message}`));
|
|
1069
1076
|
console.error(c.red(`the stack is still running; to take managed agents with it, run: cotal down --with-agents`));
|
|
1070
1077
|
stopping = false;
|
|
@@ -1078,11 +1085,15 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1078
1085
|
if (legacyManagerSpareUnverified)
|
|
1079
1086
|
printLegacyManagerSpareUncertainty();
|
|
1080
1087
|
else if (spared)
|
|
1081
|
-
printSparedAgents(spared);
|
|
1088
|
+
printSparedAgents(spared, spareSeats);
|
|
1082
1089
|
}
|
|
1083
1090
|
catch (e) {
|
|
1084
1091
|
console.error(`! manager teardown: ${e.message}`);
|
|
1085
1092
|
}
|
|
1093
|
+
finally {
|
|
1094
|
+
if (reserved)
|
|
1095
|
+
rmSync(stopMarker, { force: true });
|
|
1096
|
+
}
|
|
1086
1097
|
await stopAuthService(space).catch((e) => console.error(`! auth teardown: ${e.message}`));
|
|
1087
1098
|
child.kill("SIGTERM");
|
|
1088
1099
|
})();
|
|
@@ -1186,11 +1197,62 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1186
1197
|
storeDir,
|
|
1187
1198
|
ts: new Date().toISOString(),
|
|
1188
1199
|
}, "started");
|
|
1200
|
+
// A DAEMON THAT DIES UNDER THIS RUNNING BROKER IS RESTARTED HERE (#2469). It exits on its own once
|
|
1201
|
+
// it cannot reach the broker, and a starved host makes a running broker look unreachable, so it can
|
|
1202
|
+
// end while this broker serves on. Nothing else brings it back, and every static retirement then
|
|
1203
|
+
// fails on the ctl.delivery-admin rail and holds its name until an operator re-runs `cotal up`. A
|
|
1204
|
+
// clean exit or SIGTERM/SIGINT is a deliberate stop (`cotal down delivery`) and stays stopped, and so
|
|
1205
|
+
// is the SIGKILL `down` escalates to when a starved daemon outlives its grace, also when either ends a
|
|
1206
|
+
// replacement this loop is still waiting on: that attempt then fails, and retrying it would undo the
|
|
1207
|
+
// stop. A failed attempt waits out the lease TTL, the longest a dead holder's lease blocks its
|
|
1208
|
+
// replacement. No daemon is alive during that wait for `down` to signal, so a stop there shows only as
|
|
1209
|
+
// the dead daemon's record going away. Recovery is announced only once the responder is bound.
|
|
1210
|
+
const meshServing = () => !stopping && child.exitCode === null && child.signalCode === null;
|
|
1211
|
+
let restartingDelivery = false;
|
|
1212
|
+
let deliveryStopped = false;
|
|
1213
|
+
const onDeliveryExit = (code, signal) => {
|
|
1214
|
+
if (code === 0 || signal === "SIGTERM" || signal === "SIGINT" || (signal !== null && deliveryStoppedByDown(space))) {
|
|
1215
|
+
deliveryStopped = true;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (restartingDelivery || !meshServing())
|
|
1219
|
+
return;
|
|
1220
|
+
restartingDelivery = true;
|
|
1221
|
+
console.error(c.yellow(`! delivery daemon exited (${signal ? `signal ${signal}` : `code ${code}`}) while nats-server is running - restarting it`));
|
|
1222
|
+
void (async () => {
|
|
1223
|
+
while (meshServing()) {
|
|
1224
|
+
let failure;
|
|
1225
|
+
try {
|
|
1226
|
+
const ensured = await ensureDelivery({ space, server, tls: transport.kind === "tls-required", onDeliveryExit });
|
|
1227
|
+
// An abnormal exit during this attempt was dropped above, so the record decides.
|
|
1228
|
+
if (deliveryUp(space)) {
|
|
1229
|
+
// An unbound responder was reported by ensureDelivery, and this daemon's exit re-arms the restart.
|
|
1230
|
+
if (ensured.responderBound)
|
|
1231
|
+
console.error(c.green(`✓ delivery daemon running again${ensured.pid !== undefined ? ` (pid ${ensured.pid})` : ""}`));
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
failure = "no delivery daemon is running after the attempt";
|
|
1235
|
+
}
|
|
1236
|
+
catch (e) {
|
|
1237
|
+
failure = e.message;
|
|
1238
|
+
}
|
|
1239
|
+
if (deliveryStopped)
|
|
1240
|
+
break;
|
|
1241
|
+
console.error(c.yellow(`! delivery restart failed, retrying in ${LEASE_TTL_MS / 1000}s: ${failure}`));
|
|
1242
|
+
const recorded = !deliveryStoppedByDown(space);
|
|
1243
|
+
await new Promise((r) => setTimeout(r, LEASE_TTL_MS));
|
|
1244
|
+
if (deliveryStopped || (recorded && deliveryStoppedByDown(space)))
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
restartingDelivery = false;
|
|
1248
|
+
})();
|
|
1249
|
+
};
|
|
1189
1250
|
// Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
|
|
1190
1251
|
// It is part of the server, so `cotal up` starts it by default; open dev mode has no daemon.
|
|
1191
1252
|
// Class-2 credential renewal is NOT wired here: the MANAGER is the renewal owner (it is resident
|
|
1192
1253
|
// in every mesh mode — foreground, --detach, refresh — where this foreground process is not).
|
|
1193
1254
|
const controlPlane = await startDeliveryWithBroker(space, server, transport.kind === "tls-required", {
|
|
1255
|
+
onDeliveryExit,
|
|
1194
1256
|
runtime: values.runtime,
|
|
1195
1257
|
// The address the broker was bound to. This is what lets `cotal attach` reach this manager
|
|
1196
1258
|
// from another machine; without it the attach face stays loopback-only, so exposing terminals
|
|
@@ -2441,13 +2503,15 @@ function recordOurMesh(m, provenance) {
|
|
|
2441
2503
|
const usableCurrent = cur && findMesh(cur) ? cur : undefined; // compute before recording m
|
|
2442
2504
|
const prior = findMesh(m.space);
|
|
2443
2505
|
const origin = provenance === "refresh" && (prior?.origin === "manual" || prior?.origin === "catalog") ? prior.origin : "up";
|
|
2444
|
-
// A REFRESH starts nothing: it concluded the mesh is up from reachability alone,
|
|
2445
|
-
//
|
|
2446
|
-
//
|
|
2447
|
-
//
|
|
2448
|
-
//
|
|
2449
|
-
|
|
2450
|
-
|
|
2506
|
+
// A REFRESH starts nothing: it concluded the mesh is up from reachability alone, so `m` holds only
|
|
2507
|
+
// what THIS launch decided, and every other field is the record as it stands now. That covers the
|
|
2508
|
+
// operator's past decisions (origin, overlay consent, TLS requirement) and anything another writer
|
|
2509
|
+
// committed while the refresh ensured the control plane; rebuilding from the refresh's earlier
|
|
2510
|
+
// read reverted those and still exited 0. A `started` takeover may replace the record outright
|
|
2511
|
+
// (that launch really is the mesh now).
|
|
2512
|
+
if (provenance === "refresh" && !prior)
|
|
2513
|
+
throw new Error(`mesh "${m.space}" was removed from the registry while \`cotal up\` refreshed it - not re-recording it from an earlier read`);
|
|
2514
|
+
recordMesh(provenance === "refresh" ? { ...prior, ...m, origin } : { ...m, origin });
|
|
2451
2515
|
if (!usableCurrent) {
|
|
2452
2516
|
setCurrent(m.space);
|
|
2453
2517
|
return;
|