@cotal-ai/cli 0.50.1 → 0.52.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 +32 -1
- package/dist/command.js.map +1 -1
- package/dist/commands/agents.d.ts +41 -2
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +85 -19
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +5 -1
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +9 -1
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +13 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +151 -7
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/endpoints.d.ts.map +1 -1
- package/dist/commands/endpoints.js +2 -1
- package/dist/commands/endpoints.js.map +1 -1
- package/dist/commands/join.d.ts.map +1 -1
- package/dist/commands/join.js +25 -0
- package/dist/commands/join.js.map +1 -1
- package/dist/commands/meshes-add.d.ts +2 -54
- package/dist/commands/meshes-add.d.ts.map +1 -1
- package/dist/commands/meshes-add.js +20 -105
- package/dist/commands/meshes-add.js.map +1 -1
- package/dist/commands/meshes.d.ts.map +1 -1
- package/dist/commands/meshes.js +5 -2
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/service.d.ts +17 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +541 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -8
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn.d.ts +44 -2
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +310 -47
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +133 -39
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts +50 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +284 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/up.d.ts +7 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +174 -23
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/update.d.ts +12 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +83 -9
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/use.js +1 -1
- package/dist/commands/use.js.map +1 -1
- package/dist/console/app.d.ts +14 -6
- package/dist/console/app.d.ts.map +1 -1
- package/dist/console/app.js +308 -40
- package/dist/console/app.js.map +1 -1
- package/dist/console/commands.d.ts +29 -1
- package/dist/console/commands.d.ts.map +1 -1
- package/dist/console/commands.js +109 -13
- package/dist/console/commands.js.map +1 -1
- package/dist/console/control.d.ts +141 -0
- package/dist/console/control.d.ts.map +1 -0
- package/dist/console/control.js +189 -0
- package/dist/console/control.js.map +1 -0
- package/dist/console/mesh.d.ts +1 -1
- package/dist/console/mesh.d.ts.map +1 -1
- package/dist/console/root.d.ts +13 -1
- package/dist/console/root.d.ts.map +1 -1
- package/dist/console/root.js +25 -5
- package/dist/console/root.js.map +1 -1
- package/dist/console/ui/CommandPalette.d.ts +2 -1
- package/dist/console/ui/CommandPalette.d.ts.map +1 -1
- package/dist/console/ui/CommandPalette.js +3 -2
- package/dist/console/ui/CommandPalette.js.map +1 -1
- package/dist/console/ui/Confirm.d.ts +13 -4
- package/dist/console/ui/Confirm.d.ts.map +1 -1
- package/dist/console/ui/Confirm.js +13 -5
- package/dist/console/ui/Confirm.js.map +1 -1
- package/dist/console/ui/Detail.d.ts +8 -1
- package/dist/console/ui/Detail.d.ts.map +1 -1
- package/dist/console/ui/Detail.js +11 -4
- package/dist/console/ui/Detail.js.map +1 -1
- package/dist/console/ui/Help.d.ts.map +1 -1
- package/dist/console/ui/Help.js +3 -2
- package/dist/console/ui/Help.js.map +1 -1
- package/dist/console/ui/Roster.d.ts +9 -1
- package/dist/console/ui/Roster.d.ts.map +1 -1
- package/dist/console/ui/Roster.js +16 -5
- package/dist/console/ui/Roster.js.map +1 -1
- package/dist/console/ui/Sparkline.d.ts +13 -0
- package/dist/console/ui/Sparkline.d.ts.map +1 -0
- package/dist/console/ui/Sparkline.js +21 -0
- package/dist/console/ui/Sparkline.js.map +1 -0
- package/dist/console/ui/StatusBar.d.ts +4 -1
- package/dist/console/ui/StatusBar.d.ts.map +1 -1
- package/dist/console/ui/StatusBar.js +5 -3
- package/dist/console/ui/StatusBar.js.map +1 -1
- package/dist/console/ui/Tabs.d.ts +4 -2
- package/dist/console/ui/Tabs.d.ts.map +1 -1
- package/dist/console/ui/Tabs.js +5 -3
- package/dist/console/ui/Tabs.js.map +1 -1
- package/dist/console/ui/topo/Map.d.ts.map +1 -1
- package/dist/console/ui/topo/Map.js +34 -2
- package/dist/console/ui/topo/Map.js.map +1 -1
- package/dist/console/ui/topo/Matrix.d.ts.map +1 -1
- package/dist/console/ui/topo/Matrix.js +12 -3
- package/dist/console/ui/topo/Matrix.js.map +1 -1
- package/dist/console/ui/topo/Sequence.js +1 -1
- package/dist/console/ui/topo/Sequence.js.map +1 -1
- package/dist/console/ui/topo/Topo.d.ts +10 -2
- package/dist/console/ui/topo/Topo.d.ts.map +1 -1
- package/dist/console/ui/topo/Topo.js +6 -4
- package/dist/console/ui/topo/Topo.js.map +1 -1
- package/dist/console/ui/topo/model.d.ts +50 -2
- package/dist/console/ui/topo/model.d.ts.map +1 -1
- package/dist/console/ui/topo/model.js +69 -6
- package/dist/console/ui/topo/model.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/attach-client.d.ts.map +1 -1
- package/dist/lib/attach-client.js +7 -1
- package/dist/lib/attach-client.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts +10 -2
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +7 -1
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/manifest/apply.d.ts.map +1 -1
- package/dist/lib/manifest/apply.js +2 -0
- package/dist/lib/manifest/apply.js.map +1 -1
- package/dist/lib/manifest/model.d.ts +2 -0
- package/dist/lib/manifest/model.d.ts.map +1 -1
- package/dist/lib/manifest/prepare.d.ts +2 -0
- package/dist/lib/manifest/prepare.d.ts.map +1 -1
- package/dist/lib/manifest/prepare.js +1 -0
- package/dist/lib/manifest/prepare.js.map +1 -1
- package/dist/lib/manifest/resolve.js +1 -0
- package/dist/lib/manifest/resolve.js.map +1 -1
- package/dist/lib/manifest/schema.d.ts +2 -0
- package/dist/lib/manifest/schema.d.ts.map +1 -1
- package/dist/lib/manifest/schema.js +2 -0
- package/dist/lib/manifest/schema.js.map +1 -1
- package/dist/lib/restore.d.ts +3 -0
- package/dist/lib/restore.d.ts.map +1 -1
- package/dist/lib/restore.js +17 -0
- package/dist/lib/restore.js.map +1 -1
- package/dist/lib/seat-admission.d.ts +51 -0
- package/dist/lib/seat-admission.d.ts.map +1 -0
- package/dist/lib/seat-admission.js +81 -0
- package/dist/lib/seat-admission.js.map +1 -0
- package/dist/lib/seat-capture.d.ts +47 -0
- package/dist/lib/seat-capture.d.ts.map +1 -0
- package/dist/lib/seat-capture.js +195 -0
- package/dist/lib/seat-capture.js.map +1 -0
- package/dist/lib/seat-restore.d.ts +39 -0
- package/dist/lib/seat-restore.d.ts.map +1 -0
- package/dist/lib/seat-restore.js +249 -0
- package/dist/lib/seat-restore.js.map +1 -0
- package/dist/lib/seat-resume.d.ts +24 -0
- package/dist/lib/seat-resume.d.ts.map +1 -0
- package/dist/lib/seat-resume.js +127 -0
- package/dist/lib/seat-resume.js.map +1 -0
- package/dist/view/mesh-view.d.ts +41 -1
- package/dist/view/mesh-view.d.ts.map +1 -1
- package/dist/view/mesh-view.js +107 -8
- package/dist/view/mesh-view.js.map +1 -1
- package/package.json +14 -7
- package/dist/lib/join-target.d.ts +0 -173
- package/dist/lib/join-target.d.ts.map +0 -1
- package/dist/lib/join-target.js +0 -335
- package/dist/lib/join-target.js.map +0 -1
package/dist/commands/up.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
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, } from "node:fs";
|
|
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, 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";
|
|
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, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, 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
|
+
import { admitAndRestoreSeatCheckpoints } from "../lib/seat-resume.js";
|
|
13
14
|
import { resolveNatsServer } from "../lib/nats-bin.js";
|
|
14
15
|
import { cotalPath, cotalRoot } from "../lib/paths.js";
|
|
15
16
|
import { renderDetachedSummary } from "../lib/up-report.js";
|
|
@@ -31,15 +32,17 @@ const pendingOrdinaryResumes = new Map();
|
|
|
31
32
|
/** `cotal up` flags — colocated with the command (like `spawnFlags`) so its completion can read them. */
|
|
32
33
|
export const upFlags = [
|
|
33
34
|
{ name: "server", type: "string", value: "<url>", description: "listen URL override" },
|
|
34
|
-
{ name: "host", type: "string", value: "<host>", description: "bind host override" },
|
|
35
|
+
{ name: "host", type: "string", value: "<host>", description: "bind host override (an IP/hostname, not a URL; the URL is --server)" },
|
|
35
36
|
{ name: "space", type: "string", value: "<s>", description: "space name (default: the folder's)" },
|
|
36
37
|
{ name: "store-dir", type: "string", value: "<dir>", description: "JetStream store directory" },
|
|
38
|
+
{ name: "max-file-store", type: "string", value: "<bytes>", description: "JetStream file storage cap in bytes (default: nats-server's dynamic cap). Fixed at broker start; recorded and kept across `down --preserve-state`" },
|
|
37
39
|
{ name: "channels", type: "string", value: "<path>", description: "channel-registry seed file (JSON; default .cotal/channels.json)" },
|
|
38
40
|
{ name: "tls-cert", type: "string", value: "<file>", description: "serve the broker over TLS with this certificate (requires --tls-key)" },
|
|
39
41
|
{ name: "tls-key", type: "string", value: "<file>", description: "the private key for --tls-cert" },
|
|
40
42
|
{ name: "restore", type: "string", value: "<dir>", description: "restore an offline backup before exposing the normal listener" },
|
|
41
43
|
{ name: "restore-only", type: "string", value: "<registry>", description: "restore only the registry component" },
|
|
42
44
|
{ name: "accept-missing-source", type: "boolean", description: "explicit disaster consent when the inode-bound preserved source is absent" },
|
|
45
|
+
{ name: "accept-stale-checkpoint", type: "boolean", description: "explicit consent to resume a seat checkpoint captured outside its recorded recency horizon" },
|
|
43
46
|
{ name: "open", type: "boolean", description: "unauthenticated dev mesh (no JWT/ACLs)" },
|
|
44
47
|
{ name: "user-auth", type: "boolean", description: "per-USER auth: login + bearer through the space's auth service" },
|
|
45
48
|
{ name: "idp", type: "string", value: "<url>", description: "with --user-auth: the IdP auth base URL to pin (first enable)" },
|
|
@@ -52,6 +55,7 @@ export const upFlags = [
|
|
|
52
55
|
{ name: "detach", type: "boolean", description: "run in the background (stop with `cotal down`)" },
|
|
53
56
|
{ 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" },
|
|
54
57
|
{ 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" },
|
|
58
|
+
{ name: "no-manager", type: "boolean", description: "broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager (refuses on a mesh whose manager is live)" },
|
|
55
59
|
{ name: "file", type: "string", short: "f", value: "<cotal.yaml>", description: "launch a whole mesh from a manifest" },
|
|
56
60
|
{ name: "dry-run", type: "boolean", description: "with -f: print the plan, mutate nothing" },
|
|
57
61
|
];
|
|
@@ -143,6 +147,11 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
143
147
|
// read. A rotation is a stopped, fresh boot; a half-finished maintenance attempt is neither.
|
|
144
148
|
if (values["rotate-sys"] && (values.__restoreAttempt || values.__ordinaryResumeAttempt))
|
|
145
149
|
throw new Error("--rotate-sys cannot run during a restore/resume re-entry - finish or roll back the maintenance attempt, then `cotal down` and `cotal up --rotate-sys`");
|
|
150
|
+
// Same seam, same reason: a restore/resume re-entry needs the manager to commit the preserved
|
|
151
|
+
// inventory (completeResumeActivation asks it for resumePreserved/commitResume/finalizeResume), so
|
|
152
|
+
// a broker-only ask there is a contradiction to refuse, never a flag to drop mid-recovery.
|
|
153
|
+
if (values["no-manager"] && (values.__restoreAttempt || values.__ordinaryResumeAttempt))
|
|
154
|
+
throw new Error("--no-manager cannot run during a restore/resume re-entry - the resume needs a manager to commit the preserved inventory; finish or roll back the maintenance attempt first");
|
|
146
155
|
if (!values.__restoreAttempt && !values.__ordinaryResumeAttempt && !values.file) {
|
|
147
156
|
const root = cotalRoot();
|
|
148
157
|
// Same refusal for the AUTO-recovered journal, raised before the recovery is prepared and
|
|
@@ -150,6 +159,11 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
150
159
|
// `up` that has already begun adopting a maintenance attempt.
|
|
151
160
|
if (values["rotate-sys"] && readMaintenanceJournal(root))
|
|
152
161
|
throw new Error("--rotate-sys is refused while this root has a maintenance attempt to recover - finish or roll back that attempt (`cotal up` alone recovers it), then `cotal down` and `cotal up --rotate-sys`");
|
|
162
|
+
// Raised here for the stronger reason the journal itself carries: a pending maintenance attempt
|
|
163
|
+
// means the store is mid-recovery, and a broker-only boot over it is wrong with or without the
|
|
164
|
+
// manager half. The manager half is why the FLAG specifically cannot apply.
|
|
165
|
+
if (values["no-manager"] && readMaintenanceJournal(root))
|
|
166
|
+
throw new Error("--no-manager is refused while this root has a maintenance attempt to recover - a resume needs a manager to commit the preserved inventory; recover it (`cotal up` alone) or roll it back first");
|
|
153
167
|
const lock = acquireMaintenanceLock(root);
|
|
154
168
|
let pending;
|
|
155
169
|
let recoveredRestore;
|
|
@@ -203,9 +217,19 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
203
217
|
// Journal the CANONICAL identity-checked path, never the caller's spelling: a relative or
|
|
204
218
|
// symlinked spelling re-resolved later (other cwd, retargeted link) could open another store.
|
|
205
219
|
const resumeStore = journal.source.path;
|
|
220
|
+
// The cap is part of the preserved launch, like the store: a resume re-renders it, and a
|
|
221
|
+
// different --max-file-store is refused rather than silently resizing the preserved broker.
|
|
222
|
+
const resumeMaxFileStore = journaledMaxFileStore(launch.maxFileStore);
|
|
223
|
+
const requestedMaxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
|
|
224
|
+
if (requestedMaxFileStore !== undefined && requestedMaxFileStore !== resumeMaxFileStore)
|
|
225
|
+
throw new Error(`--max-file-store ${requestedMaxFileStore} is not the preserved launch cap (${resumeMaxFileStore ?? "unset"}); ordinary up resumes exactly the preserved cap - omit --max-file-store`);
|
|
206
226
|
if (values.runtime && runtimes[0] && values.runtime !== runtimes[0])
|
|
207
227
|
throw new Error(`--runtime ${values.runtime} contradicts the preserved agent runtime ${runtimes[0]}; omit it to resume the same principals`);
|
|
208
228
|
const resumeRuntime = values.runtime ?? runtimes[0] ?? "pty";
|
|
229
|
+
// Admit every seat checkpoint the cut wrote and take custody, BEFORE the resume attempt is
|
|
230
|
+
// journalled and long before a manager starts. A refusal here costs nothing because
|
|
231
|
+
// nothing has been started; the same refusal after a launch would be a second writer.
|
|
232
|
+
const staleConsent = admitAndRestoreSeatCheckpoints(root, journal.space, journal.cut.attemptId, values, resume.inventory);
|
|
209
233
|
const serverNonce = randomUUID().replaceAll("-", "");
|
|
210
234
|
const serverName = `${attemptId}-${serverNonce}`;
|
|
211
235
|
beginOrdinaryResume(lock, {
|
|
@@ -213,10 +237,17 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
213
237
|
launch: {
|
|
214
238
|
server: resumeServer,
|
|
215
239
|
storeDir: resumeStore,
|
|
240
|
+
...(resumeMaxFileStore !== undefined ? { maxFileStore: resumeMaxFileStore } : {}),
|
|
216
241
|
runtime: resumeRuntime,
|
|
217
242
|
detached: Boolean(values.detach),
|
|
218
243
|
serverName,
|
|
219
244
|
serverNonce,
|
|
245
|
+
// Durable evidence of consent, not a print. An operator asking later why a seat resumed
|
|
246
|
+
// from a checkpoint past its horizon must be able to read that it was admitted, for
|
|
247
|
+
// which seat, and by how far, from the journal rather than from a lost terminal.
|
|
248
|
+
...(staleConsent.length
|
|
249
|
+
? { acceptedStaleCheckpoints: staleConsent.map((seat) => ({ ...seat })) }
|
|
250
|
+
: {}),
|
|
220
251
|
},
|
|
221
252
|
});
|
|
222
253
|
pending = {
|
|
@@ -226,6 +257,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
226
257
|
mode: journal.mode,
|
|
227
258
|
server: resumeServer,
|
|
228
259
|
storeDir: resumeStore,
|
|
260
|
+
...(resumeMaxFileStore !== undefined ? { maxFileStore: resumeMaxFileStore } : {}),
|
|
229
261
|
runtime: resumeRuntime,
|
|
230
262
|
detached: Boolean(values.detach),
|
|
231
263
|
inventory: resume.inventory,
|
|
@@ -239,6 +271,10 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
239
271
|
const resume = readMaintenanceResumeDocument(root, journal.resume);
|
|
240
272
|
const launch = journal.ordinaryResume.launch;
|
|
241
273
|
const attemptId = journal.ordinaryResume.attemptId;
|
|
274
|
+
const recoveredMaxFileStore = journaledMaxFileStore(launch.maxFileStore);
|
|
275
|
+
const requestedMaxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
|
|
276
|
+
if (requestedMaxFileStore !== undefined && requestedMaxFileStore !== recoveredMaxFileStore)
|
|
277
|
+
throw new Error(`--max-file-store ${requestedMaxFileStore} is not the preserved launch cap (${recoveredMaxFileStore ?? "unset"}); resume attempt ${attemptId} re-renders exactly the preserved cap - omit --max-file-store`);
|
|
242
278
|
// Recovery re-asserts the source identity rather than trusting the journaled spelling.
|
|
243
279
|
assertStoreIdentity(journal.source);
|
|
244
280
|
// The bound listener decides re-entry: a live exact listener is ADOPTED, a provably dead
|
|
@@ -283,6 +319,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
283
319
|
mode: journal.mode,
|
|
284
320
|
server: typeof launch.server === "string" ? launch.server : DEFAULT_SERVER,
|
|
285
321
|
storeDir: journal.source.path,
|
|
322
|
+
...(recoveredMaxFileStore !== undefined ? { maxFileStore: recoveredMaxFileStore } : {}),
|
|
286
323
|
runtime: typeof launch.runtime === "string" ? launch.runtime : "pty",
|
|
287
324
|
detached: launch.detached === true,
|
|
288
325
|
inventory: resume.inventory,
|
|
@@ -341,6 +378,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
341
378
|
space: pending.space,
|
|
342
379
|
server: pending.server,
|
|
343
380
|
"store-dir": pending.storeDir,
|
|
381
|
+
"max-file-store": pending.maxFileStore === undefined ? undefined : String(pending.maxFileStore),
|
|
344
382
|
runtime: pending.runtime,
|
|
345
383
|
detach: pending.detached,
|
|
346
384
|
open: pending.mode === "open",
|
|
@@ -370,8 +408,22 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
370
408
|
if (values.idp && !wantUser && !values.file) {
|
|
371
409
|
throw new Error('--idp is for user-auth spaces; pair it with --user-auth, or set broker.auth: "user" in a manifest');
|
|
372
410
|
}
|
|
411
|
+
// BROKER-ONLY MODE (#1417). `--no-manager` names what it omits, and the two manager-shaping flags
|
|
412
|
+
// are refused beside it rather than silently dropped: a runtime selected or a ceiling recorded for
|
|
413
|
+
// a manager that will never start is a misunderstanding, and dropping it quietly is the exact
|
|
414
|
+
// silent-no-op shape this repo refuses everywhere else.
|
|
415
|
+
const noManager = Boolean(values["no-manager"]);
|
|
416
|
+
if (noManager && values.runtime)
|
|
417
|
+
throw new Error("--no-manager cannot be combined with --runtime - no manager starts, so there is no runtime to select; drop --runtime, or drop --no-manager");
|
|
418
|
+
if (noManager && values["max-sessions"])
|
|
419
|
+
throw new Error("--no-manager cannot be combined with --max-sessions - no manager starts, so there is no session ceiling to set; drop --max-sessions, or drop --no-manager");
|
|
373
420
|
const publicExchange = publicExchangeArgs(values, wantUser);
|
|
374
421
|
const maxSessions = parsePositiveIntegerFlag("--max-sessions", values["max-sessions"]);
|
|
422
|
+
const maxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
|
|
423
|
+
// The manifest path renders its broker from the manifest, which has no store fields, so the flag
|
|
424
|
+
// would be accepted and never rendered. Refuse it rather than drop it.
|
|
425
|
+
if (maxFileStore !== undefined && values.file)
|
|
426
|
+
throw new Error("--max-file-store cannot be combined with --file/-f - a manifest launch does not render a store cap; drop --max-file-store, or start the mesh without -f");
|
|
375
427
|
// An open mesh has no operator, no system account, and no $SYS creds, so there is nothing to
|
|
376
428
|
// rotate; the request is a misunderstanding to name, never a silent no-op that reports success.
|
|
377
429
|
if (values["rotate-sys"] && values.open) {
|
|
@@ -461,6 +513,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
461
513
|
idp: values.idp,
|
|
462
514
|
rotateSys: values["rotate-sys"],
|
|
463
515
|
maxSessions,
|
|
516
|
+
noManager,
|
|
464
517
|
});
|
|
465
518
|
return;
|
|
466
519
|
}
|
|
@@ -479,6 +532,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
479
532
|
idp: values.idp,
|
|
480
533
|
rotateSys: values["rotate-sys"],
|
|
481
534
|
maxSessions,
|
|
535
|
+
noManager,
|
|
482
536
|
});
|
|
483
537
|
}
|
|
484
538
|
finally {
|
|
@@ -616,6 +670,13 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
616
670
|
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>\``));
|
|
617
671
|
process.exit(1);
|
|
618
672
|
}
|
|
673
|
+
// The store cap is fixed when nats-server reads its config, and nats-server refuses a reload
|
|
674
|
+
// from its dynamic cap to an explicit one. This branch starts nothing, so a different cap can
|
|
675
|
+
// only be refused; the same cap is a no-op.
|
|
676
|
+
if (maxFileStore !== undefined && maxFileStore !== held.maxFileStore) {
|
|
677
|
+
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --max-file-store (it is fixed at start); \`cotal down\` it first, then \`cotal up --max-file-store ${maxFileStore}\``));
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
619
680
|
// `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
|
|
620
681
|
// (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
|
|
621
682
|
// That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
|
|
@@ -628,6 +689,18 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
628
689
|
process.exit(1);
|
|
629
690
|
}
|
|
630
691
|
}
|
|
692
|
+
// `--no-manager` on a refresh whose manager is LIVE is the same class of refusal: this branch
|
|
693
|
+
// starts nothing, so the flag cannot apply, and silently keeping the manager would answer a
|
|
694
|
+
// broker-only ask with a full stack while silently stopping it would kill agents this command
|
|
695
|
+
// was never asked to touch. Same shape as `--runtime`/`--max-sessions` above: refuse, name the
|
|
696
|
+
// exact remedy. A refresh of a mesh whose manager is NOT live simply proceeds broker-only.
|
|
697
|
+
if (noManager) {
|
|
698
|
+
const live = managerRecordState(undefined, undefined, held.space);
|
|
699
|
+
if (live.state === "alive") {
|
|
700
|
+
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} with a live manager - \`cotal up --no-manager\` will not keep or stop one; \`cotal down manager\` first, then \`cotal up --no-manager\``));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
631
704
|
// Live INFO must agree with the recorded/requested transport. A bare refresh that greets
|
|
632
705
|
// "already running" over a plaintext listener while policy claims TLS is S5's second half —
|
|
633
706
|
// the refuse left a durable lie and this path used to reprint TLS success over cleartext.
|
|
@@ -739,6 +812,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
739
812
|
runtime: values.runtime,
|
|
740
813
|
attachHost: attachHostFor(held.space, values.host),
|
|
741
814
|
maxSessions: maxSessionsFor(held.space, maxSessions),
|
|
815
|
+
noManager,
|
|
742
816
|
});
|
|
743
817
|
if (!controlPlane)
|
|
744
818
|
process.exitCode = 1;
|
|
@@ -751,7 +825,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
751
825
|
// no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
|
|
752
826
|
// the field here would erase the requirement on every bare refresh, exactly the way dropping
|
|
753
827
|
// `attachHost` would silently demote the mesh to loopback.
|
|
754
|
-
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");
|
|
828
|
+
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 } : {}), ts: new Date().toISOString() }, "refresh");
|
|
755
829
|
return;
|
|
756
830
|
}
|
|
757
831
|
const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
|
|
@@ -783,6 +857,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
783
857
|
transport,
|
|
784
858
|
server,
|
|
785
859
|
storeDir: values["store-dir"],
|
|
860
|
+
maxFileStore,
|
|
786
861
|
space: values.space,
|
|
787
862
|
open: values.open,
|
|
788
863
|
userAuth: wantUser ? { idpUrl: values.idp } : undefined,
|
|
@@ -795,6 +870,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
795
870
|
host: values.host,
|
|
796
871
|
runtime: values.runtime,
|
|
797
872
|
maxSessions,
|
|
873
|
+
noManager,
|
|
798
874
|
resumeAttempt,
|
|
799
875
|
resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
|
|
800
876
|
...(restored ? {
|
|
@@ -850,14 +926,14 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
850
926
|
// Decided above the branch; the dial host is only settled here (a port collision may have moved
|
|
851
927
|
// the server, and the hostname is what the certificate has to match).
|
|
852
928
|
assertServesDialHost(transport, new URL(server).hostname);
|
|
853
|
-
const setup = useAuth ? await authSetup(storeDir, server, space, host, wantUser ? { idpUrl: values.idp } : undefined, transport, values["rotate-sys"]) : undefined;
|
|
929
|
+
const setup = useAuth ? await authSetup(storeDir, server, space, host, wantUser ? { idpUrl: values.idp } : undefined, transport, values["rotate-sys"], maxFileStore) : undefined;
|
|
854
930
|
const port = Number(new URL(server).port) || 4222;
|
|
855
931
|
const restored = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
856
932
|
// Both modes go through a RENDERER, never bare CLI flags. Open mode used to start from
|
|
857
933
|
// `-js -sd … -p … -a …`, which never called a renderer at all — so the required transport union
|
|
858
934
|
// protected the auth path and was silent on the open one, and a cert/key pair passed to an
|
|
859
935
|
// open-mode `up` would have been accepted while the listener came up in cleartext.
|
|
860
|
-
const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport });
|
|
936
|
+
const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport, maxFileStore });
|
|
861
937
|
const natsArgs = [
|
|
862
938
|
"-c", confPath,
|
|
863
939
|
...(restored ? ["--name", restored.serverName]
|
|
@@ -989,6 +1065,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
989
1065
|
// though the operator had chosen it.
|
|
990
1066
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
991
1067
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
1068
|
+
...(maxFileStore !== undefined ? { maxFileStore } : {}),
|
|
992
1069
|
ts: new Date().toISOString(),
|
|
993
1070
|
}, "started");
|
|
994
1071
|
// Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
|
|
@@ -1002,6 +1079,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1002
1079
|
// is never a side effect of anything but the operator binding the mesh somewhere reachable.
|
|
1003
1080
|
attachHost: effectiveAttachHost,
|
|
1004
1081
|
maxSessions: effectiveMaxSessions,
|
|
1082
|
+
noManager,
|
|
1005
1083
|
resumeAttempt,
|
|
1006
1084
|
resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
|
|
1007
1085
|
wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
|
|
@@ -1353,6 +1431,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
|
|
|
1353
1431
|
...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
|
|
1354
1432
|
...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
|
|
1355
1433
|
...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
|
|
1434
|
+
...(pending.maxFileStore !== undefined ? { maxFileStore: pending.maxFileStore } : {}),
|
|
1356
1435
|
ts: new Date().toISOString(),
|
|
1357
1436
|
}, "started");
|
|
1358
1437
|
const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
|
|
@@ -1671,10 +1750,16 @@ async function upManifest(file, opts) {
|
|
|
1671
1750
|
const host = m.broker?.host ?? "127.0.0.1";
|
|
1672
1751
|
// Same bind-vs-probe reconciliation as the flag path: a manifest that sets `broker.host` without
|
|
1673
1752
|
// an explicit `broker.servers` would otherwise bind one address and be probed at another.
|
|
1674
|
-
const server = m.broker?.host ? reconcileHostAndServer(m.broker.host, m.broker?.servers) : (m.broker?.servers ?? DEFAULT_SERVER);
|
|
1753
|
+
const server = m.broker?.host ? reconcileHostAndServer(m.broker.host, m.broker?.servers, "broker.host", "broker.servers") : (m.broker?.servers ?? DEFAULT_SERVER);
|
|
1675
1754
|
const open = m.broker?.auth === false; // default is auth
|
|
1676
1755
|
const userAuth = m.broker?.auth === "user" ? { idpUrl: opts.idp ?? m.broker?.idp } : undefined; // flag > manifest
|
|
1677
1756
|
const runtime = m.runtime ?? "pty";
|
|
1757
|
+
// A manifest's agents are booted BY the manager `up -f` starts with the mesh, so `--no-manager`
|
|
1758
|
+
// against a manifest declaring agents asks for a boot that cannot deliver its own plan. Refused
|
|
1759
|
+
// before the dry-run print and before anything boots, with the two workable spellings named.
|
|
1760
|
+
if (opts.noManager && eff.agents.length)
|
|
1761
|
+
throw new Error(`--no-manager cannot launch this manifest: it declares ${eff.agents.length} agent(s), and a manifest's agents are booted by its manager (which --no-manager omits). Deploy it from a manager host with \`cotal spawn -f\`, or drop --no-manager.`);
|
|
1762
|
+
const effectiveNoManager = Boolean(opts.noManager);
|
|
1678
1763
|
// The open-mesh refusal must be RE-STATED here, not only against the CLI `--open` flag: on this
|
|
1679
1764
|
// path openness comes from the MANIFEST (`broker.auth: false`), which the flag-level guard cannot
|
|
1680
1765
|
// see. Without it, `cotal up -f open.yaml --rotate-sys` boots an open broker, exits 0 and rotates
|
|
@@ -1691,6 +1776,10 @@ async function upManifest(file, opts) {
|
|
|
1691
1776
|
if (opts.transport.kind === "tls-required")
|
|
1692
1777
|
announceTransport(opts.transport);
|
|
1693
1778
|
console.log(renderUpPlan(eff, server));
|
|
1779
|
+
// The omission is part of the plan (#1417): a dry run that lists a manager the real boot
|
|
1780
|
+
// will not start is the exact "green-lit a launch the applying command refuses" failure.
|
|
1781
|
+
if (effectiveNoManager)
|
|
1782
|
+
console.log(c.dim(" no manager will start on this host (--no-manager): broker, channels and, in auth mode, the delivery daemon come up, and no agents launch from here"));
|
|
1694
1783
|
return;
|
|
1695
1784
|
}
|
|
1696
1785
|
// Preflight an extension runtime before starting the broker. The detached manager re-execs this
|
|
@@ -1744,7 +1833,7 @@ async function upManifest(file, opts) {
|
|
|
1744
1833
|
try {
|
|
1745
1834
|
// `m.broker?.host`, not the defaulted `host`: same reason as the flag path — only a host the
|
|
1746
1835
|
// manifest actually declared is an exposure decision worth persisting.
|
|
1747
|
-
({ 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 }));
|
|
1836
|
+
({ 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, noManager: effectiveNoManager }));
|
|
1748
1837
|
}
|
|
1749
1838
|
catch (e) {
|
|
1750
1839
|
console.error(c.red(`✗ ${e.message}`));
|
|
@@ -1755,7 +1844,12 @@ async function upManifest(file, opts) {
|
|
|
1755
1844
|
console.log(c.dim(` seeded ${m.channels.length} channel(s): ${m.channels.map((ch) => "#" + ch.name).join(", ")}`));
|
|
1756
1845
|
// Never claim a launch the control plane can't deliver: the manager carries the launch spec, so a
|
|
1757
1846
|
// degraded control plane (announced above) means the agents are NOT coming up.
|
|
1758
|
-
if (
|
|
1847
|
+
if (effectiveNoManager) {
|
|
1848
|
+
// Truthful summary for the broker-only boot (#1417): nothing was omitted by accident, and the
|
|
1849
|
+
// agents this manifest declares (none — a declaring manifest is refused above) launch elsewhere.
|
|
1850
|
+
console.log(c.dim(` broker-only (--no-manager): no manager started on this host - run \`cotal supervise --space ${m.space} --server ${server}\` from a manager host to manage agents`));
|
|
1851
|
+
}
|
|
1852
|
+
else if (controlPlane) {
|
|
1759
1853
|
// U6: on a user mesh the agents run under the OPERATOR's identity — say whose they are.
|
|
1760
1854
|
console.log(c.green(`✓ launching ${eff.agents.length} agent(s)`) + c.dim(` via manager (${runtime})${owner ? ` as you (owner ${owner})` : ""} - see ${managerLogDisplayPath(m.space)}`));
|
|
1761
1855
|
}
|
|
@@ -1768,7 +1862,7 @@ async function upManifest(file, opts) {
|
|
|
1768
1862
|
console.log("\n" + inherited);
|
|
1769
1863
|
if (eff.warnings.length)
|
|
1770
1864
|
console.log("\n" + renderWarnings(eff.warnings));
|
|
1771
|
-
console.log(c.dim(`\nWatch: \`cotal console --space ${m.space}\`
|
|
1865
|
+
console.log(c.dim(`\nWatch: \`cotal web\` (browser dashboard) or \`cotal console --space ${m.space}\` (terminal) · Tear down: \`cotal down\``));
|
|
1772
1866
|
// A declared-user-auth manifest whose auth service never became ready: the mesh is up + recorded
|
|
1773
1867
|
// (re-`cotal up` heals), but this launch did not deliver a usable identity plane — exit non-zero
|
|
1774
1868
|
// so CI/wrappers don't read success (the red consequence line printed at the failure above).
|
|
@@ -1873,11 +1967,11 @@ opts) {
|
|
|
1873
1967
|
// effective server is finally known — a manifest may name a different host than the flags did, and
|
|
1874
1968
|
// a certificate that is valid for one is not thereby valid for the other.
|
|
1875
1969
|
const transport = assertServesDialHost(opts.transport, new URL(server).hostname);
|
|
1876
|
-
const setup = useAuth ? await authSetup(storeDir, server, space, host, opts.userAuth, transport, opts.rotateSys) : undefined;
|
|
1970
|
+
const setup = useAuth ? await authSetup(storeDir, server, space, host, opts.userAuth, transport, opts.rotateSys, opts.maxFileStore) : undefined;
|
|
1877
1971
|
const port = Number(new URL(server).port) || 4222;
|
|
1878
1972
|
// Same rule as the foreground path: every route to a listener names its transport (see
|
|
1879
1973
|
// `writeOpenBrokerConf`). Detach must not be the mode where the fence quietly does not apply.
|
|
1880
|
-
const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport });
|
|
1974
|
+
const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport, maxFileStore: opts.maxFileStore });
|
|
1881
1975
|
const args = [
|
|
1882
1976
|
"-c", confPath,
|
|
1883
1977
|
...(opts.boundListener ? ["--name", opts.boundListener.serverName] : []),
|
|
@@ -1973,6 +2067,7 @@ opts) {
|
|
|
1973
2067
|
// Persist only a real decision — declared now, or carried forward — never the loopback default.
|
|
1974
2068
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
1975
2069
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
2070
|
+
...(opts.maxFileStore !== undefined ? { maxFileStore: opts.maxFileStore } : {}),
|
|
1976
2071
|
ts: new Date().toISOString(),
|
|
1977
2072
|
}, "started");
|
|
1978
2073
|
// Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
|
|
@@ -1985,6 +2080,7 @@ opts) {
|
|
|
1985
2080
|
// See the foreground path: the broker's bind address is what makes attach reachable off-box.
|
|
1986
2081
|
attachHost: effectiveAttachHost,
|
|
1987
2082
|
maxSessions: effectiveMaxSessions,
|
|
2083
|
+
noManager: opts.noManager,
|
|
1988
2084
|
resumeAttempt: opts.resumeAttempt,
|
|
1989
2085
|
resumeCommitToken: opts.resumeCommitToken,
|
|
1990
2086
|
wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
|
|
@@ -2073,8 +2169,8 @@ export async function claimSpace(space, server, root) {
|
|
|
2073
2169
|
// reclaimed: unreachable is not proof the mesh is gone (the record describes a broker on another
|
|
2074
2170
|
// machine), the reclaim runs BEFORE this launch starts anything, and `cotal down` — what the
|
|
2075
2171
|
// liveness branch below would advise — cannot stop a mesh this machine does not run.
|
|
2076
|
-
if (existing.origin === "manual")
|
|
2077
|
-
throw new Error(`space "${space}" is registered to a mesh at ${existing.server} (${existing.root}) - it
|
|
2172
|
+
if (existing.origin === "manual" || existing.origin === "catalog")
|
|
2173
|
+
throw new Error(`space "${space}" is registered to a mesh at ${existing.server} (${existing.root}) - it is ${existing.origin === "catalog" ? "owned by a signed-in space catalog" : "registered by hand"}, so \`cotal up\` neither takes it over nor reclaims the name: ${existing.origin === "catalog" ? "use a different `--space`, or remove access at the IdP and run `cotal sync`" : `\`cotal meshes rm ${space}\` to drop that record first, or start this one under a different \`--space\``}`);
|
|
2078
2174
|
if (await isReachable(existing.server)) {
|
|
2079
2175
|
throw new Error(`space "${space}" is already in use by a mesh at ${existing.server} (${existing.root}) - pick a different \`--space\`, or \`cotal down\` it first`);
|
|
2080
2176
|
}
|
|
@@ -2109,6 +2205,15 @@ export function attachHostFor(space, explicit) {
|
|
|
2109
2205
|
export function maxSessionsFor(space, explicit) {
|
|
2110
2206
|
return explicit ?? findMesh(space)?.maxSessions;
|
|
2111
2207
|
}
|
|
2208
|
+
/** The store cap a preserved launch recorded: absent, or a positive integer byte count. Anything
|
|
2209
|
+
* else is a damaged journal, refused rather than rendered as the dynamic default. */
|
|
2210
|
+
function journaledMaxFileStore(value) {
|
|
2211
|
+
if (value === undefined)
|
|
2212
|
+
return undefined;
|
|
2213
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
2214
|
+
throw new Error(`the preserved launch records an invalid maxFileStore (${JSON.stringify(value)}); refusing to guess the broker's store cap`);
|
|
2215
|
+
return value;
|
|
2216
|
+
}
|
|
2112
2217
|
/** {@link recordOurMesh} under a name that says it is a test seam: the origin rule it enforces is
|
|
2113
2218
|
* asserted directly by the registry smoke, since reaching each branch through a full `up` would
|
|
2114
2219
|
* need a live broker per case. */
|
|
@@ -2117,7 +2222,7 @@ function recordOurMesh(m, provenance) {
|
|
|
2117
2222
|
const cur = getCurrent();
|
|
2118
2223
|
const usableCurrent = cur && findMesh(cur) ? cur : undefined; // compute before recording m
|
|
2119
2224
|
const prior = findMesh(m.space);
|
|
2120
|
-
const origin = provenance === "refresh" && prior?.origin === "manual"
|
|
2225
|
+
const origin = provenance === "refresh" && (prior?.origin === "manual" || prior?.origin === "catalog") ? prior.origin : "up";
|
|
2121
2226
|
// A REFRESH starts nothing: it concluded the mesh is up from reachability alone, and rebuilds `m`
|
|
2122
2227
|
// from what THIS launch knows, which is never the operator's past decisions. `origin` was already
|
|
2123
2228
|
// carried across for that reason; the overlay acceptance is the same class and was not, so a
|
|
@@ -2170,6 +2275,36 @@ async function waitReady(server, creds) {
|
|
|
2170
2275
|
* the probe/registry URL stays whatever it already is (loopback by default), which the wildcard
|
|
2171
2276
|
* bind necessarily covers. */
|
|
2172
2277
|
const WILDCARD_HOSTS = new Set(["0.0.0.0", "::", "[::]"]);
|
|
2278
|
+
/** Establish that a bind-host value IS a host, BEFORE anything brackets it, and return the
|
|
2279
|
+
* bracketed literal a URL can hold (#1697).
|
|
2280
|
+
*
|
|
2281
|
+
* `--host` is the bind address; its sibling `--server` takes a URL, the registry stores and prints
|
|
2282
|
+
* URLs, and every message in this area shows one — so an operator plausibly types the URL here
|
|
2283
|
+
* (the manifest's `broker.host` carries the same ambiguity against `broker.servers`). Unrejected,
|
|
2284
|
+
* the IPv6 bracketing wrapped it into `nats://[nats://127.0.0.1:4222]:4222`, which is not a URL at
|
|
2285
|
+
* all. Nothing checked the derived string: the first code to look at it compared it with the
|
|
2286
|
+
* space's registry record, failed for a reason unrelated to what was typed, and showed the
|
|
2287
|
+
* operator the "registered by hand" refusal whose remedy de-registers a live mesh. Refused by name
|
|
2288
|
+
* here instead — pointing at the URL-carrying sibling — and a value that still cannot serve as a
|
|
2289
|
+
* URL host (a `host:port` spelling, say) is refused rather than reaching that comparison as a
|
|
2290
|
+
* silently wrong server string. Never rewritten: a value this flag cannot use is an error, not a
|
|
2291
|
+
* URL to derive from. */
|
|
2292
|
+
function assertBindableHost(host, source, urlSource) {
|
|
2293
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host)) {
|
|
2294
|
+
console.error(c.red(`✗ ${source} ${host} looks like a URL - it takes a bind host only (an IP or hostname, no scheme); pass the broker URL with ${urlSource} instead`));
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
}
|
|
2297
|
+
const literal = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
2298
|
+
try {
|
|
2299
|
+
// ONE parse, both consumers served from it: `literal` builds the derived URL (a URL host needs
|
|
2300
|
+
// the brackets), `hostname` compares against the explicit server's (the URL API strips them).
|
|
2301
|
+
return { literal, hostname: new URL(`nats://${literal}:1`).hostname };
|
|
2302
|
+
}
|
|
2303
|
+
catch {
|
|
2304
|
+
console.error(c.red(`✗ ${source} ${host} is not a valid bind host - an IP or hostname only, with no port; the port comes from ${urlSource} (or its default), never from the bind host`));
|
|
2305
|
+
process.exit(1);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2173
2308
|
/**
|
|
2174
2309
|
* Reconcile the bind address (`--host`, manifest `broker.host`) with the broker URL the readiness
|
|
2175
2310
|
* probe, the mesh registry, and every later client use.
|
|
@@ -2180,16 +2315,19 @@ const WILDCARD_HOSTS = new Set(["0.0.0.0", "::", "[::]"]);
|
|
|
2180
2315
|
* derive one from the host (keeping the URL's port); with both, refuse a contradicting pair rather
|
|
2181
2316
|
* than starting a broker that nothing can reach.
|
|
2182
2317
|
*/
|
|
2183
|
-
function reconcileHostAndServer(host, explicitServer) {
|
|
2318
|
+
function reconcileHostAndServer(host, explicitServer, source = "--host", urlSource = "--server") {
|
|
2184
2319
|
const url = new URL(explicitServer ?? DEFAULT_SERVER);
|
|
2185
2320
|
if (WILDCARD_HOSTS.has(host))
|
|
2186
2321
|
return explicitServer ?? DEFAULT_SERVER;
|
|
2187
|
-
|
|
2188
|
-
const literal = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
2322
|
+
const { literal, hostname } = assertBindableHost(host, source, urlSource);
|
|
2189
2323
|
if (!explicitServer)
|
|
2190
2324
|
return `nats://${literal}:${url.port || "4222"}`;
|
|
2191
|
-
|
|
2192
|
-
|
|
2325
|
+
// NO parse on this path: `assertBindableHost` just parsed the identical literal above, so the
|
|
2326
|
+
// mismatch check compares two parsed hostnames and can only ever print its diagnostic and exit 1
|
|
2327
|
+
// — the "never throw a raw Invalid URL" requirement (#1697) holds by construction, not by a
|
|
2328
|
+
// second guard that no input could reach.
|
|
2329
|
+
if (url.hostname !== hostname) {
|
|
2330
|
+
console.error(c.red(`✗ ${source} ${host} and ${urlSource} ${explicitServer} name different addresses - the broker would bind ${host} but be probed at ${url.hostname}. Pass one, or make them agree.`));
|
|
2193
2331
|
process.exit(1);
|
|
2194
2332
|
}
|
|
2195
2333
|
return explicitServer;
|
|
@@ -2464,7 +2602,7 @@ function validateRecorded(transport, dialHost, opts = {}) {
|
|
|
2464
2602
|
*/
|
|
2465
2603
|
function writeOpenBrokerConf(storeDir, opts) {
|
|
2466
2604
|
const confPath = resolve(storeDir, "..", "server-open.conf");
|
|
2467
|
-
writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, transport: opts.transport }));
|
|
2605
|
+
writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, maxFileStore: opts.maxFileStore, transport: opts.transport }));
|
|
2468
2606
|
return confPath;
|
|
2469
2607
|
}
|
|
2470
2608
|
async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
|
|
@@ -2473,7 +2611,7 @@ async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
|
|
|
2473
2611
|
// operator's `--tls-cert` sat validated and unused. A default here cannot be right, because the
|
|
2474
2612
|
// safe value and the common value are different values — every caller knows its transport, and
|
|
2475
2613
|
// the one that does not is the one that must not compile.
|
|
2476
|
-
transport, rotateSys = false) {
|
|
2614
|
+
transport, rotateSys = false, maxFileStore) {
|
|
2477
2615
|
const dir = authDir(cotalRoot()); // the broker config (server.conf) still lands under the FS auth dir
|
|
2478
2616
|
const store = workspaceSecretStore(cotalRoot());
|
|
2479
2617
|
let auth = await getSpaceAuth(store, space);
|
|
@@ -2569,7 +2707,20 @@ transport, rotateSys = false) {
|
|
|
2569
2707
|
// map, so a config rendered from one space evicts the rest silently. `auth` leads the list because
|
|
2570
2708
|
// this function's copy can be fresher than the disk (a space created above, or one whose trust
|
|
2571
2709
|
// record `--rotate-sys` just replaced). Refuses on an unreadable record rather than dropping it.
|
|
2572
|
-
|
|
2710
|
+
const spaces = preloadSpaceAccounts(dir, auth);
|
|
2711
|
+
const enabled = userAuthSpacesOnDisk(dir);
|
|
2712
|
+
for (const enabledSpace of enabled) {
|
|
2713
|
+
if (!spaces.some((s) => s.space === enabledSpace))
|
|
2714
|
+
throw new Error(`space "${enabledSpace}" has user auth enabled but no data account record - refusing to render an incomplete broker resolver`);
|
|
2715
|
+
}
|
|
2716
|
+
const extraAccounts = [];
|
|
2717
|
+
for (const enabledSpace of enabled) {
|
|
2718
|
+
const accounts = enabledSpace === space && prepared
|
|
2719
|
+
? prepared.extraAccounts
|
|
2720
|
+
: await resolveAuthProvider().preloadAccounts({ store, space: enabledSpace });
|
|
2721
|
+
extraAccounts.push(...accounts);
|
|
2722
|
+
}
|
|
2723
|
+
writeFileSync(confPath, serverConfig(auth, spaces, { transport, port, storeDir, maxFileStore, host, wsPort, wsHost: host, extraAccounts }));
|
|
2573
2724
|
// Ephemeral setup cred: used only to probe reachability, pre-create the space streams/buckets
|
|
2574
2725
|
// (setupSpaceStreams) and seed the channel registry (seedChannelRegistry) — all within the
|
|
2575
2726
|
// enumerated `provisioner` scope. No broad `manager` residual for the up path.
|