@abelxiaoxing/cadence 2.0.1 → 2.0.3
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/README.md +104 -44
- package/docs/reuse-and-distribution.md +3 -2
- package/docs/service-management.md +62 -3
- package/package.json +10 -7
- package/src/agent-panel-cadence.ts +651 -0
- package/src/agent-panel-model.ts +521 -0
- package/src/agent-panel.ts +474 -0
- package/src/index.ts +12 -4
- package/src/install-config.mjs +25 -0
- package/src/native-client.ts +142 -3
- package/src/operator-cli.mjs +52 -16
- package/src/service/coordination.ts +35 -16
- package/src/service/private-files.ts +141 -28
- package/src/service-cli.mjs +793 -149
- package/src/service-host.mjs +847 -319
- package/src/task-shell.ts +27 -14
- package/src/task-store.ts +104 -25
- package/src/worker-tools.mjs +42 -17
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +6 -6
package/src/service-cli.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// src/service/cli.ts
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync as mkdirSync4 } from "node:fs";
|
|
6
7
|
import { request } from "node:http";
|
|
8
|
+
import { dirname as dirname8, join as join8, resolve as resolve5 } from "node:path";
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
10
|
|
|
9
11
|
// src/task-types.ts
|
|
@@ -86,7 +88,10 @@ function safeChildEnv() {
|
|
|
86
88
|
"TMPDIR",
|
|
87
89
|
"LANG",
|
|
88
90
|
"LC_ALL",
|
|
89
|
-
"PATHEXT"
|
|
91
|
+
"PATHEXT",
|
|
92
|
+
"ProgramFiles",
|
|
93
|
+
"ProgramFiles(x86)",
|
|
94
|
+
"ProgramW6432"
|
|
90
95
|
])
|
|
91
96
|
if (process.env[key] !== undefined)
|
|
92
97
|
env[key] = process.env[key];
|
|
@@ -2039,12 +2044,6 @@ class CaptureBudgetLease {
|
|
|
2039
2044
|
this.forget();
|
|
2040
2045
|
}
|
|
2041
2046
|
}
|
|
2042
|
-
// src/service/coordination.ts
|
|
2043
|
-
import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
2044
|
-
import { opendirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2045
|
-
import { hostname as hostname2 } from "node:os";
|
|
2046
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative3, sep as sep3 } from "node:path";
|
|
2047
|
-
|
|
2048
2047
|
// src/task-store.ts
|
|
2049
2048
|
import { spawnSync } from "node:child_process";
|
|
2050
2049
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -2114,11 +2113,8 @@ function safeDirectory(path, create = false, inspectExisting) {
|
|
|
2114
2113
|
at = join(at, part);
|
|
2115
2114
|
let existing;
|
|
2116
2115
|
try {
|
|
2117
|
-
const st = lstatSync(at);
|
|
2118
|
-
if (!st.isDirectory())
|
|
2119
|
-
throw new Error("Unsafe directory");
|
|
2120
2116
|
const checked = lstatSync(at);
|
|
2121
|
-
if (checked.
|
|
2117
|
+
if (!checked.isDirectory() || checked.isSymbolicLink())
|
|
2122
2118
|
throw new Error("Unsafe directory");
|
|
2123
2119
|
existing = checked;
|
|
2124
2120
|
} catch (error) {
|
|
@@ -2132,7 +2128,8 @@ function safeDirectory(path, create = false, inspectExisting) {
|
|
|
2132
2128
|
if (!object(mkdirError) || mkdirError.code !== "EEXIST")
|
|
2133
2129
|
throw mkdirError;
|
|
2134
2130
|
}
|
|
2135
|
-
|
|
2131
|
+
const created = lstatSync(at);
|
|
2132
|
+
if (!created.isDirectory() || created.isSymbolicLink())
|
|
2136
2133
|
throw new Error("Unsafe directory");
|
|
2137
2134
|
}
|
|
2138
2135
|
if (existing)
|
|
@@ -2140,13 +2137,29 @@ function safeDirectory(path, create = false, inspectExisting) {
|
|
|
2140
2137
|
}
|
|
2141
2138
|
return true;
|
|
2142
2139
|
}
|
|
2143
|
-
|
|
2140
|
+
var transientWindows = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
2141
|
+
function retryWindowsFileOperation(operation, recheck) {
|
|
2142
|
+
if (process.platform !== "win32")
|
|
2143
|
+
return operation();
|
|
2144
|
+
const deadline = performance.now() + 2000;
|
|
2145
|
+
for (let delay = 2;; delay = Math.min(delay * 2, 50)) {
|
|
2146
|
+
try {
|
|
2147
|
+
return operation();
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
if (!object(error) || !transientWindows.has(String(error.code)) || performance.now() >= deadline)
|
|
2150
|
+
throw error;
|
|
2151
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
|
|
2152
|
+
recheck?.();
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function readBounded(path, max, packaged = false) {
|
|
2144
2157
|
if (!safeDirectory(dirname(path)))
|
|
2145
2158
|
return;
|
|
2146
2159
|
let fd;
|
|
2147
2160
|
try {
|
|
2148
2161
|
const st = lstatSync(path);
|
|
2149
|
-
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max)
|
|
2162
|
+
if (!st.isFile() || st.isSymbolicLink() || !packaged && st.nlink !== 1 || st.size > max)
|
|
2150
2163
|
throw new Error("Unsafe or oversized file");
|
|
2151
2164
|
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2152
2165
|
} catch (error) {
|
|
@@ -2156,7 +2169,7 @@ function readBounded(path, max) {
|
|
|
2156
2169
|
}
|
|
2157
2170
|
try {
|
|
2158
2171
|
const st = fstatSync(fd);
|
|
2159
|
-
if (!st.isFile() || st.nlink !== 1 || st.size > max)
|
|
2172
|
+
if (!st.isFile() || !packaged && st.nlink !== 1 || st.size > max)
|
|
2160
2173
|
throw new Error("Unsafe or oversized file");
|
|
2161
2174
|
const buffer = Buffer.alloc(max + 1);
|
|
2162
2175
|
let size = 0;
|
|
@@ -2234,6 +2247,23 @@ function canonicalAmbientRoot(path) {
|
|
|
2234
2247
|
return join(canonicalAmbientRoot(parent), full.slice(parent.length));
|
|
2235
2248
|
}
|
|
2236
2249
|
}
|
|
2250
|
+
function privateRootFor(canonicalRoot, stateHome) {
|
|
2251
|
+
return join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(canonicalRoot).digest("hex"));
|
|
2252
|
+
}
|
|
2253
|
+
function contains(root, path) {
|
|
2254
|
+
const within = relative(root, resolve(path));
|
|
2255
|
+
return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
|
|
2256
|
+
}
|
|
2257
|
+
function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
2258
|
+
const canonical = realpathSync(root);
|
|
2259
|
+
return contains(canonical, privateRootFor(canonical, stateHome));
|
|
2260
|
+
}
|
|
2261
|
+
var LANDING_WORKSPACE = "cadence";
|
|
2262
|
+
function effectiveWorkspace(requested, stateHome, home = homedir()) {
|
|
2263
|
+
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
2264
|
+
return { workspace: requested, redirected: false };
|
|
2265
|
+
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
2266
|
+
}
|
|
2237
2267
|
|
|
2238
2268
|
class TaskStore {
|
|
2239
2269
|
root;
|
|
@@ -2242,9 +2272,8 @@ class TaskStore {
|
|
|
2242
2272
|
constructor(root, stateHome = defaultStateHome()) {
|
|
2243
2273
|
this.root = realpathSync(root);
|
|
2244
2274
|
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
2245
|
-
this.privateRoot =
|
|
2246
|
-
|
|
2247
|
-
if (!within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within))
|
|
2275
|
+
this.privateRoot = privateRootFor(this.root, stateHome);
|
|
2276
|
+
if (contains(this.root, this.privateRoot))
|
|
2248
2277
|
throw new Error("Private state home must be outside the working copy");
|
|
2249
2278
|
}
|
|
2250
2279
|
get lockPath() {
|
|
@@ -2296,7 +2325,7 @@ class TaskStore {
|
|
|
2296
2325
|
};
|
|
2297
2326
|
let fd;
|
|
2298
2327
|
try {
|
|
2299
|
-
fd = openSync(this.lockPath, "wx", 384);
|
|
2328
|
+
fd = retryWindowsFileOperation(() => openSync(this.lockPath, "wx", 384));
|
|
2300
2329
|
} catch {
|
|
2301
2330
|
throw new Error("Checkout busy or unresolved lock; inspect and explicitly recover only a dead owner");
|
|
2302
2331
|
}
|
|
@@ -2316,7 +2345,7 @@ class TaskStore {
|
|
|
2316
2345
|
if (lock.pid !== process.pid || lock.host !== hostname())
|
|
2317
2346
|
throw new Error("Only the owning process may release a lock");
|
|
2318
2347
|
this.assertOwner(lock);
|
|
2319
|
-
unlinkSync(this.lockPath);
|
|
2348
|
+
retryWindowsFileOperation(() => unlinkSync(this.lockPath), () => this.assertOwner(lock));
|
|
2320
2349
|
}
|
|
2321
2350
|
recover(expected, acknowledgeEscapedProcesses) {
|
|
2322
2351
|
if (!acknowledgeEscapedProcesses || expected.host !== hostname())
|
|
@@ -2396,7 +2425,12 @@ class TaskStore {
|
|
|
2396
2425
|
const latest = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
|
|
2397
2426
|
if ((latest ? contentIdentity(latest) : undefined) !== expectedIdentity)
|
|
2398
2427
|
throw new Error("Task content changed; replacement preserved without overwrite");
|
|
2399
|
-
renameSync(temporary, join(dir, "task.json"))
|
|
2428
|
+
retryWindowsFileOperation(() => renameSync(temporary, join(dir, "task.json")), () => {
|
|
2429
|
+
this.assertOwner(lock);
|
|
2430
|
+
const now = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
|
|
2431
|
+
if ((now ? contentIdentity(now) : undefined) !== expectedIdentity)
|
|
2432
|
+
throw new Error("Task content changed; replacement preserved without overwrite");
|
|
2433
|
+
});
|
|
2400
2434
|
return next;
|
|
2401
2435
|
} finally {
|
|
2402
2436
|
if (!held)
|
|
@@ -2436,6 +2470,326 @@ class TaskStore {
|
|
|
2436
2470
|
}
|
|
2437
2471
|
var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
|
|
2438
2472
|
|
|
2473
|
+
// src/service/autostart.ts
|
|
2474
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2475
|
+
import {
|
|
2476
|
+
lstatSync as lstatSync2,
|
|
2477
|
+
mkdirSync as mkdirSync2,
|
|
2478
|
+
readFileSync,
|
|
2479
|
+
realpathSync as realpathSync2,
|
|
2480
|
+
renameSync as renameSync2,
|
|
2481
|
+
rmSync,
|
|
2482
|
+
statSync,
|
|
2483
|
+
writeFileSync as writeFileSync2
|
|
2484
|
+
} from "node:fs";
|
|
2485
|
+
import { homedir as homedir2 } from "node:os";
|
|
2486
|
+
import { delimiter, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2487
|
+
var UNIT = "cadence.service";
|
|
2488
|
+
var MARKER = "# Generated by @abelxiaoxing/cadence";
|
|
2489
|
+
function hostSystem(root) {
|
|
2490
|
+
const run = (command, args) => {
|
|
2491
|
+
const r = spawnSync2(command, args, {
|
|
2492
|
+
encoding: "utf8",
|
|
2493
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2494
|
+
});
|
|
2495
|
+
return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
|
|
2496
|
+
};
|
|
2497
|
+
return {
|
|
2498
|
+
platform: process.platform,
|
|
2499
|
+
env: process.env,
|
|
2500
|
+
home: homedir2(),
|
|
2501
|
+
node: process.execPath,
|
|
2502
|
+
root,
|
|
2503
|
+
unit: UNIT,
|
|
2504
|
+
systemctl: (args) => run("systemctl", ["--user", ...args]),
|
|
2505
|
+
linger: () => {
|
|
2506
|
+
const r = run("loginctl", [
|
|
2507
|
+
"show-user",
|
|
2508
|
+
String(process.getuid?.()),
|
|
2509
|
+
"--property=Linger",
|
|
2510
|
+
"--value"
|
|
2511
|
+
]);
|
|
2512
|
+
return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
|
|
2513
|
+
},
|
|
2514
|
+
enableLinger: (interactive) => {
|
|
2515
|
+
const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
|
|
2516
|
+
"--system",
|
|
2517
|
+
"--allow-interactive-authorization=no",
|
|
2518
|
+
"--timeout=15",
|
|
2519
|
+
"call",
|
|
2520
|
+
"org.freedesktop.login1",
|
|
2521
|
+
"/org/freedesktop/login1",
|
|
2522
|
+
"org.freedesktop.login1.Manager",
|
|
2523
|
+
"SetUserLinger",
|
|
2524
|
+
"ubb",
|
|
2525
|
+
String(process.getuid?.()),
|
|
2526
|
+
"true",
|
|
2527
|
+
"false"
|
|
2528
|
+
], { stdio: "ignore", timeout: 20000 });
|
|
2529
|
+
return !r.error && r.status === 0;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
function expandHome(value, home) {
|
|
2534
|
+
if (value === "~")
|
|
2535
|
+
return home;
|
|
2536
|
+
return value.startsWith("~/") ? join2(home, value.slice(2)) : value;
|
|
2537
|
+
}
|
|
2538
|
+
function agentDirectory(s) {
|
|
2539
|
+
const selected = s.env.PI_CODING_AGENT_DIR;
|
|
2540
|
+
return resolve2(selected ? expandHome(selected, s.home) : join2(s.home, ".pi", "agent"));
|
|
2541
|
+
}
|
|
2542
|
+
function piUserInstall(s) {
|
|
2543
|
+
try {
|
|
2544
|
+
const managed = join2(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
|
|
2545
|
+
return realpathSync2(managed) === realpathSync2(s.root);
|
|
2546
|
+
} catch {
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
function sdkPackages(root) {
|
|
2551
|
+
let start;
|
|
2552
|
+
try {
|
|
2553
|
+
start = realpathSync2(join2(root, "src"));
|
|
2554
|
+
} catch {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
const found = [];
|
|
2558
|
+
for (const name of ["pi-coding-agent", "pi-ai"]) {
|
|
2559
|
+
let hit;
|
|
2560
|
+
for (let dir = start;!hit; dir = dirname2(dir)) {
|
|
2561
|
+
const candidate = join2(dir, "node_modules", "@earendil-works", name, "package.json");
|
|
2562
|
+
if (regularFile(candidate))
|
|
2563
|
+
hit = candidate;
|
|
2564
|
+
else if (dirname2(dir) === dir)
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
found.push(hit);
|
|
2568
|
+
}
|
|
2569
|
+
return found;
|
|
2570
|
+
}
|
|
2571
|
+
function unitPath(s) {
|
|
2572
|
+
const data = s.env.XDG_DATA_HOME;
|
|
2573
|
+
return join2(data && isAbsolute2(data) ? data : join2(s.home, ".local", "share"), "systemd", "user", s.unit);
|
|
2574
|
+
}
|
|
2575
|
+
function servicePath(value) {
|
|
2576
|
+
const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute2(entry) && !entry.endsWith(`${sep2}node_modules${sep2}.bin`) && !entry.includes(`${sep2}node-gyp-bin`));
|
|
2577
|
+
return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
|
|
2578
|
+
}
|
|
2579
|
+
function serviceEnvironment(s) {
|
|
2580
|
+
const out = [["PATH", servicePath(s.env.PATH)]];
|
|
2581
|
+
for (const key of ["LANG", "LC_ALL"]) {
|
|
2582
|
+
const value = s.env[key];
|
|
2583
|
+
if (value)
|
|
2584
|
+
out.push([key, value]);
|
|
2585
|
+
}
|
|
2586
|
+
const state = s.env.XDG_STATE_HOME;
|
|
2587
|
+
if (state && isAbsolute2(state))
|
|
2588
|
+
out.push(["XDG_STATE_HOME", state]);
|
|
2589
|
+
if (s.env.PI_CODING_AGENT_DIR)
|
|
2590
|
+
out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
|
|
2591
|
+
return out;
|
|
2592
|
+
}
|
|
2593
|
+
function checked(value) {
|
|
2594
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
2595
|
+
throw new ContractError("invalid");
|
|
2596
|
+
return value;
|
|
2597
|
+
}
|
|
2598
|
+
var specifiers = (value) => checked(value).replaceAll("%", "%%");
|
|
2599
|
+
var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
|
|
2600
|
+
var argument = (value) => quoted(value).replaceAll("$", "$$$$");
|
|
2601
|
+
function buildUnit(u) {
|
|
2602
|
+
const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
|
|
2603
|
+
return [
|
|
2604
|
+
`${MARKER} on Pi package install/update; upgrades rewrite this file.`,
|
|
2605
|
+
"# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
|
|
2606
|
+
"[Unit]",
|
|
2607
|
+
"Description=Cadence shared task service and WebUI",
|
|
2608
|
+
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
2609
|
+
`ConditionPathExists=${specifiers(u.cli)}`,
|
|
2610
|
+
`ConditionPathExists=${specifiers(join2(u.agentDir, "auth.json"))}`,
|
|
2611
|
+
...u.sdk.map((path) => `ConditionPathExists=${specifiers(path)}`),
|
|
2612
|
+
"StartLimitIntervalSec=300",
|
|
2613
|
+
"StartLimitBurst=3",
|
|
2614
|
+
"",
|
|
2615
|
+
"[Service]",
|
|
2616
|
+
"Type=simple",
|
|
2617
|
+
`WorkingDirectory=${specifiers(u.workspace)}`,
|
|
2618
|
+
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
2619
|
+
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
|
|
2620
|
+
`ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
|
|
2621
|
+
"KillMode=mixed",
|
|
2622
|
+
"TimeoutStopSec=90",
|
|
2623
|
+
"Restart=on-failure",
|
|
2624
|
+
"RestartSec=10",
|
|
2625
|
+
"",
|
|
2626
|
+
"[Install]",
|
|
2627
|
+
"WantedBy=default.target",
|
|
2628
|
+
""
|
|
2629
|
+
].join(`
|
|
2630
|
+
`);
|
|
2631
|
+
}
|
|
2632
|
+
function readGenerated(path) {
|
|
2633
|
+
try {
|
|
2634
|
+
if (!lstatSync2(path).isFile())
|
|
2635
|
+
return null;
|
|
2636
|
+
const text2 = readFileSync(path, "utf8");
|
|
2637
|
+
return text2.startsWith(MARKER) ? text2 : null;
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
if (error.code === "ENOENT")
|
|
2640
|
+
return;
|
|
2641
|
+
throw error;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
function generatedWorkspace(text2) {
|
|
2645
|
+
return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
|
|
2646
|
+
}
|
|
2647
|
+
function directory(path) {
|
|
2648
|
+
try {
|
|
2649
|
+
return isAbsolute2(path) && statSync(path).isDirectory();
|
|
2650
|
+
} catch {
|
|
2651
|
+
return false;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
function unitStateHome(s) {
|
|
2655
|
+
const state = s.env.XDG_STATE_HOME;
|
|
2656
|
+
return state && isAbsolute2(state) ? state : join2(s.home, ".local", "state");
|
|
2657
|
+
}
|
|
2658
|
+
function selectWorkspace(s, requested, previous) {
|
|
2659
|
+
let chosen;
|
|
2660
|
+
if (requested) {
|
|
2661
|
+
chosen = expandHome(requested, s.home);
|
|
2662
|
+
if (!directory(chosen))
|
|
2663
|
+
throw new ContractError("invalid");
|
|
2664
|
+
chosen = resolve2(chosen);
|
|
2665
|
+
} else {
|
|
2666
|
+
const kept = generatedWorkspace(previous);
|
|
2667
|
+
if (kept && directory(kept))
|
|
2668
|
+
chosen = kept;
|
|
2669
|
+
}
|
|
2670
|
+
const landing = chosen ? effectiveWorkspace(chosen, unitStateHome(s), s.home) : { workspace: join2(s.home, LANDING_WORKSPACE), redirected: false };
|
|
2671
|
+
if (landing.workspace !== chosen)
|
|
2672
|
+
mkdirSync2(landing.workspace, { recursive: true });
|
|
2673
|
+
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
2674
|
+
}
|
|
2675
|
+
function write(path, content) {
|
|
2676
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2677
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
2678
|
+
try {
|
|
2679
|
+
writeFileSync2(temporary, content, { mode: 420, flag: "wx" });
|
|
2680
|
+
renameSync2(temporary, path);
|
|
2681
|
+
} finally {
|
|
2682
|
+
rmSync(temporary, { force: true });
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function regularFile(path) {
|
|
2686
|
+
try {
|
|
2687
|
+
return statSync(path).isFile();
|
|
2688
|
+
} catch {
|
|
2689
|
+
return false;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function bootStart(s, attempt, interactive) {
|
|
2693
|
+
const current = s.linger();
|
|
2694
|
+
if (current === undefined)
|
|
2695
|
+
return {};
|
|
2696
|
+
if (current)
|
|
2697
|
+
return { linger: "on" };
|
|
2698
|
+
if (!attempt)
|
|
2699
|
+
return { linger: "off" };
|
|
2700
|
+
return { linger: s.enableLinger(interactive) ? "enabled" : "refused" };
|
|
2701
|
+
}
|
|
2702
|
+
function configureAutostart(s, mode, requested, linger = false) {
|
|
2703
|
+
if (s.platform !== "linux")
|
|
2704
|
+
return { status: "skipped" };
|
|
2705
|
+
if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
|
|
2706
|
+
return { status: "skipped" };
|
|
2707
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2708
|
+
return { status: "unavailable" };
|
|
2709
|
+
const path = unitPath(s);
|
|
2710
|
+
const previous = readGenerated(path);
|
|
2711
|
+
if (previous === null)
|
|
2712
|
+
return { status: "foreign", unit: path };
|
|
2713
|
+
const sdk = sdkPackages(s.root);
|
|
2714
|
+
if (!sdk)
|
|
2715
|
+
return { status: "missing-pi-sdk" };
|
|
2716
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested ?? s.env.CADENCE_WORKSPACE, previous);
|
|
2717
|
+
const agentDir = agentDirectory(s);
|
|
2718
|
+
const content = buildUnit({
|
|
2719
|
+
node: s.node,
|
|
2720
|
+
cli: join2(s.root, "src", "service-cli.mjs"),
|
|
2721
|
+
workspace,
|
|
2722
|
+
agentDir,
|
|
2723
|
+
sdk,
|
|
2724
|
+
environment: serviceEnvironment(s)
|
|
2725
|
+
});
|
|
2726
|
+
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
|
|
2727
|
+
const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
|
|
2728
|
+
if (active)
|
|
2729
|
+
s.systemctl(["stop", s.unit]);
|
|
2730
|
+
write(path, content);
|
|
2731
|
+
s.systemctl(["daemon-reload"]);
|
|
2732
|
+
const result = {
|
|
2733
|
+
unit: path,
|
|
2734
|
+
workspace,
|
|
2735
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
2736
|
+
};
|
|
2737
|
+
if (enabled.startsWith("masked"))
|
|
2738
|
+
return { status: "masked", ...result };
|
|
2739
|
+
const wanted = mode === "install" || previous === undefined || enabled === "enabled";
|
|
2740
|
+
if (wanted && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
2741
|
+
return { status: "failed", ...result };
|
|
2742
|
+
if (!wanted && !active)
|
|
2743
|
+
return { status: "disabled", ...result };
|
|
2744
|
+
const boot = () => wanted ? bootStart(s, mode === "install" ? linger : previous === undefined && s.env.CADENCE_LINGER !== "0", mode === "install") : {};
|
|
2745
|
+
if (!regularFile(join2(agentDir, "auth.json")))
|
|
2746
|
+
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
2747
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
2748
|
+
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
2749
|
+
return { status: started ? "started" : "failed", ...result, ...boot() };
|
|
2750
|
+
}
|
|
2751
|
+
function removeAutostart(s) {
|
|
2752
|
+
if (s.platform !== "linux")
|
|
2753
|
+
return { status: "skipped" };
|
|
2754
|
+
const path = unitPath(s);
|
|
2755
|
+
const previous = readGenerated(path);
|
|
2756
|
+
if (previous === null)
|
|
2757
|
+
return { status: "foreign", unit: path };
|
|
2758
|
+
if (previous === undefined)
|
|
2759
|
+
return { status: "absent" };
|
|
2760
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2761
|
+
return { status: "unavailable", unit: path };
|
|
2762
|
+
if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
|
|
2763
|
+
return { status: "failed", unit: path };
|
|
2764
|
+
rmSync(path);
|
|
2765
|
+
s.systemctl(["daemon-reload"]);
|
|
2766
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
2767
|
+
return { status: "removed", unit: path };
|
|
2768
|
+
}
|
|
2769
|
+
function autostartStatus(s) {
|
|
2770
|
+
if (s.platform !== "linux")
|
|
2771
|
+
return { status: "skipped" };
|
|
2772
|
+
const path = unitPath(s);
|
|
2773
|
+
const previous = readGenerated(path);
|
|
2774
|
+
if (previous === null)
|
|
2775
|
+
return { status: "foreign", unit: path };
|
|
2776
|
+
if (previous === undefined)
|
|
2777
|
+
return { status: "absent" };
|
|
2778
|
+
return {
|
|
2779
|
+
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
2780
|
+
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
2781
|
+
unit: path,
|
|
2782
|
+
workspace: generatedWorkspace(previous),
|
|
2783
|
+
...bootStart(s, false, false)
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/service/coordination.ts
|
|
2788
|
+
import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
2789
|
+
import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
2790
|
+
import { hostname as hostname2 } from "node:os";
|
|
2791
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join4, relative as relative3, sep as sep4 } from "node:path";
|
|
2792
|
+
|
|
2439
2793
|
// src/service/private-files.ts
|
|
2440
2794
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
2441
2795
|
import {
|
|
@@ -2443,13 +2797,15 @@ import {
|
|
|
2443
2797
|
constants as constants2,
|
|
2444
2798
|
fstatSync as fstatSync2,
|
|
2445
2799
|
fsyncSync as fsyncSync2,
|
|
2446
|
-
lstatSync as
|
|
2800
|
+
lstatSync as lstatSync3,
|
|
2801
|
+
mkdirSync as mkdirSync3,
|
|
2447
2802
|
openSync as openSync2,
|
|
2448
|
-
|
|
2803
|
+
readSync as readSync2,
|
|
2804
|
+
renameSync as renameSync3,
|
|
2449
2805
|
unlinkSync as unlinkSync2,
|
|
2450
|
-
writeFileSync as
|
|
2806
|
+
writeFileSync as writeFileSync3
|
|
2451
2807
|
} from "node:fs";
|
|
2452
|
-
import { dirname as
|
|
2808
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
2453
2809
|
var METADATA_BYTES = 16 * 1024;
|
|
2454
2810
|
var REGISTRY_BYTES = 1024 * 1024;
|
|
2455
2811
|
var REGISTRY_ENTRIES = 128;
|
|
@@ -2459,8 +2815,8 @@ function refused() {
|
|
|
2459
2815
|
function hash(bytes) {
|
|
2460
2816
|
return createHash2("sha256").update(bytes).digest("hex");
|
|
2461
2817
|
}
|
|
2462
|
-
function privateMode(st) {
|
|
2463
|
-
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2818
|
+
function privateMode(st, ownerOnly = true) {
|
|
2819
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2464
2820
|
refused();
|
|
2465
2821
|
}
|
|
2466
2822
|
function same(a, b) {
|
|
@@ -2469,42 +2825,114 @@ function same(a, b) {
|
|
|
2469
2825
|
function missing(error) {
|
|
2470
2826
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2471
2827
|
}
|
|
2828
|
+
var job = 0;
|
|
2829
|
+
var advancing = false;
|
|
2830
|
+
function currentJob() {
|
|
2831
|
+
if (!advancing) {
|
|
2832
|
+
advancing = true;
|
|
2833
|
+
queueMicrotask(() => {
|
|
2834
|
+
job++;
|
|
2835
|
+
advancing = false;
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
return job;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
class DirectoryWalks {
|
|
2842
|
+
job = currentJob();
|
|
2843
|
+
walked = new Map;
|
|
2844
|
+
open() {
|
|
2845
|
+
if (this.job === job)
|
|
2846
|
+
return true;
|
|
2847
|
+
this.walked.clear();
|
|
2848
|
+
return false;
|
|
2849
|
+
}
|
|
2850
|
+
remember(path, st) {
|
|
2851
|
+
if (this.open())
|
|
2852
|
+
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2853
|
+
}
|
|
2854
|
+
reuse(path, ownerOnly = true) {
|
|
2855
|
+
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2856
|
+
if (!walked)
|
|
2857
|
+
return false;
|
|
2858
|
+
let st;
|
|
2859
|
+
try {
|
|
2860
|
+
st = lstatSync3(path);
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
if (!missing(error))
|
|
2863
|
+
throw error;
|
|
2864
|
+
this.walked.delete(path);
|
|
2865
|
+
return false;
|
|
2866
|
+
}
|
|
2867
|
+
if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
|
|
2868
|
+
this.walked.delete(path);
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
privateMode(st, ownerOnly);
|
|
2872
|
+
return true;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2472
2875
|
|
|
2473
2876
|
class PrivateFiles {
|
|
2474
2877
|
namespace;
|
|
2475
|
-
|
|
2878
|
+
ownerOnly;
|
|
2879
|
+
constructor(namespace, ownerOnly = true) {
|
|
2476
2880
|
this.namespace = namespace;
|
|
2881
|
+
this.ownerOnly = ownerOnly;
|
|
2477
2882
|
}
|
|
2478
|
-
directory(path, create = false) {
|
|
2883
|
+
directory(path, create = false, walks) {
|
|
2479
2884
|
const tail = relative2(this.namespace, path);
|
|
2480
|
-
if (
|
|
2885
|
+
if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
|
|
2481
2886
|
refused();
|
|
2482
2887
|
if (!create) {
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2485
|
-
|
|
2888
|
+
const full = resolve3(path);
|
|
2889
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2890
|
+
return true;
|
|
2891
|
+
const namespace = resolve3(this.namespace);
|
|
2892
|
+
let inside = false, last;
|
|
2893
|
+
const found = safeDirectory(path, false, (at2, st) => {
|
|
2486
2894
|
if (at2 === namespace)
|
|
2487
2895
|
inside = true;
|
|
2488
2896
|
if (inside)
|
|
2489
|
-
privateMode(st);
|
|
2897
|
+
privateMode(st, this.ownerOnly);
|
|
2898
|
+
last = st;
|
|
2490
2899
|
});
|
|
2900
|
+
if (found && last)
|
|
2901
|
+
walks?.remember(full, last);
|
|
2902
|
+
return found;
|
|
2491
2903
|
}
|
|
2904
|
+
if (!safeDirectory(this.namespace, true))
|
|
2905
|
+
return false;
|
|
2906
|
+
privateMode(lstatSync3(this.namespace), this.ownerOnly);
|
|
2492
2907
|
let at = this.namespace;
|
|
2493
|
-
for (const part of
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2908
|
+
for (const part of tail.split(sep3).filter(Boolean)) {
|
|
2909
|
+
at = join3(at, part);
|
|
2910
|
+
let st;
|
|
2911
|
+
try {
|
|
2912
|
+
st = lstatSync3(at);
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
if (!missing(error))
|
|
2915
|
+
throw error;
|
|
2916
|
+
try {
|
|
2917
|
+
mkdirSync3(at, { mode: 448 });
|
|
2918
|
+
} catch (mkdirError) {
|
|
2919
|
+
if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
|
|
2920
|
+
throw mkdirError;
|
|
2921
|
+
}
|
|
2922
|
+
st = lstatSync3(at);
|
|
2923
|
+
}
|
|
2924
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2925
|
+
throw new Error("Unsafe directory");
|
|
2926
|
+
privateMode(st, this.ownerOnly);
|
|
2499
2927
|
}
|
|
2500
2928
|
return true;
|
|
2501
2929
|
}
|
|
2502
|
-
read(path, maximum = METADATA_BYTES) {
|
|
2503
|
-
if (!this.directory(
|
|
2930
|
+
read(path, maximum = METADATA_BYTES, walks) {
|
|
2931
|
+
if (!this.directory(dirname3(path), false, walks))
|
|
2504
2932
|
return;
|
|
2505
2933
|
let before;
|
|
2506
2934
|
try {
|
|
2507
|
-
before =
|
|
2935
|
+
before = lstatSync3(path);
|
|
2508
2936
|
} catch (error) {
|
|
2509
2937
|
if (missing(error))
|
|
2510
2938
|
return;
|
|
@@ -2512,37 +2940,45 @@ class PrivateFiles {
|
|
|
2512
2940
|
}
|
|
2513
2941
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2514
2942
|
refused();
|
|
2515
|
-
privateMode(before);
|
|
2943
|
+
privateMode(before, this.ownerOnly);
|
|
2516
2944
|
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2517
2945
|
try {
|
|
2518
2946
|
const opened = fstatSync2(fd);
|
|
2519
2947
|
if (!same(before, opened))
|
|
2520
2948
|
refused();
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2949
|
+
const buffer = Buffer.alloc(opened.size + 1);
|
|
2950
|
+
let size = 0;
|
|
2951
|
+
while (size < buffer.length) {
|
|
2952
|
+
const n = readSync2(fd, buffer, size, buffer.length - size, null);
|
|
2953
|
+
if (!n)
|
|
2954
|
+
break;
|
|
2955
|
+
size += n;
|
|
2956
|
+
}
|
|
2957
|
+
const bytes = buffer.subarray(0, size);
|
|
2958
|
+
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path)))
|
|
2523
2959
|
refused();
|
|
2524
2960
|
return { bytes, identity: hash(bytes), stamp: stamp(opened) };
|
|
2525
2961
|
} finally {
|
|
2526
2962
|
closeSync2(fd);
|
|
2527
2963
|
}
|
|
2528
2964
|
}
|
|
2529
|
-
assert(path, expected, maximum = METADATA_BYTES) {
|
|
2530
|
-
const current = this.read(path, maximum);
|
|
2965
|
+
assert(path, expected, maximum = METADATA_BYTES, walks) {
|
|
2966
|
+
const current = this.read(path, maximum, walks);
|
|
2531
2967
|
if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
|
|
2532
2968
|
throw new ContractError("conflict");
|
|
2533
2969
|
}
|
|
2534
2970
|
create(path, bytes, maximum = METADATA_BYTES) {
|
|
2535
2971
|
if (Buffer.byteLength(bytes) > maximum)
|
|
2536
2972
|
throw new ContractError("oversize");
|
|
2537
|
-
this.directory(
|
|
2538
|
-
const fd = openSync2(path, "wx", 384);
|
|
2973
|
+
this.directory(dirname3(path), true);
|
|
2974
|
+
const fd = retryWindowsFileOperation(() => openSync2(path, "wx", 384));
|
|
2539
2975
|
try {
|
|
2540
|
-
|
|
2976
|
+
writeFileSync3(fd, bytes);
|
|
2541
2977
|
fsyncSync2(fd);
|
|
2542
2978
|
} finally {
|
|
2543
2979
|
closeSync2(fd);
|
|
2544
2980
|
}
|
|
2545
|
-
this.syncDirectory(
|
|
2981
|
+
this.syncDirectory(dirname3(path));
|
|
2546
2982
|
const result = this.read(path, maximum);
|
|
2547
2983
|
if (!result || result.identity !== hash(bytes))
|
|
2548
2984
|
refused();
|
|
@@ -2550,11 +2986,11 @@ class PrivateFiles {
|
|
|
2550
2986
|
}
|
|
2551
2987
|
replace(path, expected, bytes, maximum = METADATA_BYTES) {
|
|
2552
2988
|
this.assert(path, expected, maximum);
|
|
2553
|
-
const temporary =
|
|
2989
|
+
const temporary = join3(dirname3(path), `pending-${randomUUID2()}.json`);
|
|
2554
2990
|
this.create(temporary, bytes, maximum);
|
|
2555
2991
|
this.assert(path, expected, maximum);
|
|
2556
|
-
|
|
2557
|
-
this.syncDirectory(
|
|
2992
|
+
retryWindowsFileOperation(() => renameSync3(temporary, path), () => this.assert(path, expected, maximum));
|
|
2993
|
+
this.syncDirectory(dirname3(path));
|
|
2558
2994
|
const result = this.read(path, maximum);
|
|
2559
2995
|
if (!result || result.identity !== hash(bytes))
|
|
2560
2996
|
refused();
|
|
@@ -2562,8 +2998,8 @@ class PrivateFiles {
|
|
|
2562
2998
|
}
|
|
2563
2999
|
remove(path, expected, maximum = METADATA_BYTES) {
|
|
2564
3000
|
this.assert(path, expected, maximum);
|
|
2565
|
-
unlinkSync2(path);
|
|
2566
|
-
this.syncDirectory(
|
|
3001
|
+
retryWindowsFileOperation(() => unlinkSync2(path), () => this.assert(path, expected, maximum));
|
|
3002
|
+
this.syncDirectory(dirname3(path));
|
|
2567
3003
|
}
|
|
2568
3004
|
syncDirectory(path) {
|
|
2569
3005
|
if (process.platform === "win32")
|
|
@@ -2617,7 +3053,7 @@ var registrationKeys = [
|
|
|
2617
3053
|
var reservationKeys = registrationKeys.filter((k) => !["schema", "phase", "checkoutToken"].includes(k));
|
|
2618
3054
|
function pathValue(value) {
|
|
2619
3055
|
const path = text(value, 4096, true);
|
|
2620
|
-
if (!
|
|
3056
|
+
if (!isAbsolute4(path))
|
|
2621
3057
|
refused();
|
|
2622
3058
|
return path;
|
|
2623
3059
|
}
|
|
@@ -2674,7 +3110,7 @@ function decodeRegistration(value) {
|
|
|
2674
3110
|
function rootsOverlap(a, b) {
|
|
2675
3111
|
const within = (x, y) => {
|
|
2676
3112
|
const r = relative3(x, y);
|
|
2677
|
-
return r === "" || r !== ".." && !r.startsWith(`..${
|
|
3113
|
+
return r === "" || r !== ".." && !r.startsWith(`..${sep4}`) && !isAbsolute4(r);
|
|
2678
3114
|
};
|
|
2679
3115
|
return within(a, b) || within(b, a);
|
|
2680
3116
|
}
|
|
@@ -2726,17 +3162,17 @@ class ServiceCoordination {
|
|
|
2726
3162
|
stateHome;
|
|
2727
3163
|
constructor(workspace, stateHome) {
|
|
2728
3164
|
this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
2729
|
-
const namespace =
|
|
3165
|
+
const namespace = dirname4(this.store.privateRoot);
|
|
2730
3166
|
this.files = new PrivateFiles(namespace);
|
|
2731
|
-
this.stateHome =
|
|
2732
|
-
this.directory =
|
|
2733
|
-
this.registryDirectory =
|
|
3167
|
+
this.stateHome = dirname4(namespace);
|
|
3168
|
+
this.directory = join4(this.store.privateRoot, "service-v1");
|
|
3169
|
+
this.registryDirectory = join4(namespace, "service-registry-v1");
|
|
2734
3170
|
}
|
|
2735
3171
|
path(name) {
|
|
2736
|
-
return
|
|
3172
|
+
return join4(this.directory, name);
|
|
2737
3173
|
}
|
|
2738
3174
|
registryPath(name) {
|
|
2739
|
-
return
|
|
3175
|
+
return join4(this.registryDirectory, name);
|
|
2740
3176
|
}
|
|
2741
3177
|
registry() {
|
|
2742
3178
|
if (!this.files.directory(this.registryDirectory))
|
|
@@ -2756,7 +3192,7 @@ class ServiceCoordination {
|
|
|
2756
3192
|
const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
|
|
2757
3193
|
const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
|
|
2758
3194
|
for (const [i, row] of entries.entries()) {
|
|
2759
|
-
if (row.stateHome !== this.stateHome ||
|
|
3195
|
+
if (row.stateHome !== this.stateHome || realpathSync3(row.root) !== row.root)
|
|
2760
3196
|
refused();
|
|
2761
3197
|
if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
|
|
2762
3198
|
refused();
|
|
@@ -2770,8 +3206,8 @@ class ServiceCoordination {
|
|
|
2770
3206
|
}
|
|
2771
3207
|
};
|
|
2772
3208
|
}
|
|
2773
|
-
assertNamespace() {
|
|
2774
|
-
if (!this.files.directory(this.directory))
|
|
3209
|
+
assertNamespace(walks) {
|
|
3210
|
+
if (!this.files.directory(this.directory, false, walks))
|
|
2775
3211
|
refused();
|
|
2776
3212
|
names(this.directory, [
|
|
2777
3213
|
"format.json",
|
|
@@ -2784,14 +3220,15 @@ class ServiceCoordination {
|
|
|
2784
3220
|
"host-config.json",
|
|
2785
3221
|
"history-v2"
|
|
2786
3222
|
]);
|
|
2787
|
-
this.files.directory(
|
|
3223
|
+
this.files.directory(join4(this.directory, "history-v2"), false, walks);
|
|
2788
3224
|
}
|
|
2789
3225
|
selected() {
|
|
2790
|
-
|
|
3226
|
+
const walks = new DirectoryWalks;
|
|
3227
|
+
if (!this.files.directory(this.directory, false, walks))
|
|
2791
3228
|
return;
|
|
2792
|
-
this.assertNamespace();
|
|
2793
|
-
const manifest = this.files.read(this.path("format.json"));
|
|
2794
|
-
const config = this.files.read(this.path("config.json"));
|
|
3229
|
+
this.assertNamespace(walks);
|
|
3230
|
+
const manifest = this.files.read(this.path("format.json"), undefined, walks);
|
|
3231
|
+
const config = this.files.read(this.path("config.json"), undefined, walks);
|
|
2795
3232
|
if (!manifest || !config)
|
|
2796
3233
|
refused();
|
|
2797
3234
|
const f = closed(json(manifest), [
|
|
@@ -2805,13 +3242,13 @@ class ServiceCoordination {
|
|
|
2805
3242
|
literal(f.stateHome, this.stateHome);
|
|
2806
3243
|
literal(f.configIdentity, config.identity);
|
|
2807
3244
|
const c = decodeOwnerConfig(json(config));
|
|
2808
|
-
const hostConfig = this.files.read(this.path("host-config.json"));
|
|
3245
|
+
const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
|
|
2809
3246
|
if (hostConfig && hostConfig.identity !== c.policyIdentity)
|
|
2810
3247
|
refused();
|
|
2811
|
-
const registration = this.files.read(this.path("registration.json"));
|
|
2812
|
-
const owner = this.files.read(this.path("owner.lock"));
|
|
2813
|
-
const capability = this.files.read(this.path("local-capability.json"));
|
|
2814
|
-
const checkout = this.files.read(this.store.lockPath, 8192);
|
|
3248
|
+
const registration = this.files.read(this.path("registration.json"), undefined, walks);
|
|
3249
|
+
const owner = this.files.read(this.path("owner.lock"), undefined, walks);
|
|
3250
|
+
const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
|
|
3251
|
+
const checkout = this.files.read(this.store.lockPath, 8192, walks);
|
|
2815
3252
|
const value = registration ? decodeRegistration(json(registration)) : undefined;
|
|
2816
3253
|
if (value && (value.root !== this.store.root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
|
|
2817
3254
|
refused();
|
|
@@ -3158,8 +3595,8 @@ class OwnerLease {
|
|
|
3158
3595
|
assert() {
|
|
3159
3596
|
if (this.released || this.selected.value.pid !== process.pid || this.selected.value.host !== hostname2())
|
|
3160
3597
|
throw new ContractError("conflict");
|
|
3161
|
-
const c = this.coordination;
|
|
3162
|
-
c.assertNamespace();
|
|
3598
|
+
const c = this.coordination, walks = new DirectoryWalks;
|
|
3599
|
+
c.assertNamespace(walks);
|
|
3163
3600
|
for (const [name, snapshot] of [
|
|
3164
3601
|
["config.json", this.selected.config],
|
|
3165
3602
|
["host-config.json", this.selected.hostConfig],
|
|
@@ -3168,8 +3605,8 @@ class OwnerLease {
|
|
|
3168
3605
|
["owner.lock", this.selected.owner],
|
|
3169
3606
|
["local-capability.json", this.selected.capability]
|
|
3170
3607
|
])
|
|
3171
|
-
c.files.assert(c.path(name), snapshot);
|
|
3172
|
-
c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
|
|
3608
|
+
c.files.assert(c.path(name), snapshot, undefined, walks);
|
|
3609
|
+
c.files.assert(c.store.lockPath, this.selected.checkout, 8192, walks);
|
|
3173
3610
|
c.store.assertOwner(this.checkout);
|
|
3174
3611
|
}
|
|
3175
3612
|
phase(phase) {
|
|
@@ -3208,8 +3645,8 @@ import { isatty } from "node:tty";
|
|
|
3208
3645
|
|
|
3209
3646
|
// src/history/recovery.ts
|
|
3210
3647
|
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
3211
|
-
import { lstatSync as
|
|
3212
|
-
import { join as
|
|
3648
|
+
import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
|
|
3649
|
+
import { join as join6, relative as relative4 } from "node:path";
|
|
3213
3650
|
|
|
3214
3651
|
// src/service/recovery.ts
|
|
3215
3652
|
async function inspectRecovery(coordination, history) {
|
|
@@ -3263,13 +3700,13 @@ import {
|
|
|
3263
3700
|
constants as constants4,
|
|
3264
3701
|
fstatSync as fstatSync4,
|
|
3265
3702
|
fsyncSync as fsyncSync3,
|
|
3266
|
-
lstatSync as
|
|
3703
|
+
lstatSync as lstatSync5,
|
|
3267
3704
|
opendirSync as opendirSync2,
|
|
3268
3705
|
openSync as openSync4,
|
|
3269
3706
|
statfsSync
|
|
3270
3707
|
} from "node:fs";
|
|
3271
3708
|
import { open } from "node:fs/promises";
|
|
3272
|
-
import { dirname as
|
|
3709
|
+
import { dirname as dirname6, join as join5 } from "node:path";
|
|
3273
3710
|
|
|
3274
3711
|
// src/history/admission.ts
|
|
3275
3712
|
function decodeRunAdmission(value) {
|
|
@@ -3293,19 +3730,19 @@ import {
|
|
|
3293
3730
|
closeSync as closeSync3,
|
|
3294
3731
|
constants as constants3,
|
|
3295
3732
|
fstatSync as fstatSync3,
|
|
3296
|
-
lstatSync as
|
|
3733
|
+
lstatSync as lstatSync4,
|
|
3297
3734
|
openSync as openSync3,
|
|
3298
|
-
readSync as
|
|
3735
|
+
readSync as readSync3
|
|
3299
3736
|
} from "node:fs";
|
|
3300
|
-
import { dirname as
|
|
3737
|
+
import { dirname as dirname5 } from "node:path";
|
|
3301
3738
|
function bad() {
|
|
3302
3739
|
throw new ContractError("corrupt-selected");
|
|
3303
3740
|
}
|
|
3304
3741
|
var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
|
|
3305
|
-
function selected(files, path, read) {
|
|
3306
|
-
if (!files.files.directory(
|
|
3742
|
+
function selected(files, path, read, walks) {
|
|
3743
|
+
if (!files.files.directory(dirname5(path), false, walks))
|
|
3307
3744
|
bad();
|
|
3308
|
-
const before =
|
|
3745
|
+
const before = lstatSync4(path);
|
|
3309
3746
|
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
3310
3747
|
bad();
|
|
3311
3748
|
const fd = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
|
|
@@ -3313,7 +3750,7 @@ function selected(files, path, read) {
|
|
|
3313
3750
|
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
3314
3751
|
bad();
|
|
3315
3752
|
const result = read(fd, before.size, stamp2(before));
|
|
3316
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(
|
|
3753
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path)) !== stamp2(before))
|
|
3317
3754
|
bad();
|
|
3318
3755
|
return result;
|
|
3319
3756
|
} finally {
|
|
@@ -3764,13 +4201,24 @@ function decodeJournalText(line) {
|
|
|
3764
4201
|
|
|
3765
4202
|
// src/history/files.ts
|
|
3766
4203
|
class HistoryReadBudget {
|
|
4204
|
+
clock;
|
|
3767
4205
|
started = performance.now();
|
|
3768
4206
|
bytes = 0;
|
|
4207
|
+
walks = new DirectoryWalks;
|
|
4208
|
+
constructor(clock = true) {
|
|
4209
|
+
this.clock = clock;
|
|
4210
|
+
}
|
|
4211
|
+
static authority() {
|
|
4212
|
+
return new HistoryReadBudget(false);
|
|
4213
|
+
}
|
|
3769
4214
|
reserve(bytes) {
|
|
3770
|
-
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes ||
|
|
4215
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
|
|
3771
4216
|
throw new ContractError("receipt-inconclusive");
|
|
3772
4217
|
this.bytes += bytes;
|
|
3773
4218
|
}
|
|
4219
|
+
get elapsed() {
|
|
4220
|
+
return this.clock && performance.now() - this.started >= LIMITS.searchMs;
|
|
4221
|
+
}
|
|
3774
4222
|
}
|
|
3775
4223
|
function corrupt() {
|
|
3776
4224
|
throw new ContractError("corrupt-selected");
|
|
@@ -3789,14 +4237,14 @@ class HistoryFiles {
|
|
|
3789
4237
|
this.coordination = coordination;
|
|
3790
4238
|
this.format = format;
|
|
3791
4239
|
this.formatSnapshot = formatSnapshot;
|
|
3792
|
-
this.directory =
|
|
4240
|
+
this.directory = join5(coordination.directory, "history-v2");
|
|
3793
4241
|
this.files = coordination.files;
|
|
3794
4242
|
}
|
|
3795
4243
|
static open(coordination) {
|
|
3796
|
-
const dir =
|
|
4244
|
+
const dir = join5(coordination.directory, "history-v2");
|
|
3797
4245
|
if (!coordination.files.directory(dir))
|
|
3798
4246
|
return;
|
|
3799
|
-
const file = coordination.files.read(
|
|
4247
|
+
const file = coordination.files.read(join5(dir, "format.json"));
|
|
3800
4248
|
if (!file)
|
|
3801
4249
|
corrupt();
|
|
3802
4250
|
const format = decodeFormat(json(file));
|
|
@@ -3812,7 +4260,7 @@ class HistoryFiles {
|
|
|
3812
4260
|
existing.assertWritable();
|
|
3813
4261
|
return existing;
|
|
3814
4262
|
}
|
|
3815
|
-
const dir =
|
|
4263
|
+
const dir = join5(c.directory, "history-v2");
|
|
3816
4264
|
const format = {
|
|
3817
4265
|
schema: FORMAT,
|
|
3818
4266
|
storeId: randomUUID4(),
|
|
@@ -3822,19 +4270,19 @@ class HistoryFiles {
|
|
|
3822
4270
|
c.files.directory(dir, true);
|
|
3823
4271
|
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
3824
4272
|
lease.assert();
|
|
3825
|
-
c.files.directory(
|
|
4273
|
+
c.files.directory(join5(dir, name), true);
|
|
3826
4274
|
}
|
|
3827
4275
|
lease.assert();
|
|
3828
|
-
const keyPath =
|
|
4276
|
+
const keyPath = join5(dir, "read-proof-key.json");
|
|
3829
4277
|
c.files.create(keyPath, encoded({
|
|
3830
4278
|
schema: "cadence-history-read-key/1",
|
|
3831
4279
|
storeId: format.storeId,
|
|
3832
4280
|
key: randomBytes2(32).toString("base64url")
|
|
3833
4281
|
}));
|
|
3834
4282
|
lease.assert();
|
|
3835
|
-
const snapshot = c.files.create(
|
|
4283
|
+
const snapshot = c.files.create(join5(dir, "format.json"), encoded(format));
|
|
3836
4284
|
const result = new HistoryFiles(c, format, snapshot);
|
|
3837
|
-
result.syncParents(
|
|
4285
|
+
result.syncParents(join5(dir, "format.json"));
|
|
3838
4286
|
result.assertWritable();
|
|
3839
4287
|
return result;
|
|
3840
4288
|
}
|
|
@@ -3869,7 +4317,7 @@ class HistoryFiles {
|
|
|
3869
4317
|
throw new ContractError("quarantine");
|
|
3870
4318
|
}
|
|
3871
4319
|
assertFormat() {
|
|
3872
|
-
this.files.assert(
|
|
4320
|
+
this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot);
|
|
3873
4321
|
}
|
|
3874
4322
|
readProofMaterial(budget) {
|
|
3875
4323
|
if (this.format.schema !== FORMAT)
|
|
@@ -3878,7 +4326,7 @@ class HistoryFiles {
|
|
|
3878
4326
|
this.assertReadFormat(budget);
|
|
3879
4327
|
else
|
|
3880
4328
|
this.assertFormat();
|
|
3881
|
-
const selected2 = this.readBudgeted(
|
|
4329
|
+
const selected2 = this.readBudgeted(join5(this.directory, "read-proof-key.json"), 512, budget);
|
|
3882
4330
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
3883
4331
|
corrupt();
|
|
3884
4332
|
try {
|
|
@@ -3906,17 +4354,17 @@ class HistoryFiles {
|
|
|
3906
4354
|
return;
|
|
3907
4355
|
}
|
|
3908
4356
|
this.assertFormat();
|
|
3909
|
-
assertIncarnation(this,
|
|
4357
|
+
assertIncarnation(this, join5(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
3910
4358
|
}
|
|
3911
4359
|
segmentPath(segment) {
|
|
3912
|
-
return
|
|
4360
|
+
return join5(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
3913
4361
|
}
|
|
3914
4362
|
artifactPath(ref) {
|
|
3915
|
-
return
|
|
4363
|
+
return join5(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
3916
4364
|
}
|
|
3917
4365
|
admissionPath(runId) {
|
|
3918
4366
|
decodeId(runId);
|
|
3919
|
-
return
|
|
4367
|
+
return join5(this.directory, "runs", runId, "admission.json");
|
|
3920
4368
|
}
|
|
3921
4369
|
writeAdmission(lease, input) {
|
|
3922
4370
|
lease.assert();
|
|
@@ -3930,19 +4378,19 @@ class HistoryFiles {
|
|
|
3930
4378
|
return this.files.read(path, maximum);
|
|
3931
4379
|
let size;
|
|
3932
4380
|
try {
|
|
3933
|
-
size =
|
|
4381
|
+
size = lstatSync5(path).size;
|
|
3934
4382
|
} catch {
|
|
3935
|
-
return this.files.read(path, 0);
|
|
4383
|
+
return this.files.read(path, 0, budget.walks);
|
|
3936
4384
|
}
|
|
3937
4385
|
if (size > maximum)
|
|
3938
4386
|
corrupt();
|
|
3939
4387
|
budget.reserve(size);
|
|
3940
|
-
return this.files.read(path, size);
|
|
4388
|
+
return this.files.read(path, size, budget.walks);
|
|
3941
4389
|
}
|
|
3942
4390
|
readCommand(commandId, budget) {
|
|
3943
4391
|
decodeId(commandId);
|
|
3944
4392
|
this.assertReadFormat(budget);
|
|
3945
|
-
return this.readBudgeted(
|
|
4393
|
+
return this.readBudgeted(join5(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
3946
4394
|
}
|
|
3947
4395
|
readAdmission(runId, identity, budget) {
|
|
3948
4396
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -3963,7 +4411,7 @@ class HistoryFiles {
|
|
|
3963
4411
|
syncParents(file) {
|
|
3964
4412
|
if (process.platform === "win32")
|
|
3965
4413
|
return;
|
|
3966
|
-
for (let path =
|
|
4414
|
+
for (let path = dirname6(file);; path = dirname6(path)) {
|
|
3967
4415
|
if (!this.files.directory(path))
|
|
3968
4416
|
corrupt();
|
|
3969
4417
|
const fd = openSync4(path, constants4.O_RDONLY | constants4.O_DIRECTORY);
|
|
@@ -3974,13 +4422,13 @@ class HistoryFiles {
|
|
|
3974
4422
|
}
|
|
3975
4423
|
if (path === this.coordination.directory)
|
|
3976
4424
|
break;
|
|
3977
|
-
if (path ===
|
|
4425
|
+
if (path === dirname6(path))
|
|
3978
4426
|
corrupt();
|
|
3979
4427
|
}
|
|
3980
4428
|
}
|
|
3981
4429
|
assertReadFormat(budget) {
|
|
3982
4430
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
3983
|
-
this.files.assert(
|
|
4431
|
+
this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
3984
4432
|
}
|
|
3985
4433
|
assertPublished(segment, end, seq) {
|
|
3986
4434
|
const fence = publicationFences.get(this.directory);
|
|
@@ -4018,7 +4466,7 @@ class HistoryFiles {
|
|
|
4018
4466
|
const dir = opendirSync2(path);
|
|
4019
4467
|
try {
|
|
4020
4468
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
4021
|
-
const p =
|
|
4469
|
+
const p = join5(path, e.name), st = lstatSync5(p);
|
|
4022
4470
|
if (st.isDirectory())
|
|
4023
4471
|
bytes += walk(p, depth + 1);
|
|
4024
4472
|
else {
|
|
@@ -4053,7 +4501,7 @@ class HistoryFiles {
|
|
|
4053
4501
|
state.bytes = 0;
|
|
4054
4502
|
state.identity = null;
|
|
4055
4503
|
let count = 0;
|
|
4056
|
-
const ledger =
|
|
4504
|
+
const ledger = join5(this.directory, "ledger");
|
|
4057
4505
|
if (!this.files.directory(ledger))
|
|
4058
4506
|
corrupt();
|
|
4059
4507
|
const dir = opendirSync2(ledger);
|
|
@@ -4218,7 +4666,7 @@ class HistoryFiles {
|
|
|
4218
4666
|
let committed;
|
|
4219
4667
|
const fd = await open(path, constants4.O_WRONLY | constants4.O_APPEND | (constants4.O_NOFOLLOW ?? 0));
|
|
4220
4668
|
const assertOriginal = (bytes) => {
|
|
4221
|
-
const original = fstatSync4(fd.fd), selected2 =
|
|
4669
|
+
const original = fstatSync4(fd.fd), selected2 = lstatSync5(path);
|
|
4222
4670
|
if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
|
|
4223
4671
|
corrupt();
|
|
4224
4672
|
const snapshot = this.files.read(path, LIMITS.segmentBytes);
|
|
@@ -4335,7 +4783,7 @@ class HistoryFiles {
|
|
|
4335
4783
|
try {
|
|
4336
4784
|
lease.assert();
|
|
4337
4785
|
this.files.assert(path, before, LIMITS.segmentBytes);
|
|
4338
|
-
const opened = await fd.stat(), selected2 =
|
|
4786
|
+
const opened = await fd.stat(), selected2 = lstatSync5(path);
|
|
4339
4787
|
if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
|
|
4340
4788
|
corrupt();
|
|
4341
4789
|
beforeEffect?.();
|
|
@@ -4370,7 +4818,7 @@ class HistoryFiles {
|
|
|
4370
4818
|
checkpoint(lease, state) {
|
|
4371
4819
|
lease.assert();
|
|
4372
4820
|
this.assertWritable();
|
|
4373
|
-
const path =
|
|
4821
|
+
const path = join5(this.directory, "checkpoint.json"), old = this.files.read(path);
|
|
4374
4822
|
const body = encoded({
|
|
4375
4823
|
schema: "cadence-history-checkpoint/2",
|
|
4376
4824
|
storeId: this.format.storeId,
|
|
@@ -4783,8 +5231,8 @@ function digestHistory(files, exclude) {
|
|
|
4783
5231
|
const walk = (path, depth) => {
|
|
4784
5232
|
if (depth > 8 || !files.files.directory(path))
|
|
4785
5233
|
throw new ContractError("corrupt-selected");
|
|
4786
|
-
const
|
|
4787
|
-
digest.update(`incarnation:${
|
|
5234
|
+
const directory2 = lstatSync6(path);
|
|
5235
|
+
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
4788
5236
|
`);
|
|
4789
5237
|
const dir = opendirSync3(path), names2 = [];
|
|
4790
5238
|
try {
|
|
@@ -4797,11 +5245,11 @@ function digestHistory(files, exclude) {
|
|
|
4797
5245
|
dir.closeSync();
|
|
4798
5246
|
}
|
|
4799
5247
|
for (const name of names2.sort()) {
|
|
4800
|
-
const selected2 =
|
|
5248
|
+
const selected2 = join6(path, name);
|
|
4801
5249
|
if (selected2 === exclude)
|
|
4802
5250
|
continue;
|
|
4803
5251
|
const rel = relative4(files.directory, selected2);
|
|
4804
|
-
if (
|
|
5252
|
+
if (lstatSync6(selected2).isDirectory()) {
|
|
4805
5253
|
digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
|
|
4806
5254
|
`);
|
|
4807
5255
|
walk(selected2, depth + 1);
|
|
@@ -5015,7 +5463,7 @@ async function confirm(expected) {
|
|
|
5015
5463
|
`resolve ${expected.epoch}`
|
|
5016
5464
|
]
|
|
5017
5465
|
];
|
|
5018
|
-
await new Promise((
|
|
5466
|
+
await new Promise((resolve4, reject) => {
|
|
5019
5467
|
let index = 0, line = "", done = false;
|
|
5020
5468
|
const raw = process.stdin.isRaw;
|
|
5021
5469
|
const finish = (ok) => {
|
|
@@ -5031,7 +5479,7 @@ async function confirm(expected) {
|
|
|
5031
5479
|
process.stdout.write(`\x1B[?2004l
|
|
5032
5480
|
`);
|
|
5033
5481
|
if (ok)
|
|
5034
|
-
|
|
5482
|
+
resolve4();
|
|
5035
5483
|
else
|
|
5036
5484
|
reject(new ContractError("forbidden"));
|
|
5037
5485
|
};
|
|
@@ -5107,8 +5555,8 @@ async function localRecovery(c, operation) {
|
|
|
5107
5555
|
}
|
|
5108
5556
|
|
|
5109
5557
|
// src/service/runtime.ts
|
|
5110
|
-
import { homedir as
|
|
5111
|
-
import { dirname as
|
|
5558
|
+
import { homedir as homedir3 } from "node:os";
|
|
5559
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve4 } from "node:path";
|
|
5112
5560
|
|
|
5113
5561
|
// src/service/config.ts
|
|
5114
5562
|
import { isIP } from "node:net";
|
|
@@ -5162,7 +5610,7 @@ function localOrigin(policy, actualPort) {
|
|
|
5162
5610
|
var STARTUP_INPUT_BYTES = 16 * 1024;
|
|
5163
5611
|
function absolute(value) {
|
|
5164
5612
|
const result = text(value, 4096, true);
|
|
5165
|
-
if (!
|
|
5613
|
+
if (!isAbsolute5(result))
|
|
5166
5614
|
throw new ContractError("invalid");
|
|
5167
5615
|
return result;
|
|
5168
5616
|
}
|
|
@@ -5242,7 +5690,7 @@ function selectStart(options, ambient) {
|
|
|
5242
5690
|
throw new ContractError("invalid");
|
|
5243
5691
|
let fromFile;
|
|
5244
5692
|
if (options.config) {
|
|
5245
|
-
const path =
|
|
5693
|
+
const path = resolve4(options.config), f = new PrivateFiles(dirname7(path));
|
|
5246
5694
|
const bytes = f.read(path);
|
|
5247
5695
|
if (!bytes)
|
|
5248
5696
|
throw new ContractError("corrupt-selected");
|
|
@@ -5257,11 +5705,11 @@ function selectStart(options, ambient) {
|
|
|
5257
5705
|
port: options.port ?? 17423,
|
|
5258
5706
|
proxy: null
|
|
5259
5707
|
},
|
|
5260
|
-
agentDir:
|
|
5708
|
+
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join7(ambient.HOME ?? ambient.USERPROFILE ?? homedir3(), ".pi", "agent")),
|
|
5261
5709
|
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
5262
|
-
tokenFile: options.tokenFile ?
|
|
5710
|
+
tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
|
|
5263
5711
|
});
|
|
5264
|
-
if (fromFile && encoded(fromFile) !== encoded(selected2) || options.port !== undefined && options.port !== selected2.http.port || options.host !== undefined && options.host !== selected2.http.host || options.agentDir !== undefined &&
|
|
5712
|
+
if (fromFile && encoded(fromFile) !== encoded(selected2) || options.port !== undefined && options.port !== selected2.http.port || options.host !== undefined && options.host !== selected2.http.host || options.agentDir !== undefined && resolve4(options.agentDir) !== selected2.agentDir || options.tokenFile !== undefined && resolve4(options.tokenFile) !== selected2.tokenFile || env && selected2.tokenSource !== "environment" || selected2.tokenSource === "environment" && !env || resolveWorkspace(selected2.cwd).root !== workspace.root)
|
|
5265
5713
|
throw new ContractError("conflict");
|
|
5266
5714
|
return {
|
|
5267
5715
|
workspace: workspace.root,
|
|
@@ -5293,21 +5741,37 @@ function portValue(value) {
|
|
|
5293
5741
|
throw new ContractError("invalid");
|
|
5294
5742
|
return integer(Number(value), 0, 65535);
|
|
5295
5743
|
}
|
|
5744
|
+
var STARTUP_STAGES = [
|
|
5745
|
+
"package",
|
|
5746
|
+
"pi-agent-directory",
|
|
5747
|
+
"pi-auth",
|
|
5748
|
+
"pi-models",
|
|
5749
|
+
"pi-runtime",
|
|
5750
|
+
"service"
|
|
5751
|
+
];
|
|
5752
|
+
function decodeStartupReport(value) {
|
|
5753
|
+
const o = closed(value, ["status", "error", "stage"]);
|
|
5754
|
+
return {
|
|
5755
|
+
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
5756
|
+
error: oneOf(o.error, ERROR_CODES),
|
|
5757
|
+
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5296
5760
|
|
|
5297
5761
|
// src/service/cli.ts
|
|
5298
5762
|
function nativeCall(c, operation, reveal = false) {
|
|
5299
5763
|
const access = c.localAccess();
|
|
5300
5764
|
const url = new URL(`${access.endpoint}/local/v1/admin/${operation}`);
|
|
5301
|
-
const
|
|
5302
|
-
const body =
|
|
5303
|
-
return new Promise((
|
|
5765
|
+
const write2 = !["status", "diagnose"].includes(operation);
|
|
5766
|
+
const body = write2 ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
|
|
5767
|
+
return new Promise((resolve6, reject) => {
|
|
5304
5768
|
let timer;
|
|
5305
5769
|
const fail = () => {
|
|
5306
5770
|
clearTimeout(timer);
|
|
5307
5771
|
reject(new ContractError("unavailable"));
|
|
5308
5772
|
};
|
|
5309
5773
|
const req = request(url, {
|
|
5310
|
-
method:
|
|
5774
|
+
method: write2 ? "POST" : "GET",
|
|
5311
5775
|
agent: false,
|
|
5312
5776
|
headers: {
|
|
5313
5777
|
"X-Cadence-Local": access.capability,
|
|
@@ -5341,7 +5805,7 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
5341
5805
|
throw new ContractError("stale");
|
|
5342
5806
|
}
|
|
5343
5807
|
clearTimeout(timer);
|
|
5344
|
-
|
|
5808
|
+
resolve6(reply.value);
|
|
5345
5809
|
} catch {
|
|
5346
5810
|
fail();
|
|
5347
5811
|
}
|
|
@@ -5356,7 +5820,7 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
5356
5820
|
req.end(body);
|
|
5357
5821
|
});
|
|
5358
5822
|
}
|
|
5359
|
-
var pause = (ms) => new Promise((
|
|
5823
|
+
var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5360
5824
|
async function serviceStatus(c, diagnose = false) {
|
|
5361
5825
|
const observed = c.inspect();
|
|
5362
5826
|
if (observed.status === "absent" || observed.status === "stopped")
|
|
@@ -5393,7 +5857,38 @@ async function stopService(c) {
|
|
|
5393
5857
|
return { status: "unconfirmed", epoch };
|
|
5394
5858
|
}
|
|
5395
5859
|
function failedStatus(result) {
|
|
5396
|
-
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined"].includes(String(result.status)));
|
|
5860
|
+
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
|
|
5861
|
+
}
|
|
5862
|
+
function startupGuidance(result, input) {
|
|
5863
|
+
let report;
|
|
5864
|
+
try {
|
|
5865
|
+
report = decodeStartupReport(result);
|
|
5866
|
+
} catch {
|
|
5867
|
+
return "";
|
|
5868
|
+
}
|
|
5869
|
+
const agent = input.config.agentDir;
|
|
5870
|
+
const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
|
|
5871
|
+
`;
|
|
5872
|
+
switch (report.stage) {
|
|
5873
|
+
case "package":
|
|
5874
|
+
return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
|
|
5875
|
+
`;
|
|
5876
|
+
case "pi-agent-directory":
|
|
5877
|
+
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
5878
|
+
`;
|
|
5879
|
+
case "pi-auth":
|
|
5880
|
+
return `${join8(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
5881
|
+
`;
|
|
5882
|
+
case "pi-models":
|
|
5883
|
+
return `${join8(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
|
|
5884
|
+
`;
|
|
5885
|
+
case "pi-runtime":
|
|
5886
|
+
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
5887
|
+
${inspect}`;
|
|
5888
|
+
default:
|
|
5889
|
+
return report.status === "quarantined" ? `The owner registered, then refused; it stays quarantined.
|
|
5890
|
+
${inspect}` : "";
|
|
5891
|
+
}
|
|
5397
5892
|
}
|
|
5398
5893
|
async function startService(options, foreground, hostUrl) {
|
|
5399
5894
|
const network = networkSnapshot(process.env);
|
|
@@ -5420,18 +5915,18 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5420
5915
|
stdio: ["pipe", "pipe", "ignore"]
|
|
5421
5916
|
});
|
|
5422
5917
|
let completed = false;
|
|
5423
|
-
const closure = new Promise((
|
|
5918
|
+
const closure = new Promise((resolve6) => child.once("close", (code, signal) => {
|
|
5424
5919
|
completed = true;
|
|
5425
5920
|
if (foreground && (code !== 0 || signal !== null))
|
|
5426
5921
|
process.exitCode = 1;
|
|
5427
|
-
|
|
5922
|
+
resolve6();
|
|
5428
5923
|
}));
|
|
5429
|
-
const startup = new Promise((
|
|
5924
|
+
const startup = new Promise((resolve6) => {
|
|
5430
5925
|
let bytes = "", resolved = false;
|
|
5431
5926
|
const finish = (value) => {
|
|
5432
5927
|
if (!resolved) {
|
|
5433
5928
|
resolved = true;
|
|
5434
|
-
|
|
5929
|
+
resolve6(value);
|
|
5435
5930
|
}
|
|
5436
5931
|
};
|
|
5437
5932
|
child.on("error", () => finish({ status: "unconfirmed" }));
|
|
@@ -5447,7 +5942,12 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5447
5942
|
`))
|
|
5448
5943
|
return;
|
|
5449
5944
|
try {
|
|
5450
|
-
const
|
|
5945
|
+
const line = parseWireJson(bytes, 4096);
|
|
5946
|
+
if (line && typeof line === "object" && "status" in line && line.status !== "ready") {
|
|
5947
|
+
finish(decodeStartupReport(line));
|
|
5948
|
+
return;
|
|
5949
|
+
}
|
|
5950
|
+
const ready = closed(line, ["status", "epoch"]);
|
|
5451
5951
|
if (ready.status !== "ready")
|
|
5452
5952
|
throw new ContractError("unavailable");
|
|
5453
5953
|
const epoch = decodeId(ready.epoch);
|
|
@@ -5466,11 +5966,14 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5466
5966
|
let timer;
|
|
5467
5967
|
const result = await Promise.race([
|
|
5468
5968
|
startup,
|
|
5469
|
-
new Promise((
|
|
5470
|
-
timer = setTimeout(() =>
|
|
5969
|
+
new Promise((resolve6) => {
|
|
5970
|
+
timer = setTimeout(() => resolve6({ status: "unconfirmed" }), LIMITS.startWaitMs);
|
|
5471
5971
|
})
|
|
5472
5972
|
]);
|
|
5473
5973
|
clearTimeout(timer);
|
|
5974
|
+
const guidance = startupGuidance(result, input);
|
|
5975
|
+
if (guidance)
|
|
5976
|
+
process.stderr.write(guidance);
|
|
5474
5977
|
child.stdout.destroy();
|
|
5475
5978
|
child.stdin.destroy();
|
|
5476
5979
|
if (!foreground)
|
|
@@ -5485,6 +5988,127 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5485
5988
|
}
|
|
5486
5989
|
return result;
|
|
5487
5990
|
}
|
|
5991
|
+
async function observeStarted(workspace) {
|
|
5992
|
+
const deadline = performance.now() + LIMITS.startWaitMs + 2000;
|
|
5993
|
+
let service = "unconfirmed";
|
|
5994
|
+
while (performance.now() < deadline) {
|
|
5995
|
+
try {
|
|
5996
|
+
const c = new ServiceCoordination(workspace);
|
|
5997
|
+
const observed = await serviceStatus(c);
|
|
5998
|
+
if (observed && typeof observed === "object" && "status" in observed)
|
|
5999
|
+
service = String(observed.status);
|
|
6000
|
+
if (service === "alive")
|
|
6001
|
+
return { service, endpoint: c.localAccess().endpoint };
|
|
6002
|
+
if (service === "quarantined")
|
|
6003
|
+
break;
|
|
6004
|
+
} catch {}
|
|
6005
|
+
await pause(250);
|
|
6006
|
+
}
|
|
6007
|
+
return { service };
|
|
6008
|
+
}
|
|
6009
|
+
function autostartGuidance(s, result) {
|
|
6010
|
+
const name = s.unit.replace(/\.service$/, "");
|
|
6011
|
+
const cli = JSON.stringify(join8(s.root, "src", "service-cli.mjs"));
|
|
6012
|
+
const workspace = JSON.stringify(result.workspace ?? "");
|
|
6013
|
+
const manage = `Manage: systemctl --user status|restart|stop ${name}; logs: journalctl --user -u ${name}; opt out: systemctl --user disable --now ${name}`;
|
|
6014
|
+
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
6015
|
+
` : "";
|
|
6016
|
+
return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
|
|
6017
|
+
}
|
|
6018
|
+
function lingerGuidance(linger) {
|
|
6019
|
+
switch (linger) {
|
|
6020
|
+
case "enabled":
|
|
6021
|
+
return `Linger enabled: Cadence starts at boot and keeps running after logout (undo: loginctl disable-linger).
|
|
6022
|
+
`;
|
|
6023
|
+
case "off":
|
|
6024
|
+
return `Linger is off: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger
|
|
6025
|
+
`;
|
|
6026
|
+
case "refused":
|
|
6027
|
+
return `Linger needs authorization: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger (may ask for your password)
|
|
6028
|
+
`;
|
|
6029
|
+
default:
|
|
6030
|
+
return "";
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
|
|
6034
|
+
switch (result.status) {
|
|
6035
|
+
case "started": {
|
|
6036
|
+
const linger = lingerGuidance(result.linger);
|
|
6037
|
+
if (result.service !== "alive")
|
|
6038
|
+
return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
|
|
6039
|
+
Inspect: node ${cli} diagnose --workspace ${workspace}
|
|
6040
|
+
${manage}
|
|
6041
|
+
${linger}`;
|
|
6042
|
+
return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
|
|
6043
|
+
Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
|
|
6044
|
+
${manage}
|
|
6045
|
+
${linger}`;
|
|
6046
|
+
}
|
|
6047
|
+
case "waiting-for-pi-login":
|
|
6048
|
+
return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
|
|
6049
|
+
Log in to Pi, then: systemctl --user start ${name}
|
|
6050
|
+
${lingerGuidance(result.linger)}`;
|
|
6051
|
+
case "disabled":
|
|
6052
|
+
return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
|
|
6053
|
+
`;
|
|
6054
|
+
case "masked":
|
|
6055
|
+
return `${s.unit} is masked; Cadence did not enable or start it.
|
|
6056
|
+
`;
|
|
6057
|
+
case "missing-pi-sdk":
|
|
6058
|
+
return `Cadence did not register or refresh ${s.unit}: the Pi SDK packages pi-coding-agent and pi-ai (scope @earendil-works) do not resolve from ${s.root}, for example after installing without optional dependencies; an owner without them would quarantine.
|
|
6059
|
+
`;
|
|
6060
|
+
case "foreign":
|
|
6061
|
+
return `${result.unit} was not generated by Cadence; left unchanged.
|
|
6062
|
+
`;
|
|
6063
|
+
case "unavailable":
|
|
6064
|
+
return `No systemd user manager; start Cadence explicitly: node ${cli} start --workspace /absolute/project
|
|
6065
|
+
`;
|
|
6066
|
+
case "failed":
|
|
6067
|
+
return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
|
|
6068
|
+
${lingerGuidance(result.linger)}`;
|
|
6069
|
+
default:
|
|
6070
|
+
return "";
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
async function autostartMain(argv, hostUrl) {
|
|
6074
|
+
const action = argv.shift();
|
|
6075
|
+
let requested;
|
|
6076
|
+
let linger = false;
|
|
6077
|
+
while (action === "install" && argv.length) {
|
|
6078
|
+
if (argv[0] === "--workspace" && argv[1] && requested === undefined)
|
|
6079
|
+
requested = resolve5(argv.splice(0, 2)[1]);
|
|
6080
|
+
else if (argv[0] === "--linger" && !linger) {
|
|
6081
|
+
argv.shift();
|
|
6082
|
+
linger = true;
|
|
6083
|
+
} else
|
|
6084
|
+
break;
|
|
6085
|
+
}
|
|
6086
|
+
if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
|
|
6087
|
+
throw new ContractError("invalid");
|
|
6088
|
+
const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
|
|
6089
|
+
let result;
|
|
6090
|
+
if (action === "remove")
|
|
6091
|
+
result = removeAutostart(s);
|
|
6092
|
+
else if (action === "status")
|
|
6093
|
+
result = autostartStatus(s);
|
|
6094
|
+
else {
|
|
6095
|
+
try {
|
|
6096
|
+
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested, linger);
|
|
6097
|
+
} catch (error) {
|
|
6098
|
+
if (error instanceof ContractError && error.code === "invalid")
|
|
6099
|
+
process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
|
|
6100
|
+
`);
|
|
6101
|
+
throw error;
|
|
6102
|
+
}
|
|
6103
|
+
if (result.status === "started" && result.workspace)
|
|
6104
|
+
Object.assign(result, await observeStarted(result.workspace));
|
|
6105
|
+
process.stderr.write(autostartGuidance(s, result));
|
|
6106
|
+
}
|
|
6107
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
6108
|
+
`);
|
|
6109
|
+
if (["failed", "foreign", "unavailable"].includes(result.status))
|
|
6110
|
+
process.exitCode = 1;
|
|
6111
|
+
}
|
|
5488
6112
|
async function managementMain(argv, hostUrl) {
|
|
5489
6113
|
try {
|
|
5490
6114
|
if (!process.versions.node || process.versions.bun)
|
|
@@ -5492,12 +6116,17 @@ async function managementMain(argv, hostUrl) {
|
|
|
5492
6116
|
const command = argv.shift();
|
|
5493
6117
|
if (command === "--help" && !argv.length) {
|
|
5494
6118
|
process.stdout.write(`cadence-service start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
5495
|
-
|
|
6119
|
+
cadence-service autostart install [--workspace PATH] [--linger]|refresh|remove|status
|
|
6120
|
+
Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out); its first registration also enables linger for boot start when logind allows it without a password (CADENCE_LINGER=0 opts out), and --linger asks for it explicitly. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
|
|
5496
6121
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
5497
6122
|
Recovery resolve requires this local native terminal, exact unchanged dead-owner resources and separate process/credential/storage acknowledgments. No --yes, network recovery, killing, repair or cleanup.
|
|
5498
6123
|
`);
|
|
5499
6124
|
return;
|
|
5500
6125
|
}
|
|
6126
|
+
if (command === "autostart") {
|
|
6127
|
+
await autostartMain(argv, hostUrl);
|
|
6128
|
+
return;
|
|
6129
|
+
}
|
|
5501
6130
|
let recovery;
|
|
5502
6131
|
if (![
|
|
5503
6132
|
"start",
|
|
@@ -5555,10 +6184,23 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
5555
6184
|
else
|
|
5556
6185
|
throw new ContractError("invalid");
|
|
5557
6186
|
}
|
|
6187
|
+
const landing = effectiveWorkspace(options.workspace, options.stateHome);
|
|
6188
|
+
if (landing.redirected) {
|
|
6189
|
+
if (seen.has("--workspace"))
|
|
6190
|
+
process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
|
|
6191
|
+
`);
|
|
6192
|
+
options.workspace = landing.workspace;
|
|
6193
|
+
if (command === "start")
|
|
6194
|
+
mkdirSync4(landing.workspace, { recursive: true });
|
|
6195
|
+
}
|
|
5558
6196
|
let result;
|
|
5559
6197
|
if (command === "start")
|
|
5560
6198
|
result = await startService(options, foreground, hostUrl);
|
|
5561
|
-
else {
|
|
6199
|
+
else if (landing.redirected && !existsSync(landing.workspace)) {
|
|
6200
|
+
if (command !== "status" && command !== "diagnose")
|
|
6201
|
+
throw new ContractError("unavailable");
|
|
6202
|
+
result = { status: "absent" };
|
|
6203
|
+
} else {
|
|
5562
6204
|
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
5563
6205
|
if (command === "recover" && recovery)
|
|
5564
6206
|
result = await localRecovery(c, recovery);
|
|
@@ -5576,6 +6218,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
5576
6218
|
else
|
|
5577
6219
|
throw new ContractError("invalid");
|
|
5578
6220
|
}
|
|
6221
|
+
if (landing.redirected && result && typeof result === "object")
|
|
6222
|
+
result = { ...result, workspace: landing.workspace };
|
|
5579
6223
|
if (result !== null)
|
|
5580
6224
|
process.stdout.write(`${JSON.stringify(result)}
|
|
5581
6225
|
`);
|