@cotal-ai/cli 0.48.2 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/commands/agents.d.ts +24 -5
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +25 -12
  4. package/dist/commands/agents.js.map +1 -1
  5. package/dist/commands/down.d.ts +18 -1
  6. package/dist/commands/down.d.ts.map +1 -1
  7. package/dist/commands/down.js +99 -3
  8. package/dist/commands/down.js.map +1 -1
  9. package/dist/commands/meshes.js +5 -5
  10. package/dist/commands/meshes.js.map +1 -1
  11. package/dist/commands/send.d.ts.map +1 -1
  12. package/dist/commands/send.js +2 -8
  13. package/dist/commands/send.js.map +1 -1
  14. package/dist/commands/spawn-manifest.d.ts.map +1 -1
  15. package/dist/commands/spawn-manifest.js +3 -2
  16. package/dist/commands/spawn-manifest.js.map +1 -1
  17. package/dist/commands/up.d.ts +13 -0
  18. package/dist/commands/up.d.ts.map +1 -1
  19. package/dist/commands/up.js +53 -4
  20. package/dist/commands/up.js.map +1 -1
  21. package/dist/commands/use.js +2 -2
  22. package/dist/commands/use.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/delivery-proc.d.ts +1 -0
  27. package/dist/lib/delivery-proc.d.ts.map +1 -1
  28. package/dist/lib/manager-proc.d.ts +15 -13
  29. package/dist/lib/manager-proc.d.ts.map +1 -1
  30. package/dist/lib/manager-proc.js +34 -6
  31. package/dist/lib/manager-proc.js.map +1 -1
  32. package/dist/lib/restore.d.ts.map +1 -1
  33. package/dist/lib/restore.js +6 -4
  34. package/dist/lib/restore.js.map +1 -1
  35. package/dist/lib/systemd-supervision.d.ts +15 -0
  36. package/dist/lib/systemd-supervision.d.ts.map +1 -0
  37. package/dist/lib/systemd-supervision.js +64 -0
  38. package/dist/lib/systemd-supervision.js.map +1 -0
  39. package/dist/lib/transient.d.ts +1 -10
  40. package/dist/lib/transient.d.ts.map +1 -1
  41. package/dist/lib/transient.js +4 -24
  42. package/dist/lib/transient.js.map +1 -1
  43. package/dist/seed/reconcile.d.ts.map +1 -1
  44. package/dist/seed/reconcile.js +27 -1
  45. package/dist/seed/reconcile.js.map +1 -1
  46. package/package.json +4 -4
@@ -6,15 +6,16 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync,
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, standaloneConnectOpts, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
8
8
  import { connect } from "@nats-io/transport-node";
9
- import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removeIdentityPin, writeIdentityPin, } from "@cotal-ai/workspace";
9
+ import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removeIdentityPin, writeIdentityPin, } from "@cotal-ai/workspace";
10
10
  import { ensureAuthService, resolveAuthProvider, stopAuthService } from "../lib/auth-proc.js";
11
11
  import { resolveSpace } from "../lib/status.js";
12
12
  import { c } from "../ui.js";
13
13
  import { resolveNatsServer } from "../lib/nats-bin.js";
14
14
  import { cotalPath, cotalRoot } from "../lib/paths.js";
15
15
  import { renderDetachedSummary } from "../lib/up-report.js";
16
+ import { detachedSystemdSupervisionWarning } from "../lib/systemd-supervision.js";
16
17
  import { deliveryUp, ensureControlPlane, stopDelivery } from "../lib/delivery-proc.js";
17
- import { managerHasDeliveryMarker, managerLogDisplayPath, managerUp, stopManager } from "../lib/manager-proc.js";
18
+ import { liveManagerWouldApplyMaxSessions, managerHasDeliveryMarker, managerLogDisplayPath, managerRecordState, managerUp, stopManager } from "../lib/manager-proc.js";
18
19
  import { loadManifest } from "../lib/manifest/index.js";
19
20
  import { buildLaunchSpec, genRunId, manifestToChannels, preflightConnectors, writeLaunchSpec } from "../lib/manifest/apply.js";
20
21
  import { renderUpPlan, renderInherited, renderWarnings } from "../lib/manifest/render.js";
@@ -48,6 +49,7 @@ export const upFlags = [
48
49
  { name: "rotate-sys", type: "boolean", description: "renew the expired/expiring $SYS creds by rotating the system account (agents, data and creds survive; needs a stopped mesh)" },
49
50
  { name: "detach", type: "boolean", description: "run in the background (stop with `cotal down`)" },
50
51
  { name: "runtime", type: "string", value: "<name>", description: "agent runtime for the mesh manager (default pty; extension runtimes are explicit-only, see `cotal runtimes`); with -f overrides the manifest's runtime" },
52
+ { name: "max-sessions", type: "string", value: "<n>", description: "live-session ceiling for the mesh manager (default 64; one session per console pane, so size for agents × panes). Recorded and reused by later manager launches" },
51
53
  { name: "file", type: "string", short: "f", value: "<cotal.yaml>", description: "launch a whole mesh from a manifest" },
52
54
  { name: "dry-run", type: "boolean", description: "with -f: print the plan, mutate nothing" },
53
55
  ];
@@ -367,6 +369,7 @@ async function runUp(args, inheritedLock, onAdopt) {
367
369
  throw new Error('--idp is for user-auth spaces; pair it with --user-auth, or set broker.auth: "user" in a manifest');
368
370
  }
369
371
  const publicExchange = publicExchangeArgs(values, wantUser);
372
+ const maxSessions = parsePositiveIntegerFlag("--max-sessions", values["max-sessions"]);
370
373
  // An open mesh has no operator, no system account, and no $SYS creds, so there is nothing to
371
374
  // rotate; the request is a misunderstanding to name, never a silent no-op that reports success.
372
375
  if (values["rotate-sys"] && values.open) {
@@ -455,6 +458,7 @@ async function runUp(args, inheritedLock, onAdopt) {
455
458
  userAuth: wantUser,
456
459
  idp: values.idp,
457
460
  rotateSys: values["rotate-sys"],
461
+ maxSessions,
458
462
  });
459
463
  return;
460
464
  }
@@ -472,6 +476,7 @@ async function runUp(args, inheritedLock, onAdopt) {
472
476
  userAuth: wantUser,
473
477
  idp: values.idp,
474
478
  rotateSys: values["rotate-sys"],
479
+ maxSessions,
475
480
  });
476
481
  }
477
482
  finally {
@@ -609,6 +614,18 @@ async function runUp(args, inheritedLock, onAdopt) {
609
614
  console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change its transport; \`cotal down\` it first, then \`cotal up --tls-cert <cert> --tls-key <key>\``));
610
615
  process.exit(1);
611
616
  }
617
+ // `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
618
+ // (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
619
+ // That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
620
+ // Refuse rather than warn: the flag either applies or the command fails. Matching the live
621
+ // argv is a no-op and may proceed. An unreadable command line cannot prove a match.
622
+ if (maxSessions !== undefined) {
623
+ const live = managerRecordState(undefined, undefined, held.space);
624
+ if (live.state === "alive" && !liveManagerWouldApplyMaxSessions(live.command, maxSessions)) {
625
+ console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running manager can't change --max-sessions (it is fixed at start); \`cotal down\` it first, then \`cotal up --max-sessions ${maxSessions}\``));
626
+ process.exit(1);
627
+ }
628
+ }
612
629
  // Live INFO must agree with the recorded/requested transport. A bare refresh that greets
613
630
  // "already running" over a plaintext listener while policy claims TLS is S5's second half —
614
631
  // the refuse left a durable lie and this path used to reprint TLS success over cleartext.
@@ -719,18 +736,20 @@ async function runUp(args, inheritedLock, onAdopt) {
719
736
  const controlPlane = await startDeliveryWithBroker(held.space, server, wantTls, {
720
737
  runtime: values.runtime,
721
738
  attachHost: attachHostFor(held.space, values.host),
739
+ maxSessions: maxSessionsFor(held.space, maxSessions),
722
740
  });
723
741
  if (!controlPlane)
724
742
  process.exitCode = 1;
725
743
  }
726
744
  const heldAttachHost = attachHostFor(held.space, values.host);
745
+ const heldMaxSessions = maxSessionsFor(held.space, maxSessions);
727
746
  // A broker was already answering here — this branch starts nothing, so it must not claim the
728
747
  // record as ours (see `Provenance`).
729
748
  // `tlsRequired` is CARRIED FORWARD, not re-derived. This branch starts no listener, so it has
730
749
  // no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
731
750
  // the field here would erase the requirement on every bare refresh, exactly the way dropping
732
751
  // `attachHost` would silently demote the mesh to loopback.
733
- recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ts: new Date().toISOString() }, "refresh");
752
+ recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ts: new Date().toISOString() }, "refresh");
734
753
  return;
735
754
  }
736
755
  const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
@@ -773,6 +792,7 @@ async function runUp(args, inheritedLock, onAdopt) {
773
792
  // from "nobody asked", or every mesh would persist an exposure decision it never made.
774
793
  host: values.host,
775
794
  runtime: values.runtime,
795
+ maxSessions,
776
796
  resumeAttempt,
777
797
  resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
778
798
  ...(restored ? {
@@ -795,6 +815,9 @@ async function runUp(args, inheritedLock, onAdopt) {
795
815
  // Transport policy is committed inside startMeshDetached before delivery launch (S5+S9).
796
816
  console.log(c.dim(`Started nats-server (${source}).`));
797
817
  console.log(c.green(renderDetachedSummary({ pid, delivery, authService: wantUser && authService, manager })));
818
+ const supervisionWarning = detachedSystemdSupervisionWarning();
819
+ if (supervisionWarning)
820
+ console.error(c.yellow(supervisionWarning));
798
821
  if (restored && process.env.COTAL_SMOKE_FAIL_AFTER_RESTORE_LISTENER_READY === "1")
799
822
  throw new Error("smoke-injected failure after restore listener readiness");
800
823
  // A user mesh whose auth service never became ready is recorded + running (a re-`cotal up`
@@ -949,6 +972,7 @@ async function runUp(args, inheritedLock, onAdopt) {
949
972
  // registry — and the record below is written whole, so reading it afterwards would find the
950
973
  // field this very call had just erased.
951
974
  const effectiveAttachHost = attachHostFor(space, values.host);
975
+ const effectiveMaxSessions = maxSessionsFor(space, maxSessions);
952
976
  recordOurMesh({
953
977
  space, server, root: cotalRoot(),
954
978
  mode: setup?.prepared ? "user" : useAuth ? "auth" : "open",
@@ -962,6 +986,7 @@ async function runUp(args, inheritedLock, onAdopt) {
962
986
  // previous launch. The bare case stays absent rather than recording the loopback default as
963
987
  // though the operator had chosen it.
964
988
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
989
+ ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
965
990
  ts: new Date().toISOString(),
966
991
  }, "started");
967
992
  // Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
@@ -974,6 +999,7 @@ async function runUp(args, inheritedLock, onAdopt) {
974
999
  // from another machine; without it the attach face stays loopback-only, so exposing terminals
975
1000
  // is never a side effect of anything but the operator binding the mesh somewhere reachable.
976
1001
  attachHost: effectiveAttachHost,
1002
+ maxSessions: effectiveMaxSessions,
977
1003
  resumeAttempt,
978
1004
  resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
979
1005
  wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
@@ -1314,6 +1340,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
1314
1340
  // record built without this field would erase the operator's decision, and the adopted manager
1315
1341
  // below would then be launched loopback-only from an entry that no longer remembers otherwise.
1316
1342
  const adoptAttachHost = attachHostFor(pending.space);
1343
+ const adoptMaxSessions = maxSessionsFor(pending.space);
1317
1344
  recordOurMesh({
1318
1345
  space: pending.space,
1319
1346
  server: pending.server,
@@ -1323,11 +1350,13 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
1323
1350
  tlsRequired: adoptedTlsRequired(pending.root),
1324
1351
  ...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
1325
1352
  ...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
1353
+ ...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
1326
1354
  ts: new Date().toISOString(),
1327
1355
  }, "started");
1328
1356
  const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
1329
1357
  runtime: pending.runtime,
1330
1358
  attachHost: adoptAttachHost,
1359
+ maxSessions: adoptMaxSessions,
1331
1360
  resumeAttempt: pending.attemptId,
1332
1361
  resumeCommitToken: pending.managerCommit?.durableCommitToken,
1333
1362
  });
@@ -1355,6 +1384,7 @@ async function resumeProvenRestoreListener(prepared, heldLock) {
1355
1384
  // Same ordering constraint as the pending-adopt path above: capture the recorded exposure before
1356
1385
  // `recordOurMesh` rewrites the entry, or a restore silently demotes the mesh to loopback attach.
1357
1386
  const restoreAttachHost = attachHostFor(prepared.space);
1387
+ const restoreMaxSessions = maxSessionsFor(prepared.space);
1358
1388
  recordOurMesh({
1359
1389
  space: prepared.space,
1360
1390
  server: prepared.server,
@@ -1365,11 +1395,13 @@ async function resumeProvenRestoreListener(prepared, heldLock) {
1365
1395
  tlsRequired: adoptedTlsRequired(prepared.root),
1366
1396
  ...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
1367
1397
  ...(restoreAttachHost ? { attachHost: restoreAttachHost } : {}),
1398
+ ...(restoreMaxSessions !== undefined ? { maxSessions: restoreMaxSessions } : {}),
1368
1399
  ts: new Date().toISOString(),
1369
1400
  }, "started");
1370
1401
  const controlPlane = await startDeliveryWithBroker(prepared.space, prepared.server, adoptedTlsRequired(prepared.root), {
1371
1402
  runtime: prepared.runtime,
1372
1403
  attachHost: restoreAttachHost,
1404
+ maxSessions: restoreMaxSessions,
1373
1405
  resumeAttempt: prepared.attemptId,
1374
1406
  resumeCommitToken: prepared.managerCommit?.durableCommitToken,
1375
1407
  });
@@ -1710,7 +1742,7 @@ async function upManifest(file, opts) {
1710
1742
  try {
1711
1743
  // `m.broker?.host`, not the defaulted `host`: same reason as the flag path — only a host the
1712
1744
  // manifest actually declared is an exposure decision worth persisting.
1713
- ({ pid, controlPlane, authService } = await startMeshDetached({ transport: opts.transport, server, space: m.space, open, userAuth, rotateSys: opts.rotateSys, host: m.broker?.host, seed: manifestToChannels(eff), runtime, launch: specPath }));
1745
+ ({ pid, controlPlane, authService } = await startMeshDetached({ transport: opts.transport, server, space: m.space, open, userAuth, rotateSys: opts.rotateSys, host: m.broker?.host, seed: manifestToChannels(eff), runtime, launch: specPath, maxSessions: opts.maxSessions }));
1714
1746
  }
1715
1747
  catch (e) {
1716
1748
  console.error(c.red(`✗ ${e.message}`));
@@ -1919,6 +1951,7 @@ opts) {
1919
1951
  // RESUMES an already-recorded mesh, whose exposure decision exists only in the registry entry the
1920
1952
  // call below rewrites.
1921
1953
  const effectiveAttachHost = attachHostFor(space, opts.host);
1954
+ const effectiveMaxSessions = maxSessionsFor(space, opts.maxSessions);
1922
1955
  recordOurMesh({
1923
1956
  space, server, root: cotalRoot(),
1924
1957
  mode: setup?.prepared ? "user" : useAuth ? "auth" : "open",
@@ -1928,6 +1961,7 @@ opts) {
1928
1961
  ...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
1929
1962
  // Persist only a real decision — declared now, or carried forward — never the loopback default.
1930
1963
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
1964
+ ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
1931
1965
  ts: new Date().toISOString(),
1932
1966
  }, "started");
1933
1967
  // Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
@@ -1939,6 +1973,7 @@ opts) {
1939
1973
  launch: opts.launch,
1940
1974
  // See the foreground path: the broker's bind address is what makes attach reachable off-box.
1941
1975
  attachHost: effectiveAttachHost,
1976
+ maxSessions: effectiveMaxSessions,
1942
1977
  resumeAttempt: opts.resumeAttempt,
1943
1978
  resumeCommitToken: opts.resumeCommitToken,
1944
1979
  wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
@@ -2049,6 +2084,20 @@ export async function claimSpace(space, server, root) {
2049
2084
  export function attachHostFor(space, explicit) {
2050
2085
  return explicit ?? findMesh(space)?.attachHost;
2051
2086
  }
2087
+ /**
2088
+ * The manager live-session ceiling for a launch into `space`.
2089
+ *
2090
+ * Like {@link attachHostFor}, this is an operator DECISION (`--max-sessions`), not something later
2091
+ * commands can reconstruct. Every later manager launch — same-root repair, resume, `spawn -f` —
2092
+ * has no flag of its own unless the operator typed one, so it reads the decision back. Without
2093
+ * this a replacement manager silently falls to 64 and the next console pane past that is refused.
2094
+ *
2095
+ * An explicit value on THIS invocation always wins. Absent both, undefined — the plane's own
2096
+ * default of 64, unchanged, and nothing is recorded as though chosen.
2097
+ */
2098
+ export function maxSessionsFor(space, explicit) {
2099
+ return explicit ?? findMesh(space)?.maxSessions;
2100
+ }
2052
2101
  /** {@link recordOurMesh} under a name that says it is a test seam: the origin rule it enforces is
2053
2102
  * asserted directly by the registry smoke, since reaching each branch through a full `up` would
2054
2103
  * need a live broker per case. */