@cotal-ai/cli 0.16.0 → 0.18.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/cotal-skills/skills/team-topology/SKILL.md +1 -1
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +13 -0
- package/dist/command.js.map +1 -1
- package/dist/commands/agents.d.ts +15 -15
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +187 -77
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +4 -3
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/describe.d.ts +72 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +148 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +27 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/down-manifest.d.ts.map +1 -1
- package/dist/commands/down-manifest.js +38 -18
- package/dist/commands/down-manifest.js.map +1 -1
- package/dist/commands/down.d.ts +9 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +38 -10
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/ext.d.ts.map +1 -1
- package/dist/commands/ext.js +16 -10
- package/dist/commands/ext.js.map +1 -1
- package/dist/commands/join.d.ts.map +1 -1
- package/dist/commands/join.js +2 -4
- package/dist/commands/join.js.map +1 -1
- package/dist/commands/meshes-add.d.ts +17 -0
- package/dist/commands/meshes-add.d.ts.map +1 -1
- package/dist/commands/meshes-add.js +72 -4
- package/dist/commands/meshes-add.js.map +1 -1
- package/dist/commands/meshes-wizard.d.ts +3 -0
- package/dist/commands/meshes-wizard.d.ts.map +1 -1
- package/dist/commands/meshes-wizard.js +78 -6
- package/dist/commands/meshes-wizard.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 +14 -4
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +2 -2
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/spawn-manifest.d.ts.map +1 -1
- package/dist/commands/spawn-manifest.js +22 -12
- package/dist/commands/spawn-manifest.js.map +1 -1
- package/dist/commands/spawn.d.ts +5 -0
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +5 -3
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.js +25 -5
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts +17 -2
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +685 -50
- package/dist/commands/up.js.map +1 -1
- package/dist/console/app.d.ts +1 -1
- package/dist/console/app.d.ts.map +1 -1
- package/dist/console/app.js +16 -5
- package/dist/console/app.js.map +1 -1
- package/dist/console/commands.d.ts.map +1 -1
- package/dist/console/commands.js +5 -5
- package/dist/console/commands.js.map +1 -1
- package/dist/console/root.d.ts.map +1 -1
- package/dist/console/root.js +6 -1
- package/dist/console/root.js.map +1 -1
- package/dist/ext-loader.d.ts.map +1 -1
- package/dist/ext-loader.js +1 -0
- package/dist/ext-loader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/attach-client.d.ts +37 -4
- package/dist/lib/attach-client.d.ts.map +1 -1
- package/dist/lib/attach-client.js +88 -18
- package/dist/lib/attach-client.js.map +1 -1
- package/dist/lib/auth-proc.d.ts +3 -1
- package/dist/lib/auth-proc.d.ts.map +1 -1
- package/dist/lib/auth-proc.js +15 -4
- package/dist/lib/auth-proc.js.map +1 -1
- package/dist/lib/connect.d.ts +1 -1
- package/dist/lib/connect.d.ts.map +1 -1
- package/dist/lib/connect.js +1 -1
- package/dist/lib/connect.js.map +1 -1
- package/dist/lib/control.d.ts +48 -12
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/control.js +258 -46
- package/dist/lib/control.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts +19 -4
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +129 -43
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/join-target.d.ts +112 -0
- package/dist/lib/join-target.d.ts.map +1 -0
- package/dist/lib/join-target.js +130 -0
- package/dist/lib/join-target.js.map +1 -0
- package/dist/lib/manager-proc.d.ts +48 -5
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +131 -30
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/manifest/ledger.d.ts +52 -8
- package/dist/lib/manifest/ledger.d.ts.map +1 -1
- package/dist/lib/manifest/ledger.js +77 -19
- package/dist/lib/manifest/ledger.js.map +1 -1
- package/dist/lib/manifest/live.d.ts +21 -16
- package/dist/lib/manifest/live.d.ts.map +1 -1
- package/dist/lib/manifest/live.js +33 -20
- package/dist/lib/manifest/live.js.map +1 -1
- package/dist/lib/restore.d.ts.map +1 -1
- package/dist/lib/restore.js +21 -3
- package/dist/lib/restore.js.map +1 -1
- package/dist/view/mesh-view.js +2 -2
- package/dist/view/mesh-view.js.map +1 -1
- package/package.json +4 -3
- package/dist/lib/pid.d.ts +0 -23
- package/dist/lib/pid.d.ts.map +0 -1
- package/dist/lib/pid.js +0 -39
- package/dist/lib/pid.js.map +0 -1
|
@@ -1,50 +1,77 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, openSync, closeSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { DEFAULT_SERVER, mintCreds, newIdentity, waitForDeliveryLease, } from "@cotal-ai/core";
|
|
4
|
-
import { DELIVERY_CREDS_KEY, authDir, findCotalRoot, getSoleSpaceAuth, listSpaceAccounts, workspaceSecretStore } from "@cotal-ai/workspace";
|
|
4
|
+
import { DELIVERY_CREDS_KEY, authDir, findCotalRoot, getSoleSpaceAuth, listSpaceAccounts, parsePid, probeLiveness, workspaceSecretStore } from "@cotal-ai/workspace";
|
|
5
5
|
import { selfArgv } from "./self-exec.js";
|
|
6
6
|
import { resolveSpace } from "./status.js";
|
|
7
7
|
import { cotalPath } from "./paths.js";
|
|
8
|
-
import { ensureManager, managerHasDeliveryMarker,
|
|
8
|
+
import { MANAGER_PID_PATH, ensureManager, managerHasDeliveryMarker, managerLiveness, stopManager } from "./manager-proc.js";
|
|
9
9
|
const PID_PATH = () => cotalPath("delivery.pid");
|
|
10
10
|
// The daemon's cred goes through the secret-store seam; the shared key (== the filename, so the
|
|
11
11
|
// file stays `.cotal/delivery.creds`) comes from workspace — never a hand-copied literal.
|
|
12
12
|
const credsStore = () => workspaceSecretStore(findCotalRoot());
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/** True if the delivery daemon we started for this folder is still running (pid file + liveness). */
|
|
23
|
-
export function deliveryUp() {
|
|
13
|
+
/** The recorded daemon's liveness, THREE-VALUED plus absent. See {@link managerLiveness} for why the
|
|
14
|
+
* boolean collapse is the defect: `unknown` is reachable on a real kernel (a seccomp
|
|
15
|
+
* `SECCOMP_RET_ERRNO` filter or an LSM policy answers `kill(pid, 0)` with an arbitrary errno and
|
|
16
|
+
* libuv preserves it), and both ways of folding it into a boolean fail silently. */
|
|
17
|
+
export function deliveryLiveness(probe = probeLiveness) {
|
|
24
18
|
const p = PID_PATH();
|
|
25
19
|
if (!existsSync(p))
|
|
26
|
-
return
|
|
27
|
-
const
|
|
28
|
-
|
|
20
|
+
return "absent";
|
|
21
|
+
const raw = readFileSync(p, "utf8").trim();
|
|
22
|
+
if (raw === "")
|
|
23
|
+
return "absent";
|
|
24
|
+
const pid = parsePid(raw);
|
|
25
|
+
if (pid === undefined)
|
|
26
|
+
return "unattributable"; // see managerLiveness: never fold this into absent
|
|
27
|
+
return probe(pid);
|
|
28
|
+
}
|
|
29
|
+
/** True only if the daemon is PROVABLY running. Callers that ACT on the answer take
|
|
30
|
+
* {@link deliveryLiveness} instead; this cannot express "cannot tell". */
|
|
31
|
+
export function deliveryUp() {
|
|
32
|
+
return deliveryLiveness() === "alive";
|
|
29
33
|
}
|
|
30
34
|
/** True when this folder runs an authed mesh — the only mode with a delivery daemon (Plane-3 needs the
|
|
31
35
|
* trusted reader; open dev mode is live-only). */
|
|
32
36
|
function hasAuth() {
|
|
33
37
|
return listSpaceAccounts(authDir(findCotalRoot())).length > 0;
|
|
34
38
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
/** The cutover preflight's verdict, THREE-VALUED, because the boolean it replaced was read before the
|
|
40
|
+
* refusal boundary and therefore defeated it.
|
|
41
|
+
*
|
|
42
|
+
* `managerUp()` folds `unknown` to false, so an unattributable manager with no marker (which IS the
|
|
43
|
+
* old Plane-3-hosting shape) read as "no old manager", the preflight skipped, and `ensureDelivery`
|
|
44
|
+
* went on to mint a credential, write a pidfile and start a second daemon. Only afterwards did
|
|
45
|
+
* `ensureManager` throw. Everything the refusal was supposed to prevent had already happened, and
|
|
46
|
+
* the daemon's own lease cannot help: it is a delivery-daemon-only mechanism and can neither prove
|
|
47
|
+
* nor exclude an old manager still hosting Plane 3.
|
|
48
|
+
*
|
|
49
|
+
* A guard that runs after the work is not a guard, so this one reads the tri-state directly and the
|
|
50
|
+
* caller refuses BEFORE anything is minted, written or started. */
|
|
51
|
+
function oldHostingManagerVerdict(probe = probeLiveness) {
|
|
52
|
+
const state = managerLiveness(probe);
|
|
53
|
+
if (state === "unknown" || state === "unattributable")
|
|
54
|
+
return "indeterminate";
|
|
55
|
+
if (state !== "alive")
|
|
56
|
+
return "proceed"; // dead or absent: nothing is hosting Plane 3
|
|
57
|
+
return managerHasDeliveryMarker() ? "proceed" : "stop-it"; // alive: the marker decides
|
|
40
58
|
}
|
|
41
59
|
/** Cutover preflight — the FIRST action, BEFORE the daemon can bind: stop any old Plane-3-hosting
|
|
42
60
|
* manager (live `manager.pid` without the delivery-aware marker) so it never double-binds the
|
|
43
61
|
* daemon's durables. A delivery-aware (this-build) manager is left running. No-op on a fresh install. */
|
|
44
|
-
export function stopOldHostingManagerIfPresent() {
|
|
45
|
-
|
|
62
|
+
export async function stopOldHostingManagerIfPresent(probe = probeLiveness, signal) {
|
|
63
|
+
const verdict = oldHostingManagerVerdict(probe);
|
|
64
|
+
// FIRST action, before any mint/write/start, so the refusal actually fences the daemon.
|
|
65
|
+
if (verdict === "indeterminate")
|
|
66
|
+
throw new Error(`the recorded manager pid (${readFileSync(MANAGER_PID_PATH(), "utf8").trim()}) cannot be attributed, so the delivery cutover preflight cannot run: the kernel answered neither "running" nor "no such process" (a seccomp filter or LSM policy does this inside some sandboxes).\n` +
|
|
67
|
+
`Refusing before the daemon starts. If that manager is an old Plane-3-hosting one it is still bound to fanout/reader, and starting the daemon anyway would double-bind them; the daemon's own lease cannot detect that.\n` +
|
|
68
|
+
`NEXT: verify the process yourself (\`ps -p <pid>\`). If it is gone, remove \`.cotal/manager.pid\` and re-run. If it is running, stop it with \`cotal down\` first.`);
|
|
69
|
+
if (verdict === "stop-it") {
|
|
46
70
|
console.error("• stopping an old Plane-3-hosting manager before starting the delivery daemon (cutover preflight)");
|
|
47
|
-
stopManager(
|
|
71
|
+
// stopManager THROWS rather than reporting a stop it did not achieve (EPERM, or a process that
|
|
72
|
+
// outlived SIGTERM), so reaching the next line is the proof the old manager is gone. Letting that
|
|
73
|
+
// throw propagate is the point: the daemon must not start beside a manager still bound to Plane 3.
|
|
74
|
+
await stopManager(probe, signal);
|
|
48
75
|
}
|
|
49
76
|
}
|
|
50
77
|
/** Start the delivery daemon detached (pid in `.cotal/delivery.pid`, output to `.cotal/delivery.log`),
|
|
@@ -60,6 +87,15 @@ export function startDeliveryDetached(o = {}) {
|
|
|
60
87
|
o.space ?? resolveSpace(process.cwd()),
|
|
61
88
|
"--server",
|
|
62
89
|
o.server ?? DEFAULT_SERVER,
|
|
90
|
+
// Propagate the broker's transport decision. The daemon cannot derive it: it learns everything
|
|
91
|
+
// from argv by design - it is a pre-minted scoped-cred client, injectable behind a SecretStore,
|
|
92
|
+
// and making it read the machine-local mesh registry to pick a transport would couple a
|
|
93
|
+
// hosted-composable daemon to a workstation artifact. So the launcher, which DOES know, tells it.
|
|
94
|
+
//
|
|
95
|
+
// Without this the daemon connects plaintext-capable to a TLS broker and nothing looks wrong,
|
|
96
|
+
// because it still upgrades on the server's INFO. It holds a STANDING credential and reconnects
|
|
97
|
+
// unattended, so that exposure would repeat on every reconnect with nobody watching.
|
|
98
|
+
...(o.tls ? ["--tls"] : []),
|
|
63
99
|
];
|
|
64
100
|
// Internal child re-exec (the `up` that reached here already seeded); the delivery daemon does not
|
|
65
101
|
// launch agents, so it skips the connector seed on boot (a direct `cotal deliver` still seeds).
|
|
@@ -74,10 +110,10 @@ export function startDeliveryDetached(o = {}) {
|
|
|
74
110
|
* never double-binds. Mints a SCOPED `delivery` cred from the local signer ONCE, writes it to
|
|
75
111
|
* `.cotal/delivery.creds` (0600), and launches the daemon WITHOUT signer access. Best-effort — callers
|
|
76
112
|
* treat it as non-fatal (a missing daemon degrades durable delivery, never live). */
|
|
77
|
-
export async function ensureDelivery(o = {}) {
|
|
113
|
+
export async function ensureDelivery(o = {}, probe = probeLiveness) {
|
|
78
114
|
if (!hasAuth())
|
|
79
115
|
return { running: false }; // open dev mode — no daemon, agents are live-only
|
|
80
|
-
if (
|
|
116
|
+
if (oldHostingManagerVerdict(probe) === "stop-it") {
|
|
81
117
|
console.error("✗ delivery: an old Plane-3-hosting manager is still live (no delivery-aware marker). Refusing to start the daemon - run `cotal down` first, then retry.");
|
|
82
118
|
return { running: false };
|
|
83
119
|
}
|
|
@@ -90,7 +126,15 @@ export async function ensureDelivery(o = {}) {
|
|
|
90
126
|
const creds = await mintCreds(auth, id, "delivery");
|
|
91
127
|
const space = o.space ?? resolveSpace(process.cwd());
|
|
92
128
|
const server = o.server ?? DEFAULT_SERVER;
|
|
93
|
-
|
|
129
|
+
const deliveryState = deliveryLiveness(probe);
|
|
130
|
+
// Same refusal as the manager: an unattributable pid must not be silently reused (a daemon
|
|
131
|
+
// reported running that is not there) nor silently replaced (two daemons on one fanout).
|
|
132
|
+
if (deliveryState === "unknown" || deliveryState === "unattributable")
|
|
133
|
+
throw new Error(`the recorded delivery daemon pid (${readFileSync(PID_PATH(), "utf8").trim()}) cannot be attributed: the kernel answered neither "running" nor "no such process".\n` +
|
|
134
|
+
`A seccomp filter or LSM policy that intercepts \`kill(pid, 0)\` does this, so it is expected inside some sandboxes and containers.\n` +
|
|
135
|
+
`Cotal will not guess: reusing it would report a daemon that is not there, and starting a second would put two daemons on one fanout.\n` +
|
|
136
|
+
`NEXT: verify the process yourself (\`ps -p <pid>\`). If it is gone, remove \`.cotal/delivery.pid\` and re-run. If it is running, use it or stop it.`);
|
|
137
|
+
if (deliveryState !== "alive") {
|
|
94
138
|
// The store's put hardens `.cotal/` first (the cred is born under a private ACL, no race) and
|
|
95
139
|
// lands it atomically — same path and bytes as before the seam.
|
|
96
140
|
await credsStore().put(DELIVERY_CREDS_KEY, creds);
|
|
@@ -109,25 +153,67 @@ export async function ensureDelivery(o = {}) {
|
|
|
109
153
|
* kill runs even if the creds delete fails (finally) — a delete error must never leave the daemon
|
|
110
154
|
* alive to outlive the teardown and reattach to a restarted broker; the error still propagates
|
|
111
155
|
* after the kill so the caller can surface it. */
|
|
112
|
-
export async function stopDelivery() {
|
|
113
|
-
|
|
156
|
+
export async function stopDelivery(probe = probeLiveness, signal) {
|
|
157
|
+
const send = signal ?? ((pid, sig) => process.kill(pid, sig));
|
|
158
|
+
const p = PID_PATH();
|
|
159
|
+
// ORDER MATTERS, AND IT USED TO BE BACKWARDS. The credential was deleted FIRST, in a try whose
|
|
160
|
+
// finally then signalled and removed the pidfile unconditionally. Under a refused signal that left
|
|
161
|
+
// a LIVE daemon with no pidfile and no standing credential: still connected, still serving, and its
|
|
162
|
+
// reconnect and renewal source deleted out from under it, while the function returned success.
|
|
163
|
+
// Nothing is removed now until the process is proven gone.
|
|
164
|
+
// Once death is PROVEN, both records are stale and the pidfile goes first: a creds-delete failure
|
|
165
|
+
// (a blocked path, a store error) must still propagate loudly, but it must not leave behind a
|
|
166
|
+
// pidfile for a process that is definitely gone. delivery-teardown.smoke.ts pins exactly that
|
|
167
|
+
// combination, and caught this when the first version of this fix ordered them the other way.
|
|
168
|
+
const removeRecords = async () => {
|
|
169
|
+
rmSync(p, { force: true });
|
|
114
170
|
await credsStore().delete(DELIVERY_CREDS_KEY);
|
|
171
|
+
};
|
|
172
|
+
if (!existsSync(p)) {
|
|
173
|
+
await credsStore().delete(DELIVERY_CREDS_KEY); // no pid recorded: no live daemon to strand
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const raw = readFileSync(p, "utf8").trim();
|
|
177
|
+
const pid = parsePid(raw);
|
|
178
|
+
if (pid === undefined) {
|
|
179
|
+
if (raw === "") {
|
|
180
|
+
await removeRecords(); // pre-protocol husk, nothing behind it
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// See the manager helper: unattributable content may front a live daemon, and clearing it here
|
|
184
|
+
// would also delete the standing credential of a process still using it.
|
|
185
|
+
throw new Error(`the delivery pidfile at ${p} is unattributable (${JSON.stringify(raw)}): it may still front a running daemon nobody can identify.\n` +
|
|
186
|
+
`Refusing to remove it or its standing credential, and refusing to report a clean stop.\n` +
|
|
187
|
+
`NEXT: find and stop that process, then remove the file by hand.`);
|
|
188
|
+
}
|
|
189
|
+
const before = probe(pid);
|
|
190
|
+
if (before === "dead") {
|
|
191
|
+
await removeRecords();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (before === "unknown")
|
|
195
|
+
throw new Error(`refusing to stop the delivery daemon (pid ${pid}): its liveness cannot be determined (a seccomp filter or LSM policy answers kill(pid,0) with an arbitrary errno).\n` +
|
|
196
|
+
`The pidfile and standing credential are LEFT IN PLACE: removing them would strand a daemon that may still be connected, with its renewal source gone.\n` +
|
|
197
|
+
`NEXT: verify with \`ps -p ${pid}\`, then stop it yourself or remove \`.cotal/delivery.pid\` if it is gone.`);
|
|
198
|
+
try {
|
|
199
|
+
send(pid, "SIGTERM");
|
|
115
200
|
}
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
process.kill(pid, "SIGTERM");
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
/* already gone */
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
rmSync(p);
|
|
201
|
+
catch (e) {
|
|
202
|
+
const code = e.code;
|
|
203
|
+
if (code === "ESRCH") {
|
|
204
|
+
await removeRecords(); // raced to exit between probe and signal
|
|
205
|
+
return;
|
|
129
206
|
}
|
|
207
|
+
throw new Error(`refusing to stop the delivery daemon (pid ${pid}): the signal was rejected (${code ?? "unknown error"}).\n` +
|
|
208
|
+
`EPERM means it belongs to another user, so it is running and not ours to stop. The pidfile and standing credential are LEFT IN PLACE.\n` +
|
|
209
|
+
`NEXT: stop it as its owner, or remove \`.cotal/delivery.pid\` if you are certain it is gone.`);
|
|
130
210
|
}
|
|
211
|
+
const deadline = Date.now() + 15_000;
|
|
212
|
+
while (probe(pid) !== "dead" && Date.now() < deadline)
|
|
213
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
214
|
+
if (probe(pid) !== "dead")
|
|
215
|
+
throw new Error(`the delivery daemon (pid ${pid}) accepted SIGTERM but its death could not be confirmed; its pidfile and credential were preserved rather than recording a stop that did not happen.`);
|
|
216
|
+
await removeRecords();
|
|
131
217
|
}
|
|
132
218
|
/** Bring up the control plane in the correct cutover order: OLD-manager preflight → delivery daemon
|
|
133
219
|
* (auth only, fails closed on a live old manager) → manager (lifecycle, writes the delivery-aware
|
|
@@ -135,7 +221,7 @@ export async function stopDelivery() {
|
|
|
135
221
|
* first only to close the old-manager double-bind window and so freshly-spawned agents find the
|
|
136
222
|
* `ctl.delivery` responder for their boot self-join (a miss honest-degrades to live-only). */
|
|
137
223
|
export async function ensureControlPlane(o = {}) {
|
|
138
|
-
stopOldHostingManagerIfPresent();
|
|
224
|
+
await stopOldHostingManagerIfPresent();
|
|
139
225
|
await ensureDelivery(o);
|
|
140
226
|
return ensureManager(o);
|
|
141
227
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery-proc.js","sourceRoot":"","sources":["../../src/lib/delivery-proc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EACL,cAAc,EACd,SAAS,EACT,WAAW,EACX,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"delivery-proc.js","sourceRoot":"","sources":["../../src/lib/delivery-proc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which broker addresses `cotal meshes add` may register.
|
|
3
|
+
*
|
|
4
|
+
* Registering a mesh is how a machine begins sending its agent credentials to a broker it does
|
|
5
|
+
* not run. NATS sends the initial INFO in plaintext and unauthenticated, so an on-path attacker
|
|
6
|
+
* can forge one that does not set `tls_required`, and a client that was never told to demand
|
|
7
|
+
* encryption puts its credentials in the CONNECT line for the attacker to read. The fence has to
|
|
8
|
+
* be client-side.
|
|
9
|
+
*
|
|
10
|
+
* The URL scheme is NOT that fence. Measured against `@nats-io/transport-node` 3.4.0 (the client
|
|
11
|
+
* this repo uses) pointed at a broker with no TLS configured at all:
|
|
12
|
+
*
|
|
13
|
+
* nats://host:port {} -> CONNECTED
|
|
14
|
+
* tls://host:port {} -> CONNECTED <- the scheme is cosmetic
|
|
15
|
+
* nats://host:port {tls:{}} -> REFUSED: server does not support 'tls'
|
|
16
|
+
*
|
|
17
|
+
* Only the explicit `tls` connect option makes the client demand TLS, and that option is the
|
|
18
|
+
* broker-TLS work's surface, not this one's. So this gates on the ADDRESS instead, in two steps.
|
|
19
|
+
* Step one, here: refuse public addresses, ordinary private ranges and every hostname outright,
|
|
20
|
+
* and refuse an overlay literal TOO unless the operator accepted it explicitly
|
|
21
|
+
* ({@link DialPolicy.allowUnencryptedOverlay}); accepted, it returns a {@link JoinTarget.residual}
|
|
22
|
+
* the caller prints and records as consent. Step two, once a record can carry TLS intent: the
|
|
23
|
+
* acceptance stops being needed because the transport is proven instead of promised.
|
|
24
|
+
*
|
|
25
|
+
* ONE deliberate limitation, stated rather than papered over: this classifies an ADDRESS, which
|
|
26
|
+
* is a guard against the obvious mistake (dialing a LAN or public address in the clear), not
|
|
27
|
+
* proof that the bytes are encrypted. An overlay address only rides an encrypted tunnel while the
|
|
28
|
+
* overlay is actually up on this machine; the same literal with the overlay down is just a CGNAT
|
|
29
|
+
* address.
|
|
30
|
+
*
|
|
31
|
+
* WHERE THIS GOES NEXT, so the next editor does not relax it for the wrong reason. Measured on
|
|
32
|
+
* this stack: the `tls` connect option verifies the certificate CHAIN AND THE HOSTNAME (Node
|
|
33
|
+
* defaults, `rejectUnauthorized: true`, servername from the URL), so a redirected name cannot
|
|
34
|
+
* complete the handshake and hostnames become safe — but only once something REQUIRES TLS. The
|
|
35
|
+
* relaxation is therefore a function of recorded strictness, not of time passing, and the agreed
|
|
36
|
+
* eventual shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third verdict: one
|
|
37
|
+
* place keeps answering "may this machine send credentials to that address", with strictness as
|
|
38
|
+
* an input it cannot otherwise see. Two counterintuitive consequences worth keeping in view:
|
|
39
|
+
* the overlay ranges are the AWKWARD case under TLS (CGNAT space, no public CA will issue for it,
|
|
40
|
+
* verifying a literal needs an IP SAN, so it implies a private CA on every joiner), while the
|
|
41
|
+
* `<host>.ts.net` names refused below are the EASY case (publicly resolvable, publicly-trusted
|
|
42
|
+
* certs). Do not pre-relax: with nothing requiring TLS today, literals-only is the correct rule.
|
|
43
|
+
*/
|
|
44
|
+
/** The address class a permitted target belongs to. This is a REACHABILITY allowlist: it says the
|
|
45
|
+
* address is one we are willing to consider, never that the connection is protected. What
|
|
46
|
+
* protects it is {@link DialPolicy.tlsRequired}, except on loopback where nothing leaves the
|
|
47
|
+
* machine to protect. */
|
|
48
|
+
export type JoinReach =
|
|
49
|
+
/** A loopback literal: the bytes never leave the machine. */
|
|
50
|
+
"loopback"
|
|
51
|
+
/** A private-overlay literal. Permitted TODAY without required TLS, carrying a
|
|
52
|
+
* {@link JoinTarget.residual} that says so; it becomes conditional on TLS in step two. The
|
|
53
|
+
* address alone is not a guarantee - see {@link DialPolicy.tlsRequired}. */
|
|
54
|
+
| "overlay";
|
|
55
|
+
export interface JoinTarget {
|
|
56
|
+
/** The normalized dial URL, port defaulted. */
|
|
57
|
+
server: string;
|
|
58
|
+
reach: JoinReach;
|
|
59
|
+
/**
|
|
60
|
+
* Set when the target is permitted but its protection is NOT guaranteed, carrying the sentence
|
|
61
|
+
* the operator needs to read. Exactly one case produces it today: an overlay literal dialed
|
|
62
|
+
* without required TLS, where the address class is right but the tunnel being up is an
|
|
63
|
+
* assumption rather than a fact.
|
|
64
|
+
*
|
|
65
|
+
* It is a returned value rather than a throw because by the time it is produced the operator has
|
|
66
|
+
* ALREADY accepted the risk, so the remaining job is to carry the sentence: the caller prints it
|
|
67
|
+
* as a second notice and records it on the entry as evidence of consent. The refusal for someone
|
|
68
|
+
* who has NOT accepted happens earlier, and is a throw.
|
|
69
|
+
*/
|
|
70
|
+
residual?: string;
|
|
71
|
+
}
|
|
72
|
+
/** What the eventual connection will insist on, which the address alone cannot tell us. */
|
|
73
|
+
export interface DialPolicy {
|
|
74
|
+
/**
|
|
75
|
+
* Will the dial to this target REQUIRE TLS?
|
|
76
|
+
*
|
|
77
|
+
* This is the difference between an address and a guarantee, and getting it wrong was the
|
|
78
|
+
* defect this parameter exists to close. An overlay address is not proof the overlay transport
|
|
79
|
+
* is up: with the tunnel daemon stopped, `100.64.0.0/10` is ordinary CGNAT space, and hostile
|
|
80
|
+
* DHCP or routing can answer a dial to it. So the address class establishes only that we are
|
|
81
|
+
* willing to consider the target; requiring TLS is what makes it safe.
|
|
82
|
+
*
|
|
83
|
+
* There is no field on the mesh record to source this from yet — it arrives with the work that
|
|
84
|
+
* teaches the broker to serve TLS — so callers pass `false` today, and with it false an overlay
|
|
85
|
+
* literal is refused unless {@link DialPolicy.allowUnencryptedOverlay} says the operator
|
|
86
|
+
* accepted the tunnel dependency. When this can be `true` the acceptance stops being needed:
|
|
87
|
+
* the transport is then proven rather than promised, and the flag can go.
|
|
88
|
+
*/
|
|
89
|
+
tlsRequired: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Did the operator EXPLICITLY accept the overlay's tunnel dependency?
|
|
92
|
+
*
|
|
93
|
+
* The default is `false` and the default refuses, because the previous shape — permit and print
|
|
94
|
+
* a warning — is not a fence. A warning is written to stderr, which a non-interactive caller
|
|
95
|
+
* need not read, and it was not persisted, so nothing repeated it at the dials that followed. A
|
|
96
|
+
* scripted registration therefore got the risk with none of the notice.
|
|
97
|
+
*
|
|
98
|
+
* A flag is the one form of notice a script cannot miss: without it the command fails, and the
|
|
99
|
+
* failure is in the exit code rather than in prose nobody reads. The acceptance is then recorded
|
|
100
|
+
* on the mesh entry, so a later dial can tell that a human agreed to this rather than inferring
|
|
101
|
+
* consent from an address.
|
|
102
|
+
*/
|
|
103
|
+
allowUnencryptedOverlay: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Classify a registration target, or throw the reason it is refused.
|
|
107
|
+
*
|
|
108
|
+
* Refusal is the point: an unclassifiable target is one whose credentials would cross a network
|
|
109
|
+
* this build cannot encrypt, so it fails loud here rather than dialing and hoping.
|
|
110
|
+
*/
|
|
111
|
+
export declare function classifyJoinTarget(raw: string, policy: DialPolicy): JoinTarget;
|
|
112
|
+
//# sourceMappingURL=join-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-target.d.ts","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;;0BAG0B;AAC1B,MAAM,MAAM,SAAS;AACnB,6DAA6D;AAC3D,UAAU;AACZ;;6EAE6E;GAC3E,SAAS,CAAC;AAEd,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;;;;;OAcG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;OAYG;IACH,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAuCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CA8C9E"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which broker addresses `cotal meshes add` may register.
|
|
3
|
+
*
|
|
4
|
+
* Registering a mesh is how a machine begins sending its agent credentials to a broker it does
|
|
5
|
+
* not run. NATS sends the initial INFO in plaintext and unauthenticated, so an on-path attacker
|
|
6
|
+
* can forge one that does not set `tls_required`, and a client that was never told to demand
|
|
7
|
+
* encryption puts its credentials in the CONNECT line for the attacker to read. The fence has to
|
|
8
|
+
* be client-side.
|
|
9
|
+
*
|
|
10
|
+
* The URL scheme is NOT that fence. Measured against `@nats-io/transport-node` 3.4.0 (the client
|
|
11
|
+
* this repo uses) pointed at a broker with no TLS configured at all:
|
|
12
|
+
*
|
|
13
|
+
* nats://host:port {} -> CONNECTED
|
|
14
|
+
* tls://host:port {} -> CONNECTED <- the scheme is cosmetic
|
|
15
|
+
* nats://host:port {tls:{}} -> REFUSED: server does not support 'tls'
|
|
16
|
+
*
|
|
17
|
+
* Only the explicit `tls` connect option makes the client demand TLS, and that option is the
|
|
18
|
+
* broker-TLS work's surface, not this one's. So this gates on the ADDRESS instead, in two steps.
|
|
19
|
+
* Step one, here: refuse public addresses, ordinary private ranges and every hostname outright,
|
|
20
|
+
* and refuse an overlay literal TOO unless the operator accepted it explicitly
|
|
21
|
+
* ({@link DialPolicy.allowUnencryptedOverlay}); accepted, it returns a {@link JoinTarget.residual}
|
|
22
|
+
* the caller prints and records as consent. Step two, once a record can carry TLS intent: the
|
|
23
|
+
* acceptance stops being needed because the transport is proven instead of promised.
|
|
24
|
+
*
|
|
25
|
+
* ONE deliberate limitation, stated rather than papered over: this classifies an ADDRESS, which
|
|
26
|
+
* is a guard against the obvious mistake (dialing a LAN or public address in the clear), not
|
|
27
|
+
* proof that the bytes are encrypted. An overlay address only rides an encrypted tunnel while the
|
|
28
|
+
* overlay is actually up on this machine; the same literal with the overlay down is just a CGNAT
|
|
29
|
+
* address.
|
|
30
|
+
*
|
|
31
|
+
* WHERE THIS GOES NEXT, so the next editor does not relax it for the wrong reason. Measured on
|
|
32
|
+
* this stack: the `tls` connect option verifies the certificate CHAIN AND THE HOSTNAME (Node
|
|
33
|
+
* defaults, `rejectUnauthorized: true`, servername from the URL), so a redirected name cannot
|
|
34
|
+
* complete the handshake and hostnames become safe — but only once something REQUIRES TLS. The
|
|
35
|
+
* relaxation is therefore a function of recorded strictness, not of time passing, and the agreed
|
|
36
|
+
* eventual shape is `classifyJoinTarget(url, { tlsRequired })` rather than a third verdict: one
|
|
37
|
+
* place keeps answering "may this machine send credentials to that address", with strictness as
|
|
38
|
+
* an input it cannot otherwise see. Two counterintuitive consequences worth keeping in view:
|
|
39
|
+
* the overlay ranges are the AWKWARD case under TLS (CGNAT space, no public CA will issue for it,
|
|
40
|
+
* verifying a literal needs an IP SAN, so it implies a private CA on every joiner), while the
|
|
41
|
+
* `<host>.ts.net` names refused below are the EASY case (publicly resolvable, publicly-trusted
|
|
42
|
+
* certs). Do not pre-relax: with nothing requiring TLS today, literals-only is the correct rule.
|
|
43
|
+
*/
|
|
44
|
+
/** The NATS default client port, used when the join URL omits one. */
|
|
45
|
+
const DEFAULT_PORT = 4222;
|
|
46
|
+
/** Loopback: `127.0.0.0/8` and `::1`. LITERALS ONLY, never `localhost`, because the whole point is
|
|
47
|
+
* a verdict that does not depend on a resolver an attacker could influence (a hosts-file entry or
|
|
48
|
+
* a poisoned lookup would otherwise turn "loopback" into any address at all). */
|
|
49
|
+
function isLoopbackLiteral(host) {
|
|
50
|
+
if (host === "::1" || host === "0:0:0:0:0:0:0:1")
|
|
51
|
+
return true;
|
|
52
|
+
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
53
|
+
if (!v4)
|
|
54
|
+
return false;
|
|
55
|
+
const parts = v4.slice(1).map(Number);
|
|
56
|
+
if (parts.some((n) => n > 255))
|
|
57
|
+
return false;
|
|
58
|
+
return parts[0] === 127;
|
|
59
|
+
}
|
|
60
|
+
/** The private overlay's address space: `100.64.0.0/10` (the CGNAT range Tailscale assigns) and
|
|
61
|
+
* `fd7a:115c:a1e0::/48` (its ULA prefix). LITERALS ONLY, for the same reason as loopback: a
|
|
62
|
+
* MagicDNS name like `<host>.ts.net` is a resolver answer, and accepting it would let whoever
|
|
63
|
+
* answers that lookup redirect a plaintext credential-bearing dial anywhere they like. */
|
|
64
|
+
function isOverlayLiteral(host) {
|
|
65
|
+
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
66
|
+
if (v4) {
|
|
67
|
+
const parts = v4.slice(1).map(Number);
|
|
68
|
+
if (parts.some((n) => n > 255))
|
|
69
|
+
return false;
|
|
70
|
+
// 100.64.0.0/10 == 100.64.0.0 through 100.127.255.255.
|
|
71
|
+
return parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127;
|
|
72
|
+
}
|
|
73
|
+
return /^fd7a:115c:a1e0:/i.test(host);
|
|
74
|
+
}
|
|
75
|
+
/** A URL's hostname with an IPv6 literal's brackets removed (`[::1]` -> `::1`). */
|
|
76
|
+
function bareHost(url) {
|
|
77
|
+
return url.hostname.startsWith("[") && url.hostname.endsWith("]")
|
|
78
|
+
? url.hostname.slice(1, -1)
|
|
79
|
+
: url.hostname;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Classify a registration target, or throw the reason it is refused.
|
|
83
|
+
*
|
|
84
|
+
* Refusal is the point: an unclassifiable target is one whose credentials would cross a network
|
|
85
|
+
* this build cannot encrypt, so it fails loud here rather than dialing and hoping.
|
|
86
|
+
*/
|
|
87
|
+
export function classifyJoinTarget(raw, policy) {
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
url = new URL(raw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error(`${JSON.stringify(raw)} is not a URL - pass a broker address like nats://100.64.0.1:4222`);
|
|
94
|
+
}
|
|
95
|
+
if (url.protocol !== "nats:" && url.protocol !== "tls:")
|
|
96
|
+
throw new Error(`${JSON.stringify(raw)} must be a nats:// or tls:// URL, not ${url.protocol}//`);
|
|
97
|
+
const host = bareHost(url);
|
|
98
|
+
if (!host)
|
|
99
|
+
throw new Error(`${JSON.stringify(raw)} has no host`);
|
|
100
|
+
const port = url.port || String(DEFAULT_PORT);
|
|
101
|
+
const server = `${url.protocol}//${url.hostname}:${port}`;
|
|
102
|
+
// Loopback is the one class that needs no transport guarantee: nothing leaves the machine, so
|
|
103
|
+
// there is nothing on a wire for anyone to sit on.
|
|
104
|
+
if (isLoopbackLiteral(host))
|
|
105
|
+
return { server, reach: "loopback" };
|
|
106
|
+
if (isOverlayLiteral(host)) {
|
|
107
|
+
if (policy.tlsRequired)
|
|
108
|
+
return { server, reach: "overlay" };
|
|
109
|
+
if (!policy.allowUnencryptedOverlay)
|
|
110
|
+
throw new Error(`${JSON.stringify(raw)} refused: ${host} is a private-overlay address, and this build cannot require TLS on the connection.\n` +
|
|
111
|
+
` That is safe while the overlay tunnel is up, and NOT safe if it is down: the range is then ordinary carrier-grade\n` +
|
|
112
|
+
` NAT, and whoever answers the dial receives the credentials this machine sends.\n` +
|
|
113
|
+
` Only you can know whether the tunnel is up, so accept it explicitly with --allow-unencrypted-overlay, which records\n` +
|
|
114
|
+
` your acceptance on the mesh entry. The flag goes away once the broker can be served over TLS.`);
|
|
115
|
+
return {
|
|
116
|
+
server,
|
|
117
|
+
reach: "overlay",
|
|
118
|
+
residual: `${host} is a private-overlay address, and this build cannot yet require TLS on the connection.\n` +
|
|
119
|
+
` That is safe while the overlay tunnel is actually up: it authenticates both machines and encrypts the link.\n` +
|
|
120
|
+
` It is NOT safe if the tunnel is down. That range is then ordinary carrier-grade NAT space, and whoever\n` +
|
|
121
|
+
` answers the dial receives the credentials this machine will send. Keep the tunnel up, and expect this to\n` +
|
|
122
|
+
` become a refusal once the broker can be served over TLS and the record can say TLS is required.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`${JSON.stringify(raw)} refused: this build cannot protect a connection to ${host}, and a machine that registers a mesh sends its agent credentials to that broker.\n` +
|
|
126
|
+
` Only a loopback literal (127.0.0.0/8, ::1) or a private-overlay literal (100.64.0.0/10, fd7a:115c:a1e0::/48) may be registered. An overlay literal additionally needs --allow-unencrypted-overlay, which records that you accepted its tunnel dependency.\n` +
|
|
127
|
+
` Ordinary private ranges are refused too: a cafe network is private, and private is not the same as yours.\n` +
|
|
128
|
+
` A hostname is refused even when it resolves somewhere permitted - otherwise whoever answers the lookup chooses which machine receives the credentials. Pass the address itself.`);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=join-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join-target.js","sourceRoot":"","sources":["../../src/lib/join-target.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { type LivenessProbe } from "@cotal-ai/workspace";
|
|
2
|
+
/** Exported so the delivery cutover preflight can NAME the pid it refused on: an error that says
|
|
3
|
+
* "cannot be attributed" without saying which pid is not actionable. */
|
|
4
|
+
export declare const MANAGER_PID_PATH: () => string;
|
|
5
|
+
/** The recorded manager's liveness, THREE-VALUED plus absent, because collapsing it to a boolean is
|
|
6
|
+
* what made this dangerous. Both collapses are silent and both are wrong:
|
|
7
|
+
* `!== "dead"` -> an `unknown` reports UP forever; no retry clears it and nothing starts.
|
|
8
|
+
* `=== "alive"` -> an `unknown` reports DOWN and a second manager launches onto a possibly-live one.
|
|
9
|
+
* `unknown` is REACHABLE on a real kernel, not just under a test shim: a Linux seccomp filter
|
|
10
|
+
* (`SECCOMP_RET_ERRNO`) or an LSM policy can return an arbitrary errno for `kill(pid, 0)` without
|
|
11
|
+
* executing it at all, and libuv preserves it. Proven with a live seccomp BPF filter, not by
|
|
12
|
+
* interposition. So the caller has to SEE the third state and refuse. */
|
|
13
|
+
export declare function managerLiveness(probe?: LivenessProbe): "alive" | "dead" | "unknown" | "absent" | "unattributable";
|
|
14
|
+
/** True only if the manager is PROVABLY running. `unknown` is not up, and callers that would ACT on
|
|
15
|
+
* that answer must use {@link managerLiveness} instead: this boolean cannot express the difference
|
|
16
|
+
* between "not running" and "cannot tell", and acting on the difference is the whole point. */
|
|
2
17
|
export declare function managerUp(): boolean;
|
|
3
18
|
/** True if the live manager carries a delivery-aware marker BOUND to its current pid (i.e. it's THIS
|
|
4
19
|
* build, non-hosting). Fail-closed: the marker stores the pid it was written for, and this requires it
|
|
@@ -20,11 +35,20 @@ export declare function startManagerDetached(o?: {
|
|
|
20
35
|
attachHost?: string;
|
|
21
36
|
resumeAttempt?: string;
|
|
22
37
|
resumeCommitToken?: string;
|
|
38
|
+
wsPort?: number;
|
|
23
39
|
}): number;
|
|
24
40
|
/** Make the control plane available: reuse a manager already running for this folder, else start
|
|
25
41
|
* one detached. Best-effort — callers treat it as non-fatal. A caller that needs THE manager to
|
|
26
42
|
* carry a runtime/launch spec (`up -f`) must stop any leftover manager first — a reused one is
|
|
27
43
|
* taken as-is. */
|
|
44
|
+
/** Refuse to stand a manager up OVER a record we cannot read or attribute.
|
|
45
|
+
*
|
|
46
|
+
* Exported because `ensureManager` is not the only path that starts one: `cotal spawn -f` calls
|
|
47
|
+
* `startManagerDetached` directly after its own lease checks, and so skipped this entirely. A lease
|
|
48
|
+
* says nobody is ANSWERING; it does not say the recorded pid is dead. Overwriting an unknown or
|
|
49
|
+
* unattributable record is the same defect as deleting it, reached through a different verb, which
|
|
50
|
+
* is the third time that shape has appeared in this change. Any future starter calls this first. */
|
|
51
|
+
export declare function assertManagerRecordReplaceable(probe?: LivenessProbe): void;
|
|
28
52
|
export declare function ensureManager(o?: {
|
|
29
53
|
space?: string;
|
|
30
54
|
server?: string;
|
|
@@ -34,10 +58,29 @@ export declare function ensureManager(o?: {
|
|
|
34
58
|
attachHost?: string;
|
|
35
59
|
resumeAttempt?: string;
|
|
36
60
|
resumeCommitToken?: string;
|
|
37
|
-
|
|
61
|
+
wsPort?: number;
|
|
62
|
+
}, probe?: LivenessProbe): {
|
|
38
63
|
running: boolean;
|
|
39
64
|
};
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
65
|
+
/** A signal, injectable for the same reason the probe is: `EPERM` from `kill` is producible only by
|
|
66
|
+
* another user's process or by kernel policy, so the branch that handles it is otherwise unreachable
|
|
67
|
+
* from a test. Production passes nothing. */
|
|
68
|
+
export type SignalFn = (pid: number, signal: NodeJS.Signals) => void;
|
|
69
|
+
/** What a stop actually achieved, because "void" let this function claim success it had not earned. */
|
|
70
|
+
export type StopVerdict = "stopped" | "already-gone";
|
|
71
|
+
/** Blocking sleep. DO NOT reintroduce this for death-waiting: it blocks the event loop, so a child
|
|
72
|
+
* of this process is never reaped, remains a signalable zombie, and reads `alive` forever. That
|
|
73
|
+
* turned `cotal up` + Ctrl-C into an exit-1 that left the broker running. Death-waits await. */
|
|
74
|
+
/** Stop the detached (pty) manager if we started one, and remove its records ONLY once it is gone.
|
|
75
|
+
*
|
|
76
|
+
* THIS USED TO CATCH EVERY SIGNAL FAILURE AS "already gone" AND DELETE THE RECORDS ANYWAY. That was
|
|
77
|
+
* survivable while `EPERM` was misread as dead, because the caller never got here: the cutover
|
|
78
|
+
* preflight skipped a manager it thought was not running. Resolving `EPERM` to `alive` (the fix this
|
|
79
|
+
* change exists for) makes the preflight recognise ANOTHER USER's live manager and call this, at
|
|
80
|
+
* which point the old code sent a signal it was not permitted to send, swallowed the refusal, and
|
|
81
|
+
* deleted the pidfile and marker of a process that was still running and still bound to Plane 3.
|
|
82
|
+
* A correct fix upstream reaching a latent destructive bug downstream is the worst shape available,
|
|
83
|
+
* so this refuses instead: records are removed only on proven death, never on a signal we could not
|
|
84
|
+
* send or a death we could not confirm. Found by review, with a kernel seccomp proof. */
|
|
85
|
+
export declare function stopManager(probe?: LivenessProbe, signal?: SignalFn | undefined): Promise<StopVerdict>;
|
|
43
86
|
//# sourceMappingURL=manager-proc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager-proc.d.ts","sourceRoot":"","sources":["../../src/lib/manager-proc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manager-proc.d.ts","sourceRoot":"","sources":["../../src/lib/manager-proc.ts"],"names":[],"mappings":"AAMA,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAElF;yEACyE;AACzE,eAAO,MAAM,gBAAgB,QAAO,MAAkC,CAAC;AASvE;;;;;;;0EAO0E;AAC1E,wBAAgB,eAAe,CAAC,KAAK,GAAE,aAA6B,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,gBAAgB,CAYhI;AAED;;gGAEgG;AAChG,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAID;;;;2CAI2C;AAC3C,wBAAgB,wBAAwB,IAAI,OAAO,CAOlD;AAED;;;;0FAI0F;AAC1F,wBAAgB,oBAAoB,CAClC,CAAC,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACzL,MAAM,CA0CR;AAED;;;mBAGmB;AACnB;;;;;;qGAMqG;AACrG,wBAAgB,8BAA8B,CAAC,KAAK,GAAE,aAA6B,GAAG,IAAI,CAczF;AAED,wBAAgB,aAAa,CAC3B,CAAC,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,EAC1L,KAAK,GAAE,aAA6B,GACnC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAMtB;AAED;;8CAE8C;AAC9C,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC;AAErE,uGAAuG;AACvG,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAErD;;iGAEiG;AAGjG;;;;;;;;;;0FAU0F;AAC1F,wBAAsB,WAAW,CAAC,KAAK,GAAE,aAA6B,EAAE,MAAM,GAAE,QAAQ,GAAG,SAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsEtI"}
|