@abelxiaoxing/cadence 2.0.1 → 2.0.2
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 +61 -41
- package/docs/reuse-and-distribution.md +2 -1
- package/docs/service-management.md +50 -3
- package/package.json +7 -7
- package/src/install-config.mjs +25 -0
- package/src/native-client.ts +31 -3
- package/src/operator-cli.mjs +52 -16
- package/src/service/coordination.ts +35 -16
- package/src/service/private-files.ts +128 -23
- package/src/service-cli.mjs +734 -144
- package/src/service-host.mjs +839 -313
- package/src/task-shell.ts +16 -6
- package/src/task-store.ts +104 -25
- package/src/worker-tools.mjs +42 -17
- package/web/dist/assets/cadence.js +1 -1
- package/web/dist/manifest.json +3 -3
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,297 @@ 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
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
function expandHome(value, home) {
|
|
2517
|
+
if (value === "~")
|
|
2518
|
+
return home;
|
|
2519
|
+
return value.startsWith("~/") ? join2(home, value.slice(2)) : value;
|
|
2520
|
+
}
|
|
2521
|
+
function agentDirectory(s) {
|
|
2522
|
+
const selected = s.env.PI_CODING_AGENT_DIR;
|
|
2523
|
+
return resolve2(selected ? expandHome(selected, s.home) : join2(s.home, ".pi", "agent"));
|
|
2524
|
+
}
|
|
2525
|
+
function piUserInstall(s) {
|
|
2526
|
+
try {
|
|
2527
|
+
const managed = join2(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
|
|
2528
|
+
return realpathSync2(managed) === realpathSync2(s.root);
|
|
2529
|
+
} catch {
|
|
2530
|
+
return false;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function sdkPackages(root) {
|
|
2534
|
+
let start;
|
|
2535
|
+
try {
|
|
2536
|
+
start = realpathSync2(join2(root, "src"));
|
|
2537
|
+
} catch {
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const found = [];
|
|
2541
|
+
for (const name of ["pi-coding-agent", "pi-ai"]) {
|
|
2542
|
+
let hit;
|
|
2543
|
+
for (let dir = start;!hit; dir = dirname2(dir)) {
|
|
2544
|
+
const candidate = join2(dir, "node_modules", "@earendil-works", name, "package.json");
|
|
2545
|
+
if (regularFile(candidate))
|
|
2546
|
+
hit = candidate;
|
|
2547
|
+
else if (dirname2(dir) === dir)
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
found.push(hit);
|
|
2551
|
+
}
|
|
2552
|
+
return found;
|
|
2553
|
+
}
|
|
2554
|
+
function unitPath(s) {
|
|
2555
|
+
const data = s.env.XDG_DATA_HOME;
|
|
2556
|
+
return join2(data && isAbsolute2(data) ? data : join2(s.home, ".local", "share"), "systemd", "user", s.unit);
|
|
2557
|
+
}
|
|
2558
|
+
function servicePath(value) {
|
|
2559
|
+
const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute2(entry) && !entry.endsWith(`${sep2}node_modules${sep2}.bin`) && !entry.includes(`${sep2}node-gyp-bin`));
|
|
2560
|
+
return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
|
|
2561
|
+
}
|
|
2562
|
+
function serviceEnvironment(s) {
|
|
2563
|
+
const out = [["PATH", servicePath(s.env.PATH)]];
|
|
2564
|
+
for (const key of ["LANG", "LC_ALL"]) {
|
|
2565
|
+
const value = s.env[key];
|
|
2566
|
+
if (value)
|
|
2567
|
+
out.push([key, value]);
|
|
2568
|
+
}
|
|
2569
|
+
const state = s.env.XDG_STATE_HOME;
|
|
2570
|
+
if (state && isAbsolute2(state))
|
|
2571
|
+
out.push(["XDG_STATE_HOME", state]);
|
|
2572
|
+
if (s.env.PI_CODING_AGENT_DIR)
|
|
2573
|
+
out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
|
|
2574
|
+
return out;
|
|
2575
|
+
}
|
|
2576
|
+
function checked(value) {
|
|
2577
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
2578
|
+
throw new ContractError("invalid");
|
|
2579
|
+
return value;
|
|
2580
|
+
}
|
|
2581
|
+
var specifiers = (value) => checked(value).replaceAll("%", "%%");
|
|
2582
|
+
var quoted = (value) => `"${specifiers(value).replace(/["\\]/g, "\\$&")}"`;
|
|
2583
|
+
var argument = (value) => quoted(value).replaceAll("$", "$$$$");
|
|
2584
|
+
function buildUnit(u) {
|
|
2585
|
+
const command = (args) => [u.node, u.cli, ...args].map(argument).join(" ");
|
|
2586
|
+
return [
|
|
2587
|
+
`${MARKER} on Pi package install/update; upgrades rewrite this file.`,
|
|
2588
|
+
"# Override with `systemctl --user edit cadence`; opt out with `systemctl --user disable --now cadence` or `mask`.",
|
|
2589
|
+
"[Unit]",
|
|
2590
|
+
"Description=Cadence shared task service and WebUI",
|
|
2591
|
+
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
2592
|
+
`ConditionPathExists=${specifiers(u.cli)}`,
|
|
2593
|
+
`ConditionPathExists=${specifiers(join2(u.agentDir, "auth.json"))}`,
|
|
2594
|
+
...u.sdk.map((path) => `ConditionPathExists=${specifiers(path)}`),
|
|
2595
|
+
"StartLimitIntervalSec=300",
|
|
2596
|
+
"StartLimitBurst=3",
|
|
2597
|
+
"",
|
|
2598
|
+
"[Service]",
|
|
2599
|
+
"Type=simple",
|
|
2600
|
+
`WorkingDirectory=${specifiers(u.workspace)}`,
|
|
2601
|
+
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
2602
|
+
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
|
|
2603
|
+
`ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
|
|
2604
|
+
"KillMode=mixed",
|
|
2605
|
+
"TimeoutStopSec=90",
|
|
2606
|
+
"Restart=on-failure",
|
|
2607
|
+
"RestartSec=10",
|
|
2608
|
+
"",
|
|
2609
|
+
"[Install]",
|
|
2610
|
+
"WantedBy=default.target",
|
|
2611
|
+
""
|
|
2612
|
+
].join(`
|
|
2613
|
+
`);
|
|
2614
|
+
}
|
|
2615
|
+
function readGenerated(path) {
|
|
2616
|
+
try {
|
|
2617
|
+
if (!lstatSync2(path).isFile())
|
|
2618
|
+
return null;
|
|
2619
|
+
const text2 = readFileSync(path, "utf8");
|
|
2620
|
+
return text2.startsWith(MARKER) ? text2 : null;
|
|
2621
|
+
} catch (error) {
|
|
2622
|
+
if (error.code === "ENOENT")
|
|
2623
|
+
return;
|
|
2624
|
+
throw error;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
function generatedWorkspace(text2) {
|
|
2628
|
+
return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
|
|
2629
|
+
}
|
|
2630
|
+
function directory(path) {
|
|
2631
|
+
try {
|
|
2632
|
+
return isAbsolute2(path) && statSync(path).isDirectory();
|
|
2633
|
+
} catch {
|
|
2634
|
+
return false;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
function unitStateHome(s) {
|
|
2638
|
+
const state = s.env.XDG_STATE_HOME;
|
|
2639
|
+
return state && isAbsolute2(state) ? state : join2(s.home, ".local", "state");
|
|
2640
|
+
}
|
|
2641
|
+
function selectWorkspace(s, requested, previous) {
|
|
2642
|
+
let chosen;
|
|
2643
|
+
if (requested) {
|
|
2644
|
+
chosen = expandHome(requested, s.home);
|
|
2645
|
+
if (!directory(chosen))
|
|
2646
|
+
throw new ContractError("invalid");
|
|
2647
|
+
chosen = resolve2(chosen);
|
|
2648
|
+
} else {
|
|
2649
|
+
const kept = generatedWorkspace(previous);
|
|
2650
|
+
if (kept && directory(kept))
|
|
2651
|
+
chosen = kept;
|
|
2652
|
+
}
|
|
2653
|
+
const landing = chosen ? effectiveWorkspace(chosen, unitStateHome(s), s.home) : { workspace: join2(s.home, LANDING_WORKSPACE), redirected: false };
|
|
2654
|
+
if (landing.workspace !== chosen)
|
|
2655
|
+
mkdirSync2(landing.workspace, { recursive: true });
|
|
2656
|
+
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
2657
|
+
}
|
|
2658
|
+
function write(path, content) {
|
|
2659
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2660
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
2661
|
+
try {
|
|
2662
|
+
writeFileSync2(temporary, content, { mode: 420, flag: "wx" });
|
|
2663
|
+
renameSync2(temporary, path);
|
|
2664
|
+
} finally {
|
|
2665
|
+
rmSync(temporary, { force: true });
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function regularFile(path) {
|
|
2669
|
+
try {
|
|
2670
|
+
return statSync(path).isFile();
|
|
2671
|
+
} catch {
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
function configureAutostart(s, mode, requested) {
|
|
2676
|
+
if (s.platform !== "linux")
|
|
2677
|
+
return { status: "skipped" };
|
|
2678
|
+
if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
|
|
2679
|
+
return { status: "skipped" };
|
|
2680
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2681
|
+
return { status: "unavailable" };
|
|
2682
|
+
const path = unitPath(s);
|
|
2683
|
+
const previous = readGenerated(path);
|
|
2684
|
+
if (previous === null)
|
|
2685
|
+
return { status: "foreign", unit: path };
|
|
2686
|
+
const sdk = sdkPackages(s.root);
|
|
2687
|
+
if (!sdk)
|
|
2688
|
+
return { status: "missing-pi-sdk" };
|
|
2689
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested ?? s.env.CADENCE_WORKSPACE, previous);
|
|
2690
|
+
const agentDir = agentDirectory(s);
|
|
2691
|
+
const content = buildUnit({
|
|
2692
|
+
node: s.node,
|
|
2693
|
+
cli: join2(s.root, "src", "service-cli.mjs"),
|
|
2694
|
+
workspace,
|
|
2695
|
+
agentDir,
|
|
2696
|
+
sdk,
|
|
2697
|
+
environment: serviceEnvironment(s)
|
|
2698
|
+
});
|
|
2699
|
+
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
|
|
2700
|
+
const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
|
|
2701
|
+
if (active)
|
|
2702
|
+
s.systemctl(["stop", s.unit]);
|
|
2703
|
+
write(path, content);
|
|
2704
|
+
s.systemctl(["daemon-reload"]);
|
|
2705
|
+
const result = {
|
|
2706
|
+
unit: path,
|
|
2707
|
+
workspace,
|
|
2708
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
2709
|
+
};
|
|
2710
|
+
if (enabled.startsWith("masked"))
|
|
2711
|
+
return { status: "masked", ...result };
|
|
2712
|
+
const wanted = mode === "install" || previous === undefined || enabled === "enabled";
|
|
2713
|
+
if (wanted && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
2714
|
+
return { status: "failed", ...result };
|
|
2715
|
+
if (!wanted && !active)
|
|
2716
|
+
return { status: "disabled", ...result };
|
|
2717
|
+
if (!regularFile(join2(agentDir, "auth.json")))
|
|
2718
|
+
return { status: "waiting-for-pi-login", ...result };
|
|
2719
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
2720
|
+
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
2721
|
+
return { status: started ? "started" : "failed", ...result };
|
|
2722
|
+
}
|
|
2723
|
+
function removeAutostart(s) {
|
|
2724
|
+
if (s.platform !== "linux")
|
|
2725
|
+
return { status: "skipped" };
|
|
2726
|
+
const path = unitPath(s);
|
|
2727
|
+
const previous = readGenerated(path);
|
|
2728
|
+
if (previous === null)
|
|
2729
|
+
return { status: "foreign", unit: path };
|
|
2730
|
+
if (previous === undefined)
|
|
2731
|
+
return { status: "absent" };
|
|
2732
|
+
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2733
|
+
return { status: "unavailable", unit: path };
|
|
2734
|
+
if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
|
|
2735
|
+
return { status: "failed", unit: path };
|
|
2736
|
+
rmSync(path);
|
|
2737
|
+
s.systemctl(["daemon-reload"]);
|
|
2738
|
+
s.systemctl(["reset-failed", s.unit]);
|
|
2739
|
+
return { status: "removed", unit: path };
|
|
2740
|
+
}
|
|
2741
|
+
function autostartStatus(s) {
|
|
2742
|
+
if (s.platform !== "linux")
|
|
2743
|
+
return { status: "skipped" };
|
|
2744
|
+
const path = unitPath(s);
|
|
2745
|
+
const previous = readGenerated(path);
|
|
2746
|
+
if (previous === null)
|
|
2747
|
+
return { status: "foreign", unit: path };
|
|
2748
|
+
if (previous === undefined)
|
|
2749
|
+
return { status: "absent" };
|
|
2750
|
+
return {
|
|
2751
|
+
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
2752
|
+
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
2753
|
+
unit: path,
|
|
2754
|
+
workspace: generatedWorkspace(previous)
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// src/service/coordination.ts
|
|
2759
|
+
import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
2760
|
+
import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
2761
|
+
import { hostname as hostname2 } from "node:os";
|
|
2762
|
+
import { dirname as dirname4, isAbsolute as isAbsolute4, join as join4, relative as relative3, sep as sep4 } from "node:path";
|
|
2763
|
+
|
|
2439
2764
|
// src/service/private-files.ts
|
|
2440
2765
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
2441
2766
|
import {
|
|
@@ -2443,13 +2768,15 @@ import {
|
|
|
2443
2768
|
constants as constants2,
|
|
2444
2769
|
fstatSync as fstatSync2,
|
|
2445
2770
|
fsyncSync as fsyncSync2,
|
|
2446
|
-
lstatSync as
|
|
2771
|
+
lstatSync as lstatSync3,
|
|
2772
|
+
mkdirSync as mkdirSync3,
|
|
2447
2773
|
openSync as openSync2,
|
|
2448
|
-
|
|
2774
|
+
readSync as readSync2,
|
|
2775
|
+
renameSync as renameSync3,
|
|
2449
2776
|
unlinkSync as unlinkSync2,
|
|
2450
|
-
writeFileSync as
|
|
2777
|
+
writeFileSync as writeFileSync3
|
|
2451
2778
|
} from "node:fs";
|
|
2452
|
-
import { dirname as
|
|
2779
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
2453
2780
|
var METADATA_BYTES = 16 * 1024;
|
|
2454
2781
|
var REGISTRY_BYTES = 1024 * 1024;
|
|
2455
2782
|
var REGISTRY_ENTRIES = 128;
|
|
@@ -2469,42 +2796,112 @@ function same(a, b) {
|
|
|
2469
2796
|
function missing(error) {
|
|
2470
2797
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2471
2798
|
}
|
|
2799
|
+
var job = 0;
|
|
2800
|
+
var advancing = false;
|
|
2801
|
+
function currentJob() {
|
|
2802
|
+
if (!advancing) {
|
|
2803
|
+
advancing = true;
|
|
2804
|
+
queueMicrotask(() => {
|
|
2805
|
+
job++;
|
|
2806
|
+
advancing = false;
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
return job;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
class DirectoryWalks {
|
|
2813
|
+
job = currentJob();
|
|
2814
|
+
walked = new Map;
|
|
2815
|
+
open() {
|
|
2816
|
+
if (this.job === job)
|
|
2817
|
+
return true;
|
|
2818
|
+
this.walked.clear();
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
remember(path, st) {
|
|
2822
|
+
if (this.open())
|
|
2823
|
+
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2824
|
+
}
|
|
2825
|
+
reuse(path) {
|
|
2826
|
+
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2827
|
+
if (!walked)
|
|
2828
|
+
return false;
|
|
2829
|
+
let st;
|
|
2830
|
+
try {
|
|
2831
|
+
st = lstatSync3(path);
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
if (!missing(error))
|
|
2834
|
+
throw error;
|
|
2835
|
+
this.walked.delete(path);
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
|
|
2839
|
+
this.walked.delete(path);
|
|
2840
|
+
return false;
|
|
2841
|
+
}
|
|
2842
|
+
privateMode(st);
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2472
2846
|
|
|
2473
2847
|
class PrivateFiles {
|
|
2474
2848
|
namespace;
|
|
2475
2849
|
constructor(namespace) {
|
|
2476
2850
|
this.namespace = namespace;
|
|
2477
2851
|
}
|
|
2478
|
-
directory(path, create = false) {
|
|
2852
|
+
directory(path, create = false, walks) {
|
|
2479
2853
|
const tail = relative2(this.namespace, path);
|
|
2480
|
-
if (
|
|
2854
|
+
if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
|
|
2481
2855
|
refused();
|
|
2482
2856
|
if (!create) {
|
|
2483
|
-
const
|
|
2484
|
-
|
|
2485
|
-
|
|
2857
|
+
const full = resolve3(path);
|
|
2858
|
+
if (walks?.reuse(full))
|
|
2859
|
+
return true;
|
|
2860
|
+
const namespace = resolve3(this.namespace);
|
|
2861
|
+
let inside = false, last;
|
|
2862
|
+
const found = safeDirectory(path, false, (at2, st) => {
|
|
2486
2863
|
if (at2 === namespace)
|
|
2487
2864
|
inside = true;
|
|
2488
2865
|
if (inside)
|
|
2489
2866
|
privateMode(st);
|
|
2867
|
+
last = st;
|
|
2490
2868
|
});
|
|
2869
|
+
if (found && last)
|
|
2870
|
+
walks?.remember(full, last);
|
|
2871
|
+
return found;
|
|
2491
2872
|
}
|
|
2873
|
+
if (!safeDirectory(this.namespace, true))
|
|
2874
|
+
return false;
|
|
2875
|
+
privateMode(lstatSync3(this.namespace));
|
|
2492
2876
|
let at = this.namespace;
|
|
2493
|
-
for (const part of
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2877
|
+
for (const part of tail.split(sep3).filter(Boolean)) {
|
|
2878
|
+
at = join3(at, part);
|
|
2879
|
+
let st;
|
|
2880
|
+
try {
|
|
2881
|
+
st = lstatSync3(at);
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
if (!missing(error))
|
|
2884
|
+
throw error;
|
|
2885
|
+
try {
|
|
2886
|
+
mkdirSync3(at, { mode: 448 });
|
|
2887
|
+
} catch (mkdirError) {
|
|
2888
|
+
if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
|
|
2889
|
+
throw mkdirError;
|
|
2890
|
+
}
|
|
2891
|
+
st = lstatSync3(at);
|
|
2892
|
+
}
|
|
2893
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2894
|
+
throw new Error("Unsafe directory");
|
|
2895
|
+
privateMode(st);
|
|
2499
2896
|
}
|
|
2500
2897
|
return true;
|
|
2501
2898
|
}
|
|
2502
|
-
read(path, maximum = METADATA_BYTES) {
|
|
2503
|
-
if (!this.directory(
|
|
2899
|
+
read(path, maximum = METADATA_BYTES, walks) {
|
|
2900
|
+
if (!this.directory(dirname3(path), false, walks))
|
|
2504
2901
|
return;
|
|
2505
2902
|
let before;
|
|
2506
2903
|
try {
|
|
2507
|
-
before =
|
|
2904
|
+
before = lstatSync3(path);
|
|
2508
2905
|
} catch (error) {
|
|
2509
2906
|
if (missing(error))
|
|
2510
2907
|
return;
|
|
@@ -2518,31 +2915,39 @@ class PrivateFiles {
|
|
|
2518
2915
|
const opened = fstatSync2(fd);
|
|
2519
2916
|
if (!same(before, opened))
|
|
2520
2917
|
refused();
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2918
|
+
const buffer = Buffer.alloc(opened.size + 1);
|
|
2919
|
+
let size = 0;
|
|
2920
|
+
while (size < buffer.length) {
|
|
2921
|
+
const n = readSync2(fd, buffer, size, buffer.length - size, null);
|
|
2922
|
+
if (!n)
|
|
2923
|
+
break;
|
|
2924
|
+
size += n;
|
|
2925
|
+
}
|
|
2926
|
+
const bytes = buffer.subarray(0, size);
|
|
2927
|
+
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path)))
|
|
2523
2928
|
refused();
|
|
2524
2929
|
return { bytes, identity: hash(bytes), stamp: stamp(opened) };
|
|
2525
2930
|
} finally {
|
|
2526
2931
|
closeSync2(fd);
|
|
2527
2932
|
}
|
|
2528
2933
|
}
|
|
2529
|
-
assert(path, expected, maximum = METADATA_BYTES) {
|
|
2530
|
-
const current = this.read(path, maximum);
|
|
2934
|
+
assert(path, expected, maximum = METADATA_BYTES, walks) {
|
|
2935
|
+
const current = this.read(path, maximum, walks);
|
|
2531
2936
|
if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
|
|
2532
2937
|
throw new ContractError("conflict");
|
|
2533
2938
|
}
|
|
2534
2939
|
create(path, bytes, maximum = METADATA_BYTES) {
|
|
2535
2940
|
if (Buffer.byteLength(bytes) > maximum)
|
|
2536
2941
|
throw new ContractError("oversize");
|
|
2537
|
-
this.directory(
|
|
2538
|
-
const fd = openSync2(path, "wx", 384);
|
|
2942
|
+
this.directory(dirname3(path), true);
|
|
2943
|
+
const fd = retryWindowsFileOperation(() => openSync2(path, "wx", 384));
|
|
2539
2944
|
try {
|
|
2540
|
-
|
|
2945
|
+
writeFileSync3(fd, bytes);
|
|
2541
2946
|
fsyncSync2(fd);
|
|
2542
2947
|
} finally {
|
|
2543
2948
|
closeSync2(fd);
|
|
2544
2949
|
}
|
|
2545
|
-
this.syncDirectory(
|
|
2950
|
+
this.syncDirectory(dirname3(path));
|
|
2546
2951
|
const result = this.read(path, maximum);
|
|
2547
2952
|
if (!result || result.identity !== hash(bytes))
|
|
2548
2953
|
refused();
|
|
@@ -2550,11 +2955,11 @@ class PrivateFiles {
|
|
|
2550
2955
|
}
|
|
2551
2956
|
replace(path, expected, bytes, maximum = METADATA_BYTES) {
|
|
2552
2957
|
this.assert(path, expected, maximum);
|
|
2553
|
-
const temporary =
|
|
2958
|
+
const temporary = join3(dirname3(path), `pending-${randomUUID2()}.json`);
|
|
2554
2959
|
this.create(temporary, bytes, maximum);
|
|
2555
2960
|
this.assert(path, expected, maximum);
|
|
2556
|
-
|
|
2557
|
-
this.syncDirectory(
|
|
2961
|
+
retryWindowsFileOperation(() => renameSync3(temporary, path), () => this.assert(path, expected, maximum));
|
|
2962
|
+
this.syncDirectory(dirname3(path));
|
|
2558
2963
|
const result = this.read(path, maximum);
|
|
2559
2964
|
if (!result || result.identity !== hash(bytes))
|
|
2560
2965
|
refused();
|
|
@@ -2562,8 +2967,8 @@ class PrivateFiles {
|
|
|
2562
2967
|
}
|
|
2563
2968
|
remove(path, expected, maximum = METADATA_BYTES) {
|
|
2564
2969
|
this.assert(path, expected, maximum);
|
|
2565
|
-
unlinkSync2(path);
|
|
2566
|
-
this.syncDirectory(
|
|
2970
|
+
retryWindowsFileOperation(() => unlinkSync2(path), () => this.assert(path, expected, maximum));
|
|
2971
|
+
this.syncDirectory(dirname3(path));
|
|
2567
2972
|
}
|
|
2568
2973
|
syncDirectory(path) {
|
|
2569
2974
|
if (process.platform === "win32")
|
|
@@ -2617,7 +3022,7 @@ var registrationKeys = [
|
|
|
2617
3022
|
var reservationKeys = registrationKeys.filter((k) => !["schema", "phase", "checkoutToken"].includes(k));
|
|
2618
3023
|
function pathValue(value) {
|
|
2619
3024
|
const path = text(value, 4096, true);
|
|
2620
|
-
if (!
|
|
3025
|
+
if (!isAbsolute4(path))
|
|
2621
3026
|
refused();
|
|
2622
3027
|
return path;
|
|
2623
3028
|
}
|
|
@@ -2674,7 +3079,7 @@ function decodeRegistration(value) {
|
|
|
2674
3079
|
function rootsOverlap(a, b) {
|
|
2675
3080
|
const within = (x, y) => {
|
|
2676
3081
|
const r = relative3(x, y);
|
|
2677
|
-
return r === "" || r !== ".." && !r.startsWith(`..${
|
|
3082
|
+
return r === "" || r !== ".." && !r.startsWith(`..${sep4}`) && !isAbsolute4(r);
|
|
2678
3083
|
};
|
|
2679
3084
|
return within(a, b) || within(b, a);
|
|
2680
3085
|
}
|
|
@@ -2726,17 +3131,17 @@ class ServiceCoordination {
|
|
|
2726
3131
|
stateHome;
|
|
2727
3132
|
constructor(workspace, stateHome) {
|
|
2728
3133
|
this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
2729
|
-
const namespace =
|
|
3134
|
+
const namespace = dirname4(this.store.privateRoot);
|
|
2730
3135
|
this.files = new PrivateFiles(namespace);
|
|
2731
|
-
this.stateHome =
|
|
2732
|
-
this.directory =
|
|
2733
|
-
this.registryDirectory =
|
|
3136
|
+
this.stateHome = dirname4(namespace);
|
|
3137
|
+
this.directory = join4(this.store.privateRoot, "service-v1");
|
|
3138
|
+
this.registryDirectory = join4(namespace, "service-registry-v1");
|
|
2734
3139
|
}
|
|
2735
3140
|
path(name) {
|
|
2736
|
-
return
|
|
3141
|
+
return join4(this.directory, name);
|
|
2737
3142
|
}
|
|
2738
3143
|
registryPath(name) {
|
|
2739
|
-
return
|
|
3144
|
+
return join4(this.registryDirectory, name);
|
|
2740
3145
|
}
|
|
2741
3146
|
registry() {
|
|
2742
3147
|
if (!this.files.directory(this.registryDirectory))
|
|
@@ -2756,7 +3161,7 @@ class ServiceCoordination {
|
|
|
2756
3161
|
const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
|
|
2757
3162
|
const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
|
|
2758
3163
|
for (const [i, row] of entries.entries()) {
|
|
2759
|
-
if (row.stateHome !== this.stateHome ||
|
|
3164
|
+
if (row.stateHome !== this.stateHome || realpathSync3(row.root) !== row.root)
|
|
2760
3165
|
refused();
|
|
2761
3166
|
if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
|
|
2762
3167
|
refused();
|
|
@@ -2770,8 +3175,8 @@ class ServiceCoordination {
|
|
|
2770
3175
|
}
|
|
2771
3176
|
};
|
|
2772
3177
|
}
|
|
2773
|
-
assertNamespace() {
|
|
2774
|
-
if (!this.files.directory(this.directory))
|
|
3178
|
+
assertNamespace(walks) {
|
|
3179
|
+
if (!this.files.directory(this.directory, false, walks))
|
|
2775
3180
|
refused();
|
|
2776
3181
|
names(this.directory, [
|
|
2777
3182
|
"format.json",
|
|
@@ -2784,14 +3189,15 @@ class ServiceCoordination {
|
|
|
2784
3189
|
"host-config.json",
|
|
2785
3190
|
"history-v2"
|
|
2786
3191
|
]);
|
|
2787
|
-
this.files.directory(
|
|
3192
|
+
this.files.directory(join4(this.directory, "history-v2"), false, walks);
|
|
2788
3193
|
}
|
|
2789
3194
|
selected() {
|
|
2790
|
-
|
|
3195
|
+
const walks = new DirectoryWalks;
|
|
3196
|
+
if (!this.files.directory(this.directory, false, walks))
|
|
2791
3197
|
return;
|
|
2792
|
-
this.assertNamespace();
|
|
2793
|
-
const manifest = this.files.read(this.path("format.json"));
|
|
2794
|
-
const config = this.files.read(this.path("config.json"));
|
|
3198
|
+
this.assertNamespace(walks);
|
|
3199
|
+
const manifest = this.files.read(this.path("format.json"), undefined, walks);
|
|
3200
|
+
const config = this.files.read(this.path("config.json"), undefined, walks);
|
|
2795
3201
|
if (!manifest || !config)
|
|
2796
3202
|
refused();
|
|
2797
3203
|
const f = closed(json(manifest), [
|
|
@@ -2805,13 +3211,13 @@ class ServiceCoordination {
|
|
|
2805
3211
|
literal(f.stateHome, this.stateHome);
|
|
2806
3212
|
literal(f.configIdentity, config.identity);
|
|
2807
3213
|
const c = decodeOwnerConfig(json(config));
|
|
2808
|
-
const hostConfig = this.files.read(this.path("host-config.json"));
|
|
3214
|
+
const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
|
|
2809
3215
|
if (hostConfig && hostConfig.identity !== c.policyIdentity)
|
|
2810
3216
|
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);
|
|
3217
|
+
const registration = this.files.read(this.path("registration.json"), undefined, walks);
|
|
3218
|
+
const owner = this.files.read(this.path("owner.lock"), undefined, walks);
|
|
3219
|
+
const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
|
|
3220
|
+
const checkout = this.files.read(this.store.lockPath, 8192, walks);
|
|
2815
3221
|
const value = registration ? decodeRegistration(json(registration)) : undefined;
|
|
2816
3222
|
if (value && (value.root !== this.store.root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
|
|
2817
3223
|
refused();
|
|
@@ -3158,8 +3564,8 @@ class OwnerLease {
|
|
|
3158
3564
|
assert() {
|
|
3159
3565
|
if (this.released || this.selected.value.pid !== process.pid || this.selected.value.host !== hostname2())
|
|
3160
3566
|
throw new ContractError("conflict");
|
|
3161
|
-
const c = this.coordination;
|
|
3162
|
-
c.assertNamespace();
|
|
3567
|
+
const c = this.coordination, walks = new DirectoryWalks;
|
|
3568
|
+
c.assertNamespace(walks);
|
|
3163
3569
|
for (const [name, snapshot] of [
|
|
3164
3570
|
["config.json", this.selected.config],
|
|
3165
3571
|
["host-config.json", this.selected.hostConfig],
|
|
@@ -3168,8 +3574,8 @@ class OwnerLease {
|
|
|
3168
3574
|
["owner.lock", this.selected.owner],
|
|
3169
3575
|
["local-capability.json", this.selected.capability]
|
|
3170
3576
|
])
|
|
3171
|
-
c.files.assert(c.path(name), snapshot);
|
|
3172
|
-
c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
|
|
3577
|
+
c.files.assert(c.path(name), snapshot, undefined, walks);
|
|
3578
|
+
c.files.assert(c.store.lockPath, this.selected.checkout, 8192, walks);
|
|
3173
3579
|
c.store.assertOwner(this.checkout);
|
|
3174
3580
|
}
|
|
3175
3581
|
phase(phase) {
|
|
@@ -3208,8 +3614,8 @@ import { isatty } from "node:tty";
|
|
|
3208
3614
|
|
|
3209
3615
|
// src/history/recovery.ts
|
|
3210
3616
|
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
3211
|
-
import { lstatSync as
|
|
3212
|
-
import { join as
|
|
3617
|
+
import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
|
|
3618
|
+
import { join as join6, relative as relative4 } from "node:path";
|
|
3213
3619
|
|
|
3214
3620
|
// src/service/recovery.ts
|
|
3215
3621
|
async function inspectRecovery(coordination, history) {
|
|
@@ -3263,13 +3669,13 @@ import {
|
|
|
3263
3669
|
constants as constants4,
|
|
3264
3670
|
fstatSync as fstatSync4,
|
|
3265
3671
|
fsyncSync as fsyncSync3,
|
|
3266
|
-
lstatSync as
|
|
3672
|
+
lstatSync as lstatSync5,
|
|
3267
3673
|
opendirSync as opendirSync2,
|
|
3268
3674
|
openSync as openSync4,
|
|
3269
3675
|
statfsSync
|
|
3270
3676
|
} from "node:fs";
|
|
3271
3677
|
import { open } from "node:fs/promises";
|
|
3272
|
-
import { dirname as
|
|
3678
|
+
import { dirname as dirname6, join as join5 } from "node:path";
|
|
3273
3679
|
|
|
3274
3680
|
// src/history/admission.ts
|
|
3275
3681
|
function decodeRunAdmission(value) {
|
|
@@ -3293,19 +3699,19 @@ import {
|
|
|
3293
3699
|
closeSync as closeSync3,
|
|
3294
3700
|
constants as constants3,
|
|
3295
3701
|
fstatSync as fstatSync3,
|
|
3296
|
-
lstatSync as
|
|
3702
|
+
lstatSync as lstatSync4,
|
|
3297
3703
|
openSync as openSync3,
|
|
3298
|
-
readSync as
|
|
3704
|
+
readSync as readSync3
|
|
3299
3705
|
} from "node:fs";
|
|
3300
|
-
import { dirname as
|
|
3706
|
+
import { dirname as dirname5 } from "node:path";
|
|
3301
3707
|
function bad() {
|
|
3302
3708
|
throw new ContractError("corrupt-selected");
|
|
3303
3709
|
}
|
|
3304
3710
|
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(
|
|
3711
|
+
function selected(files, path, read, walks) {
|
|
3712
|
+
if (!files.files.directory(dirname5(path), false, walks))
|
|
3307
3713
|
bad();
|
|
3308
|
-
const before =
|
|
3714
|
+
const before = lstatSync4(path);
|
|
3309
3715
|
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
3310
3716
|
bad();
|
|
3311
3717
|
const fd = openSync3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
|
|
@@ -3313,7 +3719,7 @@ function selected(files, path, read) {
|
|
|
3313
3719
|
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
3314
3720
|
bad();
|
|
3315
3721
|
const result = read(fd, before.size, stamp2(before));
|
|
3316
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(
|
|
3722
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path)) !== stamp2(before))
|
|
3317
3723
|
bad();
|
|
3318
3724
|
return result;
|
|
3319
3725
|
} finally {
|
|
@@ -3764,13 +4170,24 @@ function decodeJournalText(line) {
|
|
|
3764
4170
|
|
|
3765
4171
|
// src/history/files.ts
|
|
3766
4172
|
class HistoryReadBudget {
|
|
4173
|
+
clock;
|
|
3767
4174
|
started = performance.now();
|
|
3768
4175
|
bytes = 0;
|
|
4176
|
+
walks = new DirectoryWalks;
|
|
4177
|
+
constructor(clock = true) {
|
|
4178
|
+
this.clock = clock;
|
|
4179
|
+
}
|
|
4180
|
+
static authority() {
|
|
4181
|
+
return new HistoryReadBudget(false);
|
|
4182
|
+
}
|
|
3769
4183
|
reserve(bytes) {
|
|
3770
|
-
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes ||
|
|
4184
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
|
|
3771
4185
|
throw new ContractError("receipt-inconclusive");
|
|
3772
4186
|
this.bytes += bytes;
|
|
3773
4187
|
}
|
|
4188
|
+
get elapsed() {
|
|
4189
|
+
return this.clock && performance.now() - this.started >= LIMITS.searchMs;
|
|
4190
|
+
}
|
|
3774
4191
|
}
|
|
3775
4192
|
function corrupt() {
|
|
3776
4193
|
throw new ContractError("corrupt-selected");
|
|
@@ -3789,14 +4206,14 @@ class HistoryFiles {
|
|
|
3789
4206
|
this.coordination = coordination;
|
|
3790
4207
|
this.format = format;
|
|
3791
4208
|
this.formatSnapshot = formatSnapshot;
|
|
3792
|
-
this.directory =
|
|
4209
|
+
this.directory = join5(coordination.directory, "history-v2");
|
|
3793
4210
|
this.files = coordination.files;
|
|
3794
4211
|
}
|
|
3795
4212
|
static open(coordination) {
|
|
3796
|
-
const dir =
|
|
4213
|
+
const dir = join5(coordination.directory, "history-v2");
|
|
3797
4214
|
if (!coordination.files.directory(dir))
|
|
3798
4215
|
return;
|
|
3799
|
-
const file = coordination.files.read(
|
|
4216
|
+
const file = coordination.files.read(join5(dir, "format.json"));
|
|
3800
4217
|
if (!file)
|
|
3801
4218
|
corrupt();
|
|
3802
4219
|
const format = decodeFormat(json(file));
|
|
@@ -3812,7 +4229,7 @@ class HistoryFiles {
|
|
|
3812
4229
|
existing.assertWritable();
|
|
3813
4230
|
return existing;
|
|
3814
4231
|
}
|
|
3815
|
-
const dir =
|
|
4232
|
+
const dir = join5(c.directory, "history-v2");
|
|
3816
4233
|
const format = {
|
|
3817
4234
|
schema: FORMAT,
|
|
3818
4235
|
storeId: randomUUID4(),
|
|
@@ -3822,19 +4239,19 @@ class HistoryFiles {
|
|
|
3822
4239
|
c.files.directory(dir, true);
|
|
3823
4240
|
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
3824
4241
|
lease.assert();
|
|
3825
|
-
c.files.directory(
|
|
4242
|
+
c.files.directory(join5(dir, name), true);
|
|
3826
4243
|
}
|
|
3827
4244
|
lease.assert();
|
|
3828
|
-
const keyPath =
|
|
4245
|
+
const keyPath = join5(dir, "read-proof-key.json");
|
|
3829
4246
|
c.files.create(keyPath, encoded({
|
|
3830
4247
|
schema: "cadence-history-read-key/1",
|
|
3831
4248
|
storeId: format.storeId,
|
|
3832
4249
|
key: randomBytes2(32).toString("base64url")
|
|
3833
4250
|
}));
|
|
3834
4251
|
lease.assert();
|
|
3835
|
-
const snapshot = c.files.create(
|
|
4252
|
+
const snapshot = c.files.create(join5(dir, "format.json"), encoded(format));
|
|
3836
4253
|
const result = new HistoryFiles(c, format, snapshot);
|
|
3837
|
-
result.syncParents(
|
|
4254
|
+
result.syncParents(join5(dir, "format.json"));
|
|
3838
4255
|
result.assertWritable();
|
|
3839
4256
|
return result;
|
|
3840
4257
|
}
|
|
@@ -3869,7 +4286,7 @@ class HistoryFiles {
|
|
|
3869
4286
|
throw new ContractError("quarantine");
|
|
3870
4287
|
}
|
|
3871
4288
|
assertFormat() {
|
|
3872
|
-
this.files.assert(
|
|
4289
|
+
this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot);
|
|
3873
4290
|
}
|
|
3874
4291
|
readProofMaterial(budget) {
|
|
3875
4292
|
if (this.format.schema !== FORMAT)
|
|
@@ -3878,7 +4295,7 @@ class HistoryFiles {
|
|
|
3878
4295
|
this.assertReadFormat(budget);
|
|
3879
4296
|
else
|
|
3880
4297
|
this.assertFormat();
|
|
3881
|
-
const selected2 = this.readBudgeted(
|
|
4298
|
+
const selected2 = this.readBudgeted(join5(this.directory, "read-proof-key.json"), 512, budget);
|
|
3882
4299
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
3883
4300
|
corrupt();
|
|
3884
4301
|
try {
|
|
@@ -3906,17 +4323,17 @@ class HistoryFiles {
|
|
|
3906
4323
|
return;
|
|
3907
4324
|
}
|
|
3908
4325
|
this.assertFormat();
|
|
3909
|
-
assertIncarnation(this,
|
|
4326
|
+
assertIncarnation(this, join5(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
3910
4327
|
}
|
|
3911
4328
|
segmentPath(segment) {
|
|
3912
|
-
return
|
|
4329
|
+
return join5(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
3913
4330
|
}
|
|
3914
4331
|
artifactPath(ref) {
|
|
3915
|
-
return
|
|
4332
|
+
return join5(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
3916
4333
|
}
|
|
3917
4334
|
admissionPath(runId) {
|
|
3918
4335
|
decodeId(runId);
|
|
3919
|
-
return
|
|
4336
|
+
return join5(this.directory, "runs", runId, "admission.json");
|
|
3920
4337
|
}
|
|
3921
4338
|
writeAdmission(lease, input) {
|
|
3922
4339
|
lease.assert();
|
|
@@ -3930,19 +4347,19 @@ class HistoryFiles {
|
|
|
3930
4347
|
return this.files.read(path, maximum);
|
|
3931
4348
|
let size;
|
|
3932
4349
|
try {
|
|
3933
|
-
size =
|
|
4350
|
+
size = lstatSync5(path).size;
|
|
3934
4351
|
} catch {
|
|
3935
|
-
return this.files.read(path, 0);
|
|
4352
|
+
return this.files.read(path, 0, budget.walks);
|
|
3936
4353
|
}
|
|
3937
4354
|
if (size > maximum)
|
|
3938
4355
|
corrupt();
|
|
3939
4356
|
budget.reserve(size);
|
|
3940
|
-
return this.files.read(path, size);
|
|
4357
|
+
return this.files.read(path, size, budget.walks);
|
|
3941
4358
|
}
|
|
3942
4359
|
readCommand(commandId, budget) {
|
|
3943
4360
|
decodeId(commandId);
|
|
3944
4361
|
this.assertReadFormat(budget);
|
|
3945
|
-
return this.readBudgeted(
|
|
4362
|
+
return this.readBudgeted(join5(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
3946
4363
|
}
|
|
3947
4364
|
readAdmission(runId, identity, budget) {
|
|
3948
4365
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -3963,7 +4380,7 @@ class HistoryFiles {
|
|
|
3963
4380
|
syncParents(file) {
|
|
3964
4381
|
if (process.platform === "win32")
|
|
3965
4382
|
return;
|
|
3966
|
-
for (let path =
|
|
4383
|
+
for (let path = dirname6(file);; path = dirname6(path)) {
|
|
3967
4384
|
if (!this.files.directory(path))
|
|
3968
4385
|
corrupt();
|
|
3969
4386
|
const fd = openSync4(path, constants4.O_RDONLY | constants4.O_DIRECTORY);
|
|
@@ -3974,13 +4391,13 @@ class HistoryFiles {
|
|
|
3974
4391
|
}
|
|
3975
4392
|
if (path === this.coordination.directory)
|
|
3976
4393
|
break;
|
|
3977
|
-
if (path ===
|
|
4394
|
+
if (path === dirname6(path))
|
|
3978
4395
|
corrupt();
|
|
3979
4396
|
}
|
|
3980
4397
|
}
|
|
3981
4398
|
assertReadFormat(budget) {
|
|
3982
4399
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
3983
|
-
this.files.assert(
|
|
4400
|
+
this.files.assert(join5(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
3984
4401
|
}
|
|
3985
4402
|
assertPublished(segment, end, seq) {
|
|
3986
4403
|
const fence = publicationFences.get(this.directory);
|
|
@@ -4018,7 +4435,7 @@ class HistoryFiles {
|
|
|
4018
4435
|
const dir = opendirSync2(path);
|
|
4019
4436
|
try {
|
|
4020
4437
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
4021
|
-
const p =
|
|
4438
|
+
const p = join5(path, e.name), st = lstatSync5(p);
|
|
4022
4439
|
if (st.isDirectory())
|
|
4023
4440
|
bytes += walk(p, depth + 1);
|
|
4024
4441
|
else {
|
|
@@ -4053,7 +4470,7 @@ class HistoryFiles {
|
|
|
4053
4470
|
state.bytes = 0;
|
|
4054
4471
|
state.identity = null;
|
|
4055
4472
|
let count = 0;
|
|
4056
|
-
const ledger =
|
|
4473
|
+
const ledger = join5(this.directory, "ledger");
|
|
4057
4474
|
if (!this.files.directory(ledger))
|
|
4058
4475
|
corrupt();
|
|
4059
4476
|
const dir = opendirSync2(ledger);
|
|
@@ -4218,7 +4635,7 @@ class HistoryFiles {
|
|
|
4218
4635
|
let committed;
|
|
4219
4636
|
const fd = await open(path, constants4.O_WRONLY | constants4.O_APPEND | (constants4.O_NOFOLLOW ?? 0));
|
|
4220
4637
|
const assertOriginal = (bytes) => {
|
|
4221
|
-
const original = fstatSync4(fd.fd), selected2 =
|
|
4638
|
+
const original = fstatSync4(fd.fd), selected2 = lstatSync5(path);
|
|
4222
4639
|
if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
|
|
4223
4640
|
corrupt();
|
|
4224
4641
|
const snapshot = this.files.read(path, LIMITS.segmentBytes);
|
|
@@ -4335,7 +4752,7 @@ class HistoryFiles {
|
|
|
4335
4752
|
try {
|
|
4336
4753
|
lease.assert();
|
|
4337
4754
|
this.files.assert(path, before, LIMITS.segmentBytes);
|
|
4338
|
-
const opened = await fd.stat(), selected2 =
|
|
4755
|
+
const opened = await fd.stat(), selected2 = lstatSync5(path);
|
|
4339
4756
|
if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
|
|
4340
4757
|
corrupt();
|
|
4341
4758
|
beforeEffect?.();
|
|
@@ -4370,7 +4787,7 @@ class HistoryFiles {
|
|
|
4370
4787
|
checkpoint(lease, state) {
|
|
4371
4788
|
lease.assert();
|
|
4372
4789
|
this.assertWritable();
|
|
4373
|
-
const path =
|
|
4790
|
+
const path = join5(this.directory, "checkpoint.json"), old = this.files.read(path);
|
|
4374
4791
|
const body = encoded({
|
|
4375
4792
|
schema: "cadence-history-checkpoint/2",
|
|
4376
4793
|
storeId: this.format.storeId,
|
|
@@ -4783,8 +5200,8 @@ function digestHistory(files, exclude) {
|
|
|
4783
5200
|
const walk = (path, depth) => {
|
|
4784
5201
|
if (depth > 8 || !files.files.directory(path))
|
|
4785
5202
|
throw new ContractError("corrupt-selected");
|
|
4786
|
-
const
|
|
4787
|
-
digest.update(`incarnation:${
|
|
5203
|
+
const directory2 = lstatSync6(path);
|
|
5204
|
+
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
4788
5205
|
`);
|
|
4789
5206
|
const dir = opendirSync3(path), names2 = [];
|
|
4790
5207
|
try {
|
|
@@ -4797,11 +5214,11 @@ function digestHistory(files, exclude) {
|
|
|
4797
5214
|
dir.closeSync();
|
|
4798
5215
|
}
|
|
4799
5216
|
for (const name of names2.sort()) {
|
|
4800
|
-
const selected2 =
|
|
5217
|
+
const selected2 = join6(path, name);
|
|
4801
5218
|
if (selected2 === exclude)
|
|
4802
5219
|
continue;
|
|
4803
5220
|
const rel = relative4(files.directory, selected2);
|
|
4804
|
-
if (
|
|
5221
|
+
if (lstatSync6(selected2).isDirectory()) {
|
|
4805
5222
|
digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
|
|
4806
5223
|
`);
|
|
4807
5224
|
walk(selected2, depth + 1);
|
|
@@ -5015,7 +5432,7 @@ async function confirm(expected) {
|
|
|
5015
5432
|
`resolve ${expected.epoch}`
|
|
5016
5433
|
]
|
|
5017
5434
|
];
|
|
5018
|
-
await new Promise((
|
|
5435
|
+
await new Promise((resolve4, reject) => {
|
|
5019
5436
|
let index = 0, line = "", done = false;
|
|
5020
5437
|
const raw = process.stdin.isRaw;
|
|
5021
5438
|
const finish = (ok) => {
|
|
@@ -5031,7 +5448,7 @@ async function confirm(expected) {
|
|
|
5031
5448
|
process.stdout.write(`\x1B[?2004l
|
|
5032
5449
|
`);
|
|
5033
5450
|
if (ok)
|
|
5034
|
-
|
|
5451
|
+
resolve4();
|
|
5035
5452
|
else
|
|
5036
5453
|
reject(new ContractError("forbidden"));
|
|
5037
5454
|
};
|
|
@@ -5107,8 +5524,8 @@ async function localRecovery(c, operation) {
|
|
|
5107
5524
|
}
|
|
5108
5525
|
|
|
5109
5526
|
// src/service/runtime.ts
|
|
5110
|
-
import { homedir as
|
|
5111
|
-
import { dirname as
|
|
5527
|
+
import { homedir as homedir3 } from "node:os";
|
|
5528
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve4 } from "node:path";
|
|
5112
5529
|
|
|
5113
5530
|
// src/service/config.ts
|
|
5114
5531
|
import { isIP } from "node:net";
|
|
@@ -5162,7 +5579,7 @@ function localOrigin(policy, actualPort) {
|
|
|
5162
5579
|
var STARTUP_INPUT_BYTES = 16 * 1024;
|
|
5163
5580
|
function absolute(value) {
|
|
5164
5581
|
const result = text(value, 4096, true);
|
|
5165
|
-
if (!
|
|
5582
|
+
if (!isAbsolute5(result))
|
|
5166
5583
|
throw new ContractError("invalid");
|
|
5167
5584
|
return result;
|
|
5168
5585
|
}
|
|
@@ -5242,7 +5659,7 @@ function selectStart(options, ambient) {
|
|
|
5242
5659
|
throw new ContractError("invalid");
|
|
5243
5660
|
let fromFile;
|
|
5244
5661
|
if (options.config) {
|
|
5245
|
-
const path =
|
|
5662
|
+
const path = resolve4(options.config), f = new PrivateFiles(dirname7(path));
|
|
5246
5663
|
const bytes = f.read(path);
|
|
5247
5664
|
if (!bytes)
|
|
5248
5665
|
throw new ContractError("corrupt-selected");
|
|
@@ -5257,11 +5674,11 @@ function selectStart(options, ambient) {
|
|
|
5257
5674
|
port: options.port ?? 17423,
|
|
5258
5675
|
proxy: null
|
|
5259
5676
|
},
|
|
5260
|
-
agentDir:
|
|
5677
|
+
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join7(ambient.HOME ?? ambient.USERPROFILE ?? homedir3(), ".pi", "agent")),
|
|
5261
5678
|
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
5262
|
-
tokenFile: options.tokenFile ?
|
|
5679
|
+
tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
|
|
5263
5680
|
});
|
|
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 &&
|
|
5681
|
+
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
5682
|
throw new ContractError("conflict");
|
|
5266
5683
|
return {
|
|
5267
5684
|
workspace: workspace.root,
|
|
@@ -5293,21 +5710,37 @@ function portValue(value) {
|
|
|
5293
5710
|
throw new ContractError("invalid");
|
|
5294
5711
|
return integer(Number(value), 0, 65535);
|
|
5295
5712
|
}
|
|
5713
|
+
var STARTUP_STAGES = [
|
|
5714
|
+
"package",
|
|
5715
|
+
"pi-agent-directory",
|
|
5716
|
+
"pi-auth",
|
|
5717
|
+
"pi-models",
|
|
5718
|
+
"pi-runtime",
|
|
5719
|
+
"service"
|
|
5720
|
+
];
|
|
5721
|
+
function decodeStartupReport(value) {
|
|
5722
|
+
const o = closed(value, ["status", "error", "stage"]);
|
|
5723
|
+
return {
|
|
5724
|
+
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
5725
|
+
error: oneOf(o.error, ERROR_CODES),
|
|
5726
|
+
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
5727
|
+
};
|
|
5728
|
+
}
|
|
5296
5729
|
|
|
5297
5730
|
// src/service/cli.ts
|
|
5298
5731
|
function nativeCall(c, operation, reveal = false) {
|
|
5299
5732
|
const access = c.localAccess();
|
|
5300
5733
|
const url = new URL(`${access.endpoint}/local/v1/admin/${operation}`);
|
|
5301
|
-
const
|
|
5302
|
-
const body =
|
|
5303
|
-
return new Promise((
|
|
5734
|
+
const write2 = !["status", "diagnose"].includes(operation);
|
|
5735
|
+
const body = write2 ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
|
|
5736
|
+
return new Promise((resolve6, reject) => {
|
|
5304
5737
|
let timer;
|
|
5305
5738
|
const fail = () => {
|
|
5306
5739
|
clearTimeout(timer);
|
|
5307
5740
|
reject(new ContractError("unavailable"));
|
|
5308
5741
|
};
|
|
5309
5742
|
const req = request(url, {
|
|
5310
|
-
method:
|
|
5743
|
+
method: write2 ? "POST" : "GET",
|
|
5311
5744
|
agent: false,
|
|
5312
5745
|
headers: {
|
|
5313
5746
|
"X-Cadence-Local": access.capability,
|
|
@@ -5341,7 +5774,7 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
5341
5774
|
throw new ContractError("stale");
|
|
5342
5775
|
}
|
|
5343
5776
|
clearTimeout(timer);
|
|
5344
|
-
|
|
5777
|
+
resolve6(reply.value);
|
|
5345
5778
|
} catch {
|
|
5346
5779
|
fail();
|
|
5347
5780
|
}
|
|
@@ -5356,7 +5789,7 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
5356
5789
|
req.end(body);
|
|
5357
5790
|
});
|
|
5358
5791
|
}
|
|
5359
|
-
var pause = (ms) => new Promise((
|
|
5792
|
+
var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5360
5793
|
async function serviceStatus(c, diagnose = false) {
|
|
5361
5794
|
const observed = c.inspect();
|
|
5362
5795
|
if (observed.status === "absent" || observed.status === "stopped")
|
|
@@ -5393,7 +5826,38 @@ async function stopService(c) {
|
|
|
5393
5826
|
return { status: "unconfirmed", epoch };
|
|
5394
5827
|
}
|
|
5395
5828
|
function failedStatus(result) {
|
|
5396
|
-
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined"].includes(String(result.status)));
|
|
5829
|
+
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
|
|
5830
|
+
}
|
|
5831
|
+
function startupGuidance(result, input) {
|
|
5832
|
+
let report;
|
|
5833
|
+
try {
|
|
5834
|
+
report = decodeStartupReport(result);
|
|
5835
|
+
} catch {
|
|
5836
|
+
return "";
|
|
5837
|
+
}
|
|
5838
|
+
const agent = input.config.agentDir;
|
|
5839
|
+
const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
|
|
5840
|
+
`;
|
|
5841
|
+
switch (report.stage) {
|
|
5842
|
+
case "package":
|
|
5843
|
+
return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
|
|
5844
|
+
`;
|
|
5845
|
+
case "pi-agent-directory":
|
|
5846
|
+
return `Pi's agent directory ${agent} must be an existing real directory owned by you with mode 0700 (chmod 700).
|
|
5847
|
+
`;
|
|
5848
|
+
case "pi-auth":
|
|
5849
|
+
return `${join8(agent, "auth.json")} must be your own single-link regular file with mode 0600 holding valid Pi credentials; log in to Pi first.
|
|
5850
|
+
`;
|
|
5851
|
+
case "pi-models":
|
|
5852
|
+
return `${join8(agent, "models.json")} must be your own single-link regular file with mode 0600, using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
|
|
5853
|
+
`;
|
|
5854
|
+
case "pi-runtime":
|
|
5855
|
+
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
5856
|
+
${inspect}`;
|
|
5857
|
+
default:
|
|
5858
|
+
return report.status === "quarantined" ? `The owner registered, then refused; it stays quarantined.
|
|
5859
|
+
${inspect}` : "";
|
|
5860
|
+
}
|
|
5397
5861
|
}
|
|
5398
5862
|
async function startService(options, foreground, hostUrl) {
|
|
5399
5863
|
const network = networkSnapshot(process.env);
|
|
@@ -5420,18 +5884,18 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5420
5884
|
stdio: ["pipe", "pipe", "ignore"]
|
|
5421
5885
|
});
|
|
5422
5886
|
let completed = false;
|
|
5423
|
-
const closure = new Promise((
|
|
5887
|
+
const closure = new Promise((resolve6) => child.once("close", (code, signal) => {
|
|
5424
5888
|
completed = true;
|
|
5425
5889
|
if (foreground && (code !== 0 || signal !== null))
|
|
5426
5890
|
process.exitCode = 1;
|
|
5427
|
-
|
|
5891
|
+
resolve6();
|
|
5428
5892
|
}));
|
|
5429
|
-
const startup = new Promise((
|
|
5893
|
+
const startup = new Promise((resolve6) => {
|
|
5430
5894
|
let bytes = "", resolved = false;
|
|
5431
5895
|
const finish = (value) => {
|
|
5432
5896
|
if (!resolved) {
|
|
5433
5897
|
resolved = true;
|
|
5434
|
-
|
|
5898
|
+
resolve6(value);
|
|
5435
5899
|
}
|
|
5436
5900
|
};
|
|
5437
5901
|
child.on("error", () => finish({ status: "unconfirmed" }));
|
|
@@ -5447,7 +5911,12 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5447
5911
|
`))
|
|
5448
5912
|
return;
|
|
5449
5913
|
try {
|
|
5450
|
-
const
|
|
5914
|
+
const line = parseWireJson(bytes, 4096);
|
|
5915
|
+
if (line && typeof line === "object" && "status" in line && line.status !== "ready") {
|
|
5916
|
+
finish(decodeStartupReport(line));
|
|
5917
|
+
return;
|
|
5918
|
+
}
|
|
5919
|
+
const ready = closed(line, ["status", "epoch"]);
|
|
5451
5920
|
if (ready.status !== "ready")
|
|
5452
5921
|
throw new ContractError("unavailable");
|
|
5453
5922
|
const epoch = decodeId(ready.epoch);
|
|
@@ -5466,11 +5935,14 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5466
5935
|
let timer;
|
|
5467
5936
|
const result = await Promise.race([
|
|
5468
5937
|
startup,
|
|
5469
|
-
new Promise((
|
|
5470
|
-
timer = setTimeout(() =>
|
|
5938
|
+
new Promise((resolve6) => {
|
|
5939
|
+
timer = setTimeout(() => resolve6({ status: "unconfirmed" }), LIMITS.startWaitMs);
|
|
5471
5940
|
})
|
|
5472
5941
|
]);
|
|
5473
5942
|
clearTimeout(timer);
|
|
5943
|
+
const guidance = startupGuidance(result, input);
|
|
5944
|
+
if (guidance)
|
|
5945
|
+
process.stderr.write(guidance);
|
|
5474
5946
|
child.stdout.destroy();
|
|
5475
5947
|
child.stdin.destroy();
|
|
5476
5948
|
if (!foreground)
|
|
@@ -5485,6 +5957,104 @@ async function startService(options, foreground, hostUrl) {
|
|
|
5485
5957
|
}
|
|
5486
5958
|
return result;
|
|
5487
5959
|
}
|
|
5960
|
+
async function observeStarted(workspace) {
|
|
5961
|
+
const deadline = performance.now() + LIMITS.startWaitMs + 2000;
|
|
5962
|
+
let service = "unconfirmed";
|
|
5963
|
+
while (performance.now() < deadline) {
|
|
5964
|
+
try {
|
|
5965
|
+
const c = new ServiceCoordination(workspace);
|
|
5966
|
+
const observed = await serviceStatus(c);
|
|
5967
|
+
if (observed && typeof observed === "object" && "status" in observed)
|
|
5968
|
+
service = String(observed.status);
|
|
5969
|
+
if (service === "alive")
|
|
5970
|
+
return { service, endpoint: c.localAccess().endpoint };
|
|
5971
|
+
if (service === "quarantined")
|
|
5972
|
+
break;
|
|
5973
|
+
} catch {}
|
|
5974
|
+
await pause(250);
|
|
5975
|
+
}
|
|
5976
|
+
return { service };
|
|
5977
|
+
}
|
|
5978
|
+
function autostartGuidance(s, result) {
|
|
5979
|
+
const name = s.unit.replace(/\.service$/, "");
|
|
5980
|
+
const cli = JSON.stringify(join8(s.root, "src", "service-cli.mjs"));
|
|
5981
|
+
const workspace = JSON.stringify(result.workspace ?? "");
|
|
5982
|
+
const manage = `Manage: systemctl --user status|restart|stop ${name}; logs: journalctl --user -u ${name}; opt out: systemctl --user disable --now ${name}`;
|
|
5983
|
+
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
5984
|
+
` : "";
|
|
5985
|
+
return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
|
|
5986
|
+
}
|
|
5987
|
+
function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
|
|
5988
|
+
switch (result.status) {
|
|
5989
|
+
case "started": {
|
|
5990
|
+
if (result.service !== "alive")
|
|
5991
|
+
return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
|
|
5992
|
+
Inspect: node ${cli} diagnose --workspace ${workspace}
|
|
5993
|
+
${manage}
|
|
5994
|
+
`;
|
|
5995
|
+
const linger = s.linger() === false ? "Linger is off: Cadence starts at login and stops at logout. `loginctl enable-linger` keeps it running and starts it at boot.\n" : "";
|
|
5996
|
+
return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
|
|
5997
|
+
Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
|
|
5998
|
+
${manage}
|
|
5999
|
+
${linger}`;
|
|
6000
|
+
}
|
|
6001
|
+
case "waiting-for-pi-login":
|
|
6002
|
+
return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
|
|
6003
|
+
Log in to Pi, then: systemctl --user start ${name}
|
|
6004
|
+
`;
|
|
6005
|
+
case "disabled":
|
|
6006
|
+
return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
|
|
6007
|
+
`;
|
|
6008
|
+
case "masked":
|
|
6009
|
+
return `${s.unit} is masked; Cadence did not enable or start it.
|
|
6010
|
+
`;
|
|
6011
|
+
case "missing-pi-sdk":
|
|
6012
|
+
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.
|
|
6013
|
+
`;
|
|
6014
|
+
case "foreign":
|
|
6015
|
+
return `${result.unit} was not generated by Cadence; left unchanged.
|
|
6016
|
+
`;
|
|
6017
|
+
case "unavailable":
|
|
6018
|
+
return `No systemd user manager; start Cadence explicitly: node ${cli} start --workspace /absolute/project
|
|
6019
|
+
`;
|
|
6020
|
+
case "failed":
|
|
6021
|
+
return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
|
|
6022
|
+
`;
|
|
6023
|
+
default:
|
|
6024
|
+
return "";
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
async function autostartMain(argv, hostUrl) {
|
|
6028
|
+
const action = argv.shift();
|
|
6029
|
+
let requested;
|
|
6030
|
+
if (action === "install" && argv[0] === "--workspace" && argv[1])
|
|
6031
|
+
requested = resolve5(argv.splice(0, 2)[1]);
|
|
6032
|
+
if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
|
|
6033
|
+
throw new ContractError("invalid");
|
|
6034
|
+
const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
|
|
6035
|
+
let result;
|
|
6036
|
+
if (action === "remove")
|
|
6037
|
+
result = removeAutostart(s);
|
|
6038
|
+
else if (action === "status")
|
|
6039
|
+
result = autostartStatus(s);
|
|
6040
|
+
else {
|
|
6041
|
+
try {
|
|
6042
|
+
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested);
|
|
6043
|
+
} catch (error) {
|
|
6044
|
+
if (error instanceof ContractError && error.code === "invalid")
|
|
6045
|
+
process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
|
|
6046
|
+
`);
|
|
6047
|
+
throw error;
|
|
6048
|
+
}
|
|
6049
|
+
if (result.status === "started" && result.workspace)
|
|
6050
|
+
Object.assign(result, await observeStarted(result.workspace));
|
|
6051
|
+
process.stderr.write(autostartGuidance(s, result));
|
|
6052
|
+
}
|
|
6053
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
6054
|
+
`);
|
|
6055
|
+
if (["failed", "foreign", "unavailable"].includes(result.status))
|
|
6056
|
+
process.exitCode = 1;
|
|
6057
|
+
}
|
|
5488
6058
|
async function managementMain(argv, hostUrl) {
|
|
5489
6059
|
try {
|
|
5490
6060
|
if (!process.versions.node || process.versions.bun)
|
|
@@ -5492,12 +6062,17 @@ async function managementMain(argv, hostUrl) {
|
|
|
5492
6062
|
const command = argv.shift();
|
|
5493
6063
|
if (command === "--help" && !argv.length) {
|
|
5494
6064
|
process.stdout.write(`cadence-service start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
5495
|
-
|
|
6065
|
+
cadence-service autostart install [--workspace PATH]|refresh|remove|status
|
|
6066
|
+
Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out). Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
|
|
5496
6067
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
5497
6068
|
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
6069
|
`);
|
|
5499
6070
|
return;
|
|
5500
6071
|
}
|
|
6072
|
+
if (command === "autostart") {
|
|
6073
|
+
await autostartMain(argv, hostUrl);
|
|
6074
|
+
return;
|
|
6075
|
+
}
|
|
5501
6076
|
let recovery;
|
|
5502
6077
|
if (![
|
|
5503
6078
|
"start",
|
|
@@ -5555,10 +6130,23 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
5555
6130
|
else
|
|
5556
6131
|
throw new ContractError("invalid");
|
|
5557
6132
|
}
|
|
6133
|
+
const landing = effectiveWorkspace(options.workspace, options.stateHome);
|
|
6134
|
+
if (landing.redirected) {
|
|
6135
|
+
if (seen.has("--workspace"))
|
|
6136
|
+
process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
|
|
6137
|
+
`);
|
|
6138
|
+
options.workspace = landing.workspace;
|
|
6139
|
+
if (command === "start")
|
|
6140
|
+
mkdirSync4(landing.workspace, { recursive: true });
|
|
6141
|
+
}
|
|
5558
6142
|
let result;
|
|
5559
6143
|
if (command === "start")
|
|
5560
6144
|
result = await startService(options, foreground, hostUrl);
|
|
5561
|
-
else {
|
|
6145
|
+
else if (landing.redirected && !existsSync(landing.workspace)) {
|
|
6146
|
+
if (command !== "status" && command !== "diagnose")
|
|
6147
|
+
throw new ContractError("unavailable");
|
|
6148
|
+
result = { status: "absent" };
|
|
6149
|
+
} else {
|
|
5562
6150
|
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
5563
6151
|
if (command === "recover" && recovery)
|
|
5564
6152
|
result = await localRecovery(c, recovery);
|
|
@@ -5576,6 +6164,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
5576
6164
|
else
|
|
5577
6165
|
throw new ContractError("invalid");
|
|
5578
6166
|
}
|
|
6167
|
+
if (landing.redirected && result && typeof result === "object")
|
|
6168
|
+
result = { ...result, workspace: landing.workspace };
|
|
5579
6169
|
if (result !== null)
|
|
5580
6170
|
process.stdout.write(`${JSON.stringify(result)}
|
|
5581
6171
|
`);
|