@abelxiaoxing/cadence 2.0.5 → 2.0.7
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 +36 -7
- package/docs/remote-access.md +3 -2
- package/docs/service-management.md +33 -5
- package/package.json +1 -3
- package/src/index.ts +6 -2
- package/src/service/coordination.ts +25 -2
- package/src/service-cli.mjs +816 -533
- package/src/service-host.mjs +272 -78
- package/src/startup-card.ts +4 -1
- package/web/dist/assets/cadence.js +1 -1
- package/web/dist/manifest.json +3 -3
- package/src/install-config.mjs +0 -59
package/src/service-cli.mjs
CHANGED
|
@@ -3,11 +3,48 @@
|
|
|
3
3
|
|
|
4
4
|
// src/service/cli.ts
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync, mkdirSync as
|
|
6
|
+
import { existsSync, mkdirSync as mkdirSync5 } from "node:fs";
|
|
7
7
|
import { request } from "node:http";
|
|
8
|
-
import { dirname as
|
|
8
|
+
import { dirname as dirname9, join as join11, resolve as resolve5 } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
|
+
// src/install-config.mjs
|
|
12
|
+
import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
var CONFIG_TEMPLATE = `# Cadence research configuration. Keep this file local and never commit it.
|
|
16
|
+
# Context7 works anonymously with its default URL.
|
|
17
|
+
# CONTEXT7_API_URL=https://context7.com/api/v2
|
|
18
|
+
# CONTEXT7_API_KEY=
|
|
19
|
+
|
|
20
|
+
# Required when using the Grok search skill.
|
|
21
|
+
GROK_API_URL=https://api.x.ai/v1
|
|
22
|
+
GROK_API_KEY=
|
|
23
|
+
|
|
24
|
+
# Optional Tavily integration.
|
|
25
|
+
# TAVILY_ENABLED=true
|
|
26
|
+
# TAVILY_API_URL=https://api.tavily.com
|
|
27
|
+
# TAVILY_API_KEY=
|
|
28
|
+
`;
|
|
29
|
+
function ensureUserConfig(home = homedir()) {
|
|
30
|
+
const directory = path.join(home, ".pi", "agent", "cadence");
|
|
31
|
+
const file = path.join(directory, ".env");
|
|
32
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
33
|
+
try {
|
|
34
|
+
const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
35
|
+
try {
|
|
36
|
+
writeSync(fd, CONFIG_TEMPLATE, undefined, "utf8");
|
|
37
|
+
} finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
return { created: true, path: file };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code === "EEXIST")
|
|
43
|
+
return { created: false, path: file };
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
// src/task-types.ts
|
|
12
49
|
var TASK_SCHEMA = "cadence-workflow-assistant/1";
|
|
13
50
|
var MAX_RECORD_BYTES = 128 * 1024;
|
|
@@ -2049,13 +2086,13 @@ class CaptureBudgetLease {
|
|
|
2049
2086
|
import { spawnSync } from "node:child_process";
|
|
2050
2087
|
import { createHash, randomUUID } from "node:crypto";
|
|
2051
2088
|
import {
|
|
2052
|
-
closeSync,
|
|
2053
|
-
constants,
|
|
2089
|
+
closeSync as closeSync2,
|
|
2090
|
+
constants as constants2,
|
|
2054
2091
|
fstatSync,
|
|
2055
2092
|
fsyncSync,
|
|
2056
2093
|
lstatSync,
|
|
2057
|
-
mkdirSync,
|
|
2058
|
-
openSync,
|
|
2094
|
+
mkdirSync as mkdirSync2,
|
|
2095
|
+
openSync as openSync2,
|
|
2059
2096
|
readdirSync,
|
|
2060
2097
|
readSync,
|
|
2061
2098
|
realpathSync,
|
|
@@ -2063,7 +2100,7 @@ import {
|
|
|
2063
2100
|
unlinkSync,
|
|
2064
2101
|
writeFileSync
|
|
2065
2102
|
} from "node:fs";
|
|
2066
|
-
import { homedir, hostname } from "node:os";
|
|
2103
|
+
import { homedir as homedir2, hostname } from "node:os";
|
|
2067
2104
|
import {
|
|
2068
2105
|
dirname,
|
|
2069
2106
|
isAbsolute,
|
|
@@ -2100,8 +2137,8 @@ function parseRecord(data) {
|
|
|
2100
2137
|
function absent(error) {
|
|
2101
2138
|
return object(error) && error.code === "ENOENT";
|
|
2102
2139
|
}
|
|
2103
|
-
function safeDirectory(
|
|
2104
|
-
const full = resolve(
|
|
2140
|
+
function safeDirectory(path2, create = false, inspectExisting) {
|
|
2141
|
+
const full = resolve(path2);
|
|
2105
2142
|
const root = parse(full).root;
|
|
2106
2143
|
if (inspectExisting) {
|
|
2107
2144
|
const st = lstatSync(root);
|
|
@@ -2124,7 +2161,7 @@ function safeDirectory(path, create = false, inspectExisting) {
|
|
|
2124
2161
|
if (!create)
|
|
2125
2162
|
return false;
|
|
2126
2163
|
try {
|
|
2127
|
-
|
|
2164
|
+
mkdirSync2(at, { mode: 448 });
|
|
2128
2165
|
} catch (mkdirError) {
|
|
2129
2166
|
if (!object(mkdirError) || mkdirError.code !== "EEXIST")
|
|
2130
2167
|
throw mkdirError;
|
|
@@ -2154,15 +2191,15 @@ function retryWindowsFileOperation(operation, recheck) {
|
|
|
2154
2191
|
}
|
|
2155
2192
|
}
|
|
2156
2193
|
}
|
|
2157
|
-
function readBounded(
|
|
2158
|
-
if (!safeDirectory(dirname(
|
|
2194
|
+
function readBounded(path2, max, packaged = false) {
|
|
2195
|
+
if (!safeDirectory(dirname(path2)))
|
|
2159
2196
|
return;
|
|
2160
2197
|
let fd;
|
|
2161
2198
|
try {
|
|
2162
|
-
const st = lstatSync(
|
|
2199
|
+
const st = lstatSync(path2);
|
|
2163
2200
|
if (!st.isFile() || st.isSymbolicLink() || !packaged && st.nlink !== 1 || st.size > max)
|
|
2164
2201
|
throw new Error("Unsafe or oversized file");
|
|
2165
|
-
fd =
|
|
2202
|
+
fd = openSync2(path2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2166
2203
|
} catch (error) {
|
|
2167
2204
|
if (absent(error))
|
|
2168
2205
|
return;
|
|
@@ -2184,7 +2221,7 @@ function readBounded(path, max, packaged = false) {
|
|
|
2184
2221
|
throw new Error("Oversized file");
|
|
2185
2222
|
return buffer.subarray(0, size);
|
|
2186
2223
|
} finally {
|
|
2187
|
-
|
|
2224
|
+
closeSync2(fd);
|
|
2188
2225
|
}
|
|
2189
2226
|
}
|
|
2190
2227
|
function gitEnvironment() {
|
|
@@ -2219,7 +2256,7 @@ function defaultStateHome() {
|
|
|
2219
2256
|
const candidate = process.platform === "win32" ? process.env.LOCALAPPDATA : process.env.XDG_STATE_HOME;
|
|
2220
2257
|
if (candidate && !isAbsolute(candidate))
|
|
2221
2258
|
throw new Error("State home must be absolute");
|
|
2222
|
-
return candidate ?? join(
|
|
2259
|
+
return candidate ?? join(homedir2(), process.platform === "win32" ? "AppData/Local" : ".local/state");
|
|
2223
2260
|
}
|
|
2224
2261
|
function recordText(record) {
|
|
2225
2262
|
return `${JSON.stringify(record, null, 2)}
|
|
@@ -2228,8 +2265,8 @@ function recordText(record) {
|
|
|
2228
2265
|
function contentIdentity(text2) {
|
|
2229
2266
|
return createHash("sha256").update(text2).digest("hex");
|
|
2230
2267
|
}
|
|
2231
|
-
function canonicalAmbientRoot(
|
|
2232
|
-
const full = resolve(
|
|
2268
|
+
function canonicalAmbientRoot(path2) {
|
|
2269
|
+
const full = resolve(path2);
|
|
2233
2270
|
try {
|
|
2234
2271
|
return realpathSync(full);
|
|
2235
2272
|
} catch (error) {
|
|
@@ -2251,8 +2288,8 @@ function canonicalAmbientRoot(path) {
|
|
|
2251
2288
|
function privateRootFor(canonicalRoot, stateHome) {
|
|
2252
2289
|
return join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1", createHash("sha256").update(canonicalRoot).digest("hex"));
|
|
2253
2290
|
}
|
|
2254
|
-
function contains(root,
|
|
2255
|
-
const within = relative(root, resolve(
|
|
2291
|
+
function contains(root, path2) {
|
|
2292
|
+
const within = relative(root, resolve(path2));
|
|
2256
2293
|
return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
|
|
2257
2294
|
}
|
|
2258
2295
|
function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
@@ -2260,7 +2297,7 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
2260
2297
|
return contains(canonical, privateRootFor(canonical, stateHome));
|
|
2261
2298
|
}
|
|
2262
2299
|
var LANDING_WORKSPACE = "cadence";
|
|
2263
|
-
function effectiveWorkspace(requested, stateHome, home =
|
|
2300
|
+
function effectiveWorkspace(requested, stateHome, home = homedir2()) {
|
|
2264
2301
|
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
2265
2302
|
return { workspace: requested, redirected: false };
|
|
2266
2303
|
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
@@ -2282,20 +2319,20 @@ class TaskStore {
|
|
|
2282
2319
|
}
|
|
2283
2320
|
inspectPrivate() {
|
|
2284
2321
|
const namespace = dirname(this.privateRoot);
|
|
2285
|
-
for (const
|
|
2286
|
-
if (!safeDirectory(
|
|
2322
|
+
for (const path2 of [namespace, this.privateRoot]) {
|
|
2323
|
+
if (!safeDirectory(path2))
|
|
2287
2324
|
continue;
|
|
2288
|
-
const st = lstatSync(
|
|
2325
|
+
const st = lstatSync(path2);
|
|
2289
2326
|
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2290
2327
|
throw new Error("Private state permissions refused");
|
|
2291
2328
|
}
|
|
2292
2329
|
}
|
|
2293
|
-
inspectPrivateFile(
|
|
2330
|
+
inspectPrivateFile(path2, max) {
|
|
2294
2331
|
this.inspectPrivate();
|
|
2295
2332
|
if (!safeDirectory(this.privateRoot))
|
|
2296
2333
|
return;
|
|
2297
2334
|
try {
|
|
2298
|
-
const st = lstatSync(
|
|
2335
|
+
const st = lstatSync(path2);
|
|
2299
2336
|
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.size > max || process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2300
2337
|
throw new Error("Unsafe private state file");
|
|
2301
2338
|
} catch (error) {
|
|
@@ -2326,7 +2363,7 @@ class TaskStore {
|
|
|
2326
2363
|
};
|
|
2327
2364
|
let fd;
|
|
2328
2365
|
try {
|
|
2329
|
-
fd = retryWindowsFileOperation(() =>
|
|
2366
|
+
fd = retryWindowsFileOperation(() => openSync2(this.lockPath, "wx", 384));
|
|
2330
2367
|
} catch {
|
|
2331
2368
|
throw new Error("Checkout busy or unresolved lock; inspect and explicitly recover only a dead owner");
|
|
2332
2369
|
}
|
|
@@ -2334,7 +2371,7 @@ class TaskStore {
|
|
|
2334
2371
|
writeFileSync(fd, JSON.stringify(lock));
|
|
2335
2372
|
fsyncSync(fd);
|
|
2336
2373
|
} finally {
|
|
2337
|
-
|
|
2374
|
+
closeSync2(fd);
|
|
2338
2375
|
}
|
|
2339
2376
|
return lock;
|
|
2340
2377
|
}
|
|
@@ -2415,12 +2452,12 @@ class TaskStore {
|
|
|
2415
2452
|
if (!current && readdirSync(dir).length)
|
|
2416
2453
|
throw new Error("Unknown task directory");
|
|
2417
2454
|
const temporary = join(dir, `task.${randomUUID()}.tmp`);
|
|
2418
|
-
const fd =
|
|
2455
|
+
const fd = openSync2(temporary, "wx", 384);
|
|
2419
2456
|
try {
|
|
2420
2457
|
writeFileSync(fd, text2);
|
|
2421
2458
|
fsyncSync(fd);
|
|
2422
2459
|
} finally {
|
|
2423
|
-
|
|
2460
|
+
closeSync2(fd);
|
|
2424
2461
|
}
|
|
2425
2462
|
this.assertOwner(lock);
|
|
2426
2463
|
const latest = readBounded(join(dir, "task.json"), MAX_RECORD_BYTES);
|
|
@@ -2474,25 +2511,420 @@ var TASK_CONFIG_SCHEMA = "cadence-assistant-checks/1";
|
|
|
2474
2511
|
// src/service/autostart.ts
|
|
2475
2512
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2476
2513
|
import {
|
|
2477
|
-
lstatSync as
|
|
2478
|
-
mkdirSync as
|
|
2514
|
+
lstatSync as lstatSync3,
|
|
2515
|
+
mkdirSync as mkdirSync4,
|
|
2479
2516
|
readFileSync,
|
|
2480
2517
|
realpathSync as realpathSync2,
|
|
2481
|
-
renameSync as
|
|
2518
|
+
renameSync as renameSync3,
|
|
2482
2519
|
rmSync,
|
|
2483
2520
|
statSync,
|
|
2484
|
-
writeFileSync as
|
|
2521
|
+
writeFileSync as writeFileSync3
|
|
2485
2522
|
} from "node:fs";
|
|
2486
|
-
import { homedir as
|
|
2523
|
+
import { homedir as homedir3 } from "node:os";
|
|
2487
2524
|
import {
|
|
2488
2525
|
delimiter,
|
|
2489
|
-
dirname as
|
|
2490
|
-
isAbsolute as
|
|
2491
|
-
join as
|
|
2492
|
-
resolve as
|
|
2493
|
-
sep as
|
|
2526
|
+
dirname as dirname4,
|
|
2527
|
+
isAbsolute as isAbsolute3,
|
|
2528
|
+
join as join4,
|
|
2529
|
+
resolve as resolve3,
|
|
2530
|
+
sep as sep3,
|
|
2494
2531
|
win32
|
|
2495
2532
|
} from "node:path";
|
|
2533
|
+
|
|
2534
|
+
// src/service/server-config.ts
|
|
2535
|
+
import { randomBytes } from "node:crypto";
|
|
2536
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2537
|
+
|
|
2538
|
+
// skills/_shared/load-config.mjs
|
|
2539
|
+
function parseEnvFile(content) {
|
|
2540
|
+
const values = {};
|
|
2541
|
+
const lines = content.replaceAll(`\r
|
|
2542
|
+
`, `
|
|
2543
|
+
`).replaceAll("\r", `
|
|
2544
|
+
`).split(`
|
|
2545
|
+
`);
|
|
2546
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
2547
|
+
const line = lines[index].trim();
|
|
2548
|
+
if (!line || line.startsWith("#"))
|
|
2549
|
+
continue;
|
|
2550
|
+
const separator = line.indexOf("=");
|
|
2551
|
+
if (separator <= 0)
|
|
2552
|
+
throw new Error(`Invalid configuration syntax at line ${index + 1}`);
|
|
2553
|
+
const name = line.slice(0, separator).trim();
|
|
2554
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
2555
|
+
throw new Error(`Invalid configuration name at line ${index + 1}`);
|
|
2556
|
+
}
|
|
2557
|
+
let value = line.slice(separator + 1).trim();
|
|
2558
|
+
const quote = value[0];
|
|
2559
|
+
if (quote === '"' || quote === "'") {
|
|
2560
|
+
if (value.at(-1) !== quote) {
|
|
2561
|
+
throw new Error(`Unterminated quoted configuration value at line ${index + 1}`);
|
|
2562
|
+
}
|
|
2563
|
+
value = value.slice(1, -1);
|
|
2564
|
+
}
|
|
2565
|
+
values[name] = value;
|
|
2566
|
+
}
|
|
2567
|
+
return values;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/service/private-files.ts
|
|
2571
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
2572
|
+
import {
|
|
2573
|
+
closeSync as closeSync3,
|
|
2574
|
+
constants as constants3,
|
|
2575
|
+
fstatSync as fstatSync2,
|
|
2576
|
+
fsyncSync as fsyncSync2,
|
|
2577
|
+
lstatSync as lstatSync2,
|
|
2578
|
+
mkdirSync as mkdirSync3,
|
|
2579
|
+
openSync as openSync3,
|
|
2580
|
+
readSync as readSync2,
|
|
2581
|
+
renameSync as renameSync2,
|
|
2582
|
+
unlinkSync as unlinkSync2,
|
|
2583
|
+
writeFileSync as writeFileSync2
|
|
2584
|
+
} from "node:fs";
|
|
2585
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2586
|
+
var METADATA_BYTES = 16 * 1024;
|
|
2587
|
+
var REGISTRY_BYTES = 1024 * 1024;
|
|
2588
|
+
var REGISTRY_ENTRIES = 128;
|
|
2589
|
+
function refused() {
|
|
2590
|
+
throw new ContractError("corrupt-selected");
|
|
2591
|
+
}
|
|
2592
|
+
function hash(bytes) {
|
|
2593
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
2594
|
+
}
|
|
2595
|
+
function privateMode(st, ownerOnly = true) {
|
|
2596
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2597
|
+
refused();
|
|
2598
|
+
}
|
|
2599
|
+
function same(a, b) {
|
|
2600
|
+
return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.ctimeMs === b.ctimeMs && a.mtimeMs === b.mtimeMs && a.mode === b.mode && a.uid === b.uid && a.nlink === b.nlink;
|
|
2601
|
+
}
|
|
2602
|
+
function missing(error) {
|
|
2603
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2604
|
+
}
|
|
2605
|
+
var job = 0;
|
|
2606
|
+
var advancing = false;
|
|
2607
|
+
function currentJob() {
|
|
2608
|
+
if (!advancing) {
|
|
2609
|
+
advancing = true;
|
|
2610
|
+
queueMicrotask(() => {
|
|
2611
|
+
job++;
|
|
2612
|
+
advancing = false;
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
return job;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
class DirectoryWalks {
|
|
2619
|
+
job = currentJob();
|
|
2620
|
+
walked = new Map;
|
|
2621
|
+
open() {
|
|
2622
|
+
if (this.job === job)
|
|
2623
|
+
return true;
|
|
2624
|
+
this.walked.clear();
|
|
2625
|
+
return false;
|
|
2626
|
+
}
|
|
2627
|
+
remember(path2, st) {
|
|
2628
|
+
if (this.open())
|
|
2629
|
+
this.walked.set(path2, { dev: st.dev, ino: st.ino });
|
|
2630
|
+
}
|
|
2631
|
+
reuse(path2, ownerOnly = true) {
|
|
2632
|
+
const walked = this.open() ? this.walked.get(path2) : undefined;
|
|
2633
|
+
if (!walked)
|
|
2634
|
+
return false;
|
|
2635
|
+
let st;
|
|
2636
|
+
try {
|
|
2637
|
+
st = lstatSync2(path2);
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
if (!missing(error))
|
|
2640
|
+
throw error;
|
|
2641
|
+
this.walked.delete(path2);
|
|
2642
|
+
return false;
|
|
2643
|
+
}
|
|
2644
|
+
if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
|
|
2645
|
+
this.walked.delete(path2);
|
|
2646
|
+
return false;
|
|
2647
|
+
}
|
|
2648
|
+
privateMode(st, ownerOnly);
|
|
2649
|
+
return true;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
class PrivateFiles {
|
|
2654
|
+
namespace;
|
|
2655
|
+
ownerOnly;
|
|
2656
|
+
constructor(namespace, ownerOnly = true) {
|
|
2657
|
+
this.namespace = namespace;
|
|
2658
|
+
this.ownerOnly = ownerOnly;
|
|
2659
|
+
}
|
|
2660
|
+
directory(path2, create = false, walks) {
|
|
2661
|
+
const tail = relative2(this.namespace, path2);
|
|
2662
|
+
if (isAbsolute2(tail) || tail === ".." || tail.startsWith(`..${sep2}`))
|
|
2663
|
+
refused();
|
|
2664
|
+
if (!create) {
|
|
2665
|
+
const full = resolve2(path2);
|
|
2666
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2667
|
+
return true;
|
|
2668
|
+
const namespace = resolve2(this.namespace);
|
|
2669
|
+
let inside = false, last;
|
|
2670
|
+
const found = safeDirectory(path2, false, (at2, st) => {
|
|
2671
|
+
if (at2 === namespace)
|
|
2672
|
+
inside = true;
|
|
2673
|
+
if (inside)
|
|
2674
|
+
privateMode(st, this.ownerOnly);
|
|
2675
|
+
last = st;
|
|
2676
|
+
});
|
|
2677
|
+
if (found && last)
|
|
2678
|
+
walks?.remember(full, last);
|
|
2679
|
+
return found;
|
|
2680
|
+
}
|
|
2681
|
+
if (!safeDirectory(this.namespace, true))
|
|
2682
|
+
return false;
|
|
2683
|
+
privateMode(lstatSync2(this.namespace), this.ownerOnly);
|
|
2684
|
+
let at = this.namespace;
|
|
2685
|
+
for (const part of tail.split(sep2).filter(Boolean)) {
|
|
2686
|
+
at = join2(at, part);
|
|
2687
|
+
let st;
|
|
2688
|
+
try {
|
|
2689
|
+
st = lstatSync2(at);
|
|
2690
|
+
} catch (error) {
|
|
2691
|
+
if (!missing(error))
|
|
2692
|
+
throw error;
|
|
2693
|
+
try {
|
|
2694
|
+
mkdirSync3(at, { mode: 448 });
|
|
2695
|
+
} catch (mkdirError) {
|
|
2696
|
+
if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
|
|
2697
|
+
throw mkdirError;
|
|
2698
|
+
}
|
|
2699
|
+
st = lstatSync2(at);
|
|
2700
|
+
}
|
|
2701
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2702
|
+
throw new Error("Unsafe directory");
|
|
2703
|
+
privateMode(st, this.ownerOnly);
|
|
2704
|
+
}
|
|
2705
|
+
return true;
|
|
2706
|
+
}
|
|
2707
|
+
read(path2, maximum = METADATA_BYTES, walks) {
|
|
2708
|
+
if (!this.directory(dirname2(path2), false, walks))
|
|
2709
|
+
return;
|
|
2710
|
+
let before;
|
|
2711
|
+
try {
|
|
2712
|
+
before = lstatSync2(path2);
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
if (missing(error))
|
|
2715
|
+
return;
|
|
2716
|
+
return refused();
|
|
2717
|
+
}
|
|
2718
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2719
|
+
refused();
|
|
2720
|
+
privateMode(before, this.ownerOnly);
|
|
2721
|
+
const fd = openSync3(path2, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
|
|
2722
|
+
try {
|
|
2723
|
+
const opened = fstatSync2(fd);
|
|
2724
|
+
if (!same(before, opened))
|
|
2725
|
+
refused();
|
|
2726
|
+
const buffer = Buffer.alloc(opened.size + 1);
|
|
2727
|
+
let size = 0;
|
|
2728
|
+
while (size < buffer.length) {
|
|
2729
|
+
const n = readSync2(fd, buffer, size, buffer.length - size, null);
|
|
2730
|
+
if (!n)
|
|
2731
|
+
break;
|
|
2732
|
+
size += n;
|
|
2733
|
+
}
|
|
2734
|
+
const bytes = buffer.subarray(0, size);
|
|
2735
|
+
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync2(path2)))
|
|
2736
|
+
refused();
|
|
2737
|
+
return { bytes, identity: hash(bytes), stamp: stamp(opened) };
|
|
2738
|
+
} finally {
|
|
2739
|
+
closeSync3(fd);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
assert(path2, expected, maximum = METADATA_BYTES, walks) {
|
|
2743
|
+
const current = this.read(path2, maximum, walks);
|
|
2744
|
+
if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
|
|
2745
|
+
throw new ContractError("conflict");
|
|
2746
|
+
}
|
|
2747
|
+
create(path2, bytes, maximum = METADATA_BYTES) {
|
|
2748
|
+
if (Buffer.byteLength(bytes) > maximum)
|
|
2749
|
+
throw new ContractError("oversize");
|
|
2750
|
+
this.directory(dirname2(path2), true);
|
|
2751
|
+
const fd = retryWindowsFileOperation(() => openSync3(path2, "wx", 384));
|
|
2752
|
+
try {
|
|
2753
|
+
writeFileSync2(fd, bytes);
|
|
2754
|
+
fsyncSync2(fd);
|
|
2755
|
+
} finally {
|
|
2756
|
+
closeSync3(fd);
|
|
2757
|
+
}
|
|
2758
|
+
this.syncDirectory(dirname2(path2));
|
|
2759
|
+
const result = this.read(path2, maximum);
|
|
2760
|
+
if (!result || result.identity !== hash(bytes))
|
|
2761
|
+
refused();
|
|
2762
|
+
return result;
|
|
2763
|
+
}
|
|
2764
|
+
replace(path2, expected, bytes, maximum = METADATA_BYTES) {
|
|
2765
|
+
this.assert(path2, expected, maximum);
|
|
2766
|
+
const temporary = join2(dirname2(path2), `pending-${randomUUID2()}.json`);
|
|
2767
|
+
this.create(temporary, bytes, maximum);
|
|
2768
|
+
this.assert(path2, expected, maximum);
|
|
2769
|
+
retryWindowsFileOperation(() => renameSync2(temporary, path2), () => this.assert(path2, expected, maximum));
|
|
2770
|
+
this.syncDirectory(dirname2(path2));
|
|
2771
|
+
const result = this.read(path2, maximum);
|
|
2772
|
+
if (!result || result.identity !== hash(bytes))
|
|
2773
|
+
refused();
|
|
2774
|
+
return result;
|
|
2775
|
+
}
|
|
2776
|
+
remove(path2, expected, maximum = METADATA_BYTES) {
|
|
2777
|
+
this.assert(path2, expected, maximum);
|
|
2778
|
+
retryWindowsFileOperation(() => unlinkSync2(path2), () => this.assert(path2, expected, maximum));
|
|
2779
|
+
this.syncDirectory(dirname2(path2));
|
|
2780
|
+
}
|
|
2781
|
+
syncDirectory(path2) {
|
|
2782
|
+
if (process.platform === "win32")
|
|
2783
|
+
return;
|
|
2784
|
+
const fd = openSync3(path2, constants3.O_RDONLY | constants3.O_DIRECTORY);
|
|
2785
|
+
try {
|
|
2786
|
+
fsyncSync2(fd);
|
|
2787
|
+
} finally {
|
|
2788
|
+
closeSync3(fd);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
function stamp(st) {
|
|
2793
|
+
return [
|
|
2794
|
+
st.dev,
|
|
2795
|
+
st.ino,
|
|
2796
|
+
st.size,
|
|
2797
|
+
st.mode,
|
|
2798
|
+
st.uid,
|
|
2799
|
+
st.nlink,
|
|
2800
|
+
st.ctimeMs,
|
|
2801
|
+
st.mtimeMs
|
|
2802
|
+
].join(":");
|
|
2803
|
+
}
|
|
2804
|
+
function json(snapshot, maximum = METADATA_BYTES) {
|
|
2805
|
+
try {
|
|
2806
|
+
return parseWireJson(new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes), maximum);
|
|
2807
|
+
} catch {
|
|
2808
|
+
return refused();
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
function encoded(value) {
|
|
2812
|
+
return `${JSON.stringify(value)}
|
|
2813
|
+
`;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// src/service/server-config.ts
|
|
2817
|
+
var DEFAULT_PORT = 17423;
|
|
2818
|
+
var PORT_KEY = "CADENCE_PORT";
|
|
2819
|
+
var TOKEN_KEY = "CADENCE_TOKEN";
|
|
2820
|
+
var CONFIG_BYTES = 64 * 1024;
|
|
2821
|
+
var HEADER = [
|
|
2822
|
+
"# Cadence WebUI for `cadence server install`: edit, then run `cadence server restart`.",
|
|
2823
|
+
"# The token admits anyone who has it to your workspace; keep it private."
|
|
2824
|
+
];
|
|
2825
|
+
var TOKEN_BYTES = 1024;
|
|
2826
|
+
function accessToken(value) {
|
|
2827
|
+
const result = text(value, TOKEN_BYTES, true);
|
|
2828
|
+
if (utf8Bytes(result) < 32 || /[\p{Cc}\p{Cf}]/u.test(result))
|
|
2829
|
+
throw new ContractError("invalid");
|
|
2830
|
+
return result;
|
|
2831
|
+
}
|
|
2832
|
+
function serverConfigPath(home) {
|
|
2833
|
+
return join3(home, ".pi", "agent", "cadence", ".env");
|
|
2834
|
+
}
|
|
2835
|
+
function settings(source) {
|
|
2836
|
+
parseEnvFile(source);
|
|
2837
|
+
const parts = source.split(/(\r\n|\r|\n)/);
|
|
2838
|
+
const found = {};
|
|
2839
|
+
for (let at = 0;at < parts.length; at += 2) {
|
|
2840
|
+
const line = parts[at].trim();
|
|
2841
|
+
if (!line || line.startsWith("#"))
|
|
2842
|
+
continue;
|
|
2843
|
+
const separator = line.indexOf("=");
|
|
2844
|
+
const key = line.slice(0, separator).trim();
|
|
2845
|
+
const name = key === PORT_KEY ? "port" : key === TOKEN_KEY ? "token" : undefined;
|
|
2846
|
+
if (!name)
|
|
2847
|
+
continue;
|
|
2848
|
+
if (found[name])
|
|
2849
|
+
throw new ContractError("invalid");
|
|
2850
|
+
let value = line.slice(separator + 1).trim();
|
|
2851
|
+
if (value[0] === '"' || value[0] === "'")
|
|
2852
|
+
value = value.slice(1, -1);
|
|
2853
|
+
found[name] = { at, value };
|
|
2854
|
+
}
|
|
2855
|
+
return { parts, ...found };
|
|
2856
|
+
}
|
|
2857
|
+
function decodeServerConfig(source) {
|
|
2858
|
+
const { port, token } = settings(source);
|
|
2859
|
+
if (!port || !/^[1-9][0-9]{0,4}$/.test(port.value))
|
|
2860
|
+
throw new ContractError("invalid");
|
|
2861
|
+
if (!token)
|
|
2862
|
+
throw new ContractError("invalid");
|
|
2863
|
+
return {
|
|
2864
|
+
port: integer(Number(port.value), 1, 65535),
|
|
2865
|
+
token: accessToken(token.value)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
class ServerConfigRefusal extends ContractError {
|
|
2870
|
+
path;
|
|
2871
|
+
constructor(path2) {
|
|
2872
|
+
super("corrupt-selected");
|
|
2873
|
+
this.path = path2;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
function decoded(snapshot) {
|
|
2877
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes);
|
|
2878
|
+
}
|
|
2879
|
+
function readServerConfig(path2) {
|
|
2880
|
+
try {
|
|
2881
|
+
const files = new PrivateFiles(dirname3(path2));
|
|
2882
|
+
const snapshot = files.read(path2, CONFIG_BYTES);
|
|
2883
|
+
if (!snapshot)
|
|
2884
|
+
throw new ContractError("corrupt-selected");
|
|
2885
|
+
return {
|
|
2886
|
+
path: path2,
|
|
2887
|
+
config: decodeServerConfig(decoded(snapshot)),
|
|
2888
|
+
files,
|
|
2889
|
+
snapshot
|
|
2890
|
+
};
|
|
2891
|
+
} catch {
|
|
2892
|
+
throw new ServerConfigRefusal(path2);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function ensureServerConfig(home) {
|
|
2896
|
+
const path2 = serverConfigPath(home);
|
|
2897
|
+
const added = [];
|
|
2898
|
+
try {
|
|
2899
|
+
ensureUserConfig(home);
|
|
2900
|
+
const files = new PrivateFiles(dirname3(path2));
|
|
2901
|
+
const snapshot = files.read(path2, CONFIG_BYTES);
|
|
2902
|
+
if (!snapshot)
|
|
2903
|
+
throw new ContractError("corrupt-selected");
|
|
2904
|
+
const source = decoded(snapshot);
|
|
2905
|
+
const found = settings(source);
|
|
2906
|
+
const lines = [];
|
|
2907
|
+
if (!found.port)
|
|
2908
|
+
lines.push(`${PORT_KEY}=${DEFAULT_PORT}`);
|
|
2909
|
+
if (!found.token)
|
|
2910
|
+
lines.push(`${TOKEN_KEY}=${randomBytes(32).toString("base64url")}`);
|
|
2911
|
+
if (lines.length) {
|
|
2912
|
+
const eol = source.includes(`\r
|
|
2913
|
+
`) ? `\r
|
|
2914
|
+
` : `
|
|
2915
|
+
`;
|
|
2916
|
+
const ending = !source || /[\r\n]$/.test(source) ? "" : eol;
|
|
2917
|
+
const gap = source ? eol : "";
|
|
2918
|
+
files.replace(path2, snapshot, `${source}${ending}${gap}${[...HEADER, ...lines].join(eol)}${eol}`, CONFIG_BYTES);
|
|
2919
|
+
added.push(...lines.map((line) => line.slice(0, line.indexOf("="))));
|
|
2920
|
+
}
|
|
2921
|
+
} catch {
|
|
2922
|
+
throw new ServerConfigRefusal(path2);
|
|
2923
|
+
}
|
|
2924
|
+
return { ...readServerConfig(path2), added };
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// src/service/autostart.ts
|
|
2496
2928
|
var UNIT = "cadence.service";
|
|
2497
2929
|
var MARKER = "# Generated by @abelxiaoxing/cadence";
|
|
2498
2930
|
function wscript(env) {
|
|
@@ -2511,7 +2943,7 @@ function hostSystem(root, owner) {
|
|
|
2511
2943
|
return {
|
|
2512
2944
|
platform: process.platform,
|
|
2513
2945
|
env: process.env,
|
|
2514
|
-
home:
|
|
2946
|
+
home: homedir3(),
|
|
2515
2947
|
node: process.execPath,
|
|
2516
2948
|
root,
|
|
2517
2949
|
unit: UNIT,
|
|
@@ -2545,8 +2977,8 @@ function hostSystem(root, owner) {
|
|
|
2545
2977
|
},
|
|
2546
2978
|
launchctl: (args) => run("launchctl", args),
|
|
2547
2979
|
reg: (args) => run("reg", args),
|
|
2548
|
-
launch: (
|
|
2549
|
-
const r = spawnSync2(wscript(process.env), [
|
|
2980
|
+
launch: (path2) => {
|
|
2981
|
+
const r = spawnSync2(wscript(process.env), [path2], {
|
|
2550
2982
|
stdio: "ignore",
|
|
2551
2983
|
windowsHide: true,
|
|
2552
2984
|
timeout: 20000
|
|
@@ -2559,15 +2991,15 @@ function hostSystem(root, owner) {
|
|
|
2559
2991
|
function expandHome(value, home) {
|
|
2560
2992
|
if (value === "~")
|
|
2561
2993
|
return home;
|
|
2562
|
-
return value.startsWith("~/") ?
|
|
2994
|
+
return value.startsWith("~/") ? join4(home, value.slice(2)) : value;
|
|
2563
2995
|
}
|
|
2564
2996
|
function agentDirectory(s) {
|
|
2565
2997
|
const selected = s.env.PI_CODING_AGENT_DIR;
|
|
2566
|
-
return
|
|
2998
|
+
return resolve3(selected ? expandHome(selected, s.home) : join4(s.home, ".pi", "agent"));
|
|
2567
2999
|
}
|
|
2568
3000
|
function piUserInstall(s) {
|
|
2569
3001
|
try {
|
|
2570
|
-
const managed =
|
|
3002
|
+
const managed = join4(agentDirectory(s), "npm", "node_modules", "@abelxiaoxing", "cadence");
|
|
2571
3003
|
return realpathSync2(managed) === realpathSync2(s.root);
|
|
2572
3004
|
} catch {
|
|
2573
3005
|
return false;
|
|
@@ -2576,18 +3008,18 @@ function piUserInstall(s) {
|
|
|
2576
3008
|
function sdkPackages(root) {
|
|
2577
3009
|
let start;
|
|
2578
3010
|
try {
|
|
2579
|
-
start = realpathSync2(
|
|
3011
|
+
start = realpathSync2(join4(root, "src"));
|
|
2580
3012
|
} catch {
|
|
2581
3013
|
return;
|
|
2582
3014
|
}
|
|
2583
3015
|
const found = [];
|
|
2584
3016
|
for (const name of ["pi-coding-agent", "pi-ai"]) {
|
|
2585
3017
|
let hit;
|
|
2586
|
-
for (let dir = start;!hit; dir =
|
|
2587
|
-
const candidate =
|
|
3018
|
+
for (let dir = start;!hit; dir = dirname4(dir)) {
|
|
3019
|
+
const candidate = join4(dir, "node_modules", "@earendil-works", name, "package.json");
|
|
2588
3020
|
if (regularFile(candidate))
|
|
2589
3021
|
hit = candidate;
|
|
2590
|
-
else if (
|
|
3022
|
+
else if (dirname4(dir) === dir)
|
|
2591
3023
|
return;
|
|
2592
3024
|
}
|
|
2593
3025
|
found.push(hit);
|
|
@@ -2596,10 +3028,10 @@ function sdkPackages(root) {
|
|
|
2596
3028
|
}
|
|
2597
3029
|
function unitPath(s) {
|
|
2598
3030
|
const data = s.env.XDG_DATA_HOME;
|
|
2599
|
-
return
|
|
3031
|
+
return join4(data && isAbsolute3(data) ? data : join4(s.home, ".local", "share"), "systemd", "user", s.unit);
|
|
2600
3032
|
}
|
|
2601
3033
|
function servicePath(value) {
|
|
2602
|
-
const kept = (value ?? "").split(delimiter).filter((entry) =>
|
|
3034
|
+
const kept = (value ?? "").split(delimiter).filter((entry) => isAbsolute3(entry) && !entry.endsWith(`${sep3}node_modules${sep3}.bin`) && !entry.includes(`${sep3}node-gyp-bin`));
|
|
2603
3035
|
return kept.length ? [...new Set(kept)].join(delimiter) : "/usr/local/bin:/usr/bin:/bin";
|
|
2604
3036
|
}
|
|
2605
3037
|
function serviceEnvironment(s) {
|
|
@@ -2610,7 +3042,7 @@ function serviceEnvironment(s) {
|
|
|
2610
3042
|
out.push([key, value]);
|
|
2611
3043
|
}
|
|
2612
3044
|
const state = s.env.XDG_STATE_HOME;
|
|
2613
|
-
if (state &&
|
|
3045
|
+
if (state && isAbsolute3(state))
|
|
2614
3046
|
out.push(["XDG_STATE_HOME", state]);
|
|
2615
3047
|
if (s.env.PI_CODING_AGENT_DIR)
|
|
2616
3048
|
out.push(["PI_CODING_AGENT_DIR", agentDirectory(s)]);
|
|
@@ -2633,8 +3065,8 @@ function buildUnit(u) {
|
|
|
2633
3065
|
"Description=Cadence shared task service and WebUI",
|
|
2634
3066
|
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
2635
3067
|
`ConditionPathExists=${specifiers(u.cli)}`,
|
|
2636
|
-
`ConditionPathExists=${specifiers(
|
|
2637
|
-
...u.sdk.map((
|
|
3068
|
+
`ConditionPathExists=${specifiers(join4(u.agentDir, "auth.json"))}`,
|
|
3069
|
+
...u.sdk.map((path2) => `ConditionPathExists=${specifiers(path2)}`),
|
|
2638
3070
|
"StartLimitIntervalSec=300",
|
|
2639
3071
|
"StartLimitBurst=3",
|
|
2640
3072
|
"",
|
|
@@ -2642,7 +3074,7 @@ function buildUnit(u) {
|
|
|
2642
3074
|
"Type=simple",
|
|
2643
3075
|
`WorkingDirectory=${specifiers(u.workspace)}`,
|
|
2644
3076
|
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
2645
|
-
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace])}`,
|
|
3077
|
+
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace, "--server-config", u.serverConfig])}`,
|
|
2646
3078
|
`ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
|
|
2647
3079
|
"KillMode=mixed",
|
|
2648
3080
|
"TimeoutStopSec=90",
|
|
@@ -2655,11 +3087,11 @@ function buildUnit(u) {
|
|
|
2655
3087
|
].join(`
|
|
2656
3088
|
`);
|
|
2657
3089
|
}
|
|
2658
|
-
function readGenerated(
|
|
3090
|
+
function readGenerated(path2, marker) {
|
|
2659
3091
|
try {
|
|
2660
|
-
if (!
|
|
3092
|
+
if (!lstatSync3(path2).isFile())
|
|
2661
3093
|
return null;
|
|
2662
|
-
const bytes = readFileSync(
|
|
3094
|
+
const bytes = readFileSync(path2);
|
|
2663
3095
|
const text2 = bytes[0] === 255 && bytes[1] === 254 ? bytes.subarray(2).toString("utf16le") : bytes.toString("utf8");
|
|
2664
3096
|
return text2.startsWith(marker) ? text2 : null;
|
|
2665
3097
|
} catch (error) {
|
|
@@ -2671,16 +3103,16 @@ function readGenerated(path, marker) {
|
|
|
2671
3103
|
function unitWorkspace(text2) {
|
|
2672
3104
|
return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
|
|
2673
3105
|
}
|
|
2674
|
-
function directory(
|
|
3106
|
+
function directory(path2) {
|
|
2675
3107
|
try {
|
|
2676
|
-
return
|
|
3108
|
+
return isAbsolute3(path2) && statSync(path2).isDirectory();
|
|
2677
3109
|
} catch {
|
|
2678
3110
|
return false;
|
|
2679
3111
|
}
|
|
2680
3112
|
}
|
|
2681
3113
|
function serviceStateHome(s) {
|
|
2682
3114
|
const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
|
|
2683
|
-
return state &&
|
|
3115
|
+
return state && isAbsolute3(state) ? state : join4(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
|
|
2684
3116
|
}
|
|
2685
3117
|
function selectWorkspace(s, requested, kept) {
|
|
2686
3118
|
let chosen;
|
|
@@ -2688,27 +3120,27 @@ function selectWorkspace(s, requested, kept) {
|
|
|
2688
3120
|
chosen = expandHome(requested, s.home);
|
|
2689
3121
|
if (!directory(chosen))
|
|
2690
3122
|
throw new ContractError("invalid");
|
|
2691
|
-
chosen =
|
|
3123
|
+
chosen = resolve3(chosen);
|
|
2692
3124
|
} else if (kept && directory(kept))
|
|
2693
3125
|
chosen = kept;
|
|
2694
|
-
const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace:
|
|
3126
|
+
const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join4(s.home, LANDING_WORKSPACE), redirected: false };
|
|
2695
3127
|
if (landing.workspace !== chosen)
|
|
2696
|
-
|
|
3128
|
+
mkdirSync4(landing.workspace, { recursive: true });
|
|
2697
3129
|
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
2698
3130
|
}
|
|
2699
|
-
function write(
|
|
2700
|
-
|
|
2701
|
-
const temporary = `${
|
|
3131
|
+
function write(path2, content) {
|
|
3132
|
+
mkdirSync4(dirname4(path2), { recursive: true });
|
|
3133
|
+
const temporary = `${path2}.${process.pid}.tmp`;
|
|
2702
3134
|
try {
|
|
2703
|
-
|
|
2704
|
-
|
|
3135
|
+
writeFileSync3(temporary, content, { mode: 420, flag: "wx" });
|
|
3136
|
+
renameSync3(temporary, path2);
|
|
2705
3137
|
} finally {
|
|
2706
3138
|
rmSync(temporary, { force: true });
|
|
2707
3139
|
}
|
|
2708
3140
|
}
|
|
2709
|
-
function regularFile(
|
|
3141
|
+
function regularFile(path2) {
|
|
2710
3142
|
try {
|
|
2711
|
-
return statSync(
|
|
3143
|
+
return statSync(path2).isFile();
|
|
2712
3144
|
} catch {
|
|
2713
3145
|
return false;
|
|
2714
3146
|
}
|
|
@@ -2741,10 +3173,10 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
2741
3173
|
return { status: "unsupported" };
|
|
2742
3174
|
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2743
3175
|
return { status: "unavailable" };
|
|
2744
|
-
const
|
|
2745
|
-
const previous = readGenerated(
|
|
3176
|
+
const path2 = unitPath(s);
|
|
3177
|
+
const previous = readGenerated(path2, MARKER);
|
|
2746
3178
|
if (previous === null)
|
|
2747
|
-
return { status: "foreign", unit:
|
|
3179
|
+
return { status: "foreign", unit: path2 };
|
|
2748
3180
|
if (mode === "restart" && previous === undefined)
|
|
2749
3181
|
return { status: "absent" };
|
|
2750
3182
|
const sdk = sdkPackages(s.root);
|
|
@@ -2754,8 +3186,9 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
2754
3186
|
const agentDir = agentDirectory(s);
|
|
2755
3187
|
const content = buildUnit({
|
|
2756
3188
|
node: s.node,
|
|
2757
|
-
cli:
|
|
3189
|
+
cli: join4(s.root, "src", "service-cli.mjs"),
|
|
2758
3190
|
workspace,
|
|
3191
|
+
serverConfig: serverConfigPath(s.home),
|
|
2759
3192
|
agentDir,
|
|
2760
3193
|
sdk,
|
|
2761
3194
|
environment: serviceEnvironment(s)
|
|
@@ -2764,10 +3197,10 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
2764
3197
|
const active = s.systemctl(["is-active", "--quiet", s.unit]).status === 0;
|
|
2765
3198
|
if (active)
|
|
2766
3199
|
s.systemctl(["stop", s.unit]);
|
|
2767
|
-
write(
|
|
3200
|
+
write(path2, content);
|
|
2768
3201
|
s.systemctl(["daemon-reload"]);
|
|
2769
3202
|
const result = {
|
|
2770
|
-
unit:
|
|
3203
|
+
unit: path2,
|
|
2771
3204
|
workspace,
|
|
2772
3205
|
...redirectedFrom ? { redirectedFrom } : {}
|
|
2773
3206
|
};
|
|
@@ -2776,7 +3209,7 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
2776
3209
|
if (mode === "install" && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
2777
3210
|
return { status: "failed", ...result };
|
|
2778
3211
|
const boot = () => mode === "install" ? bootStart(s, linger) : {};
|
|
2779
|
-
if (!regularFile(
|
|
3212
|
+
if (!regularFile(join4(agentDir, "auth.json")))
|
|
2780
3213
|
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
2781
3214
|
s.systemctl(["reset-failed", s.unit]);
|
|
2782
3215
|
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
@@ -2785,309 +3218,61 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
2785
3218
|
function stopAutostart(s) {
|
|
2786
3219
|
if (s.platform !== "linux")
|
|
2787
3220
|
return { status: "unsupported" };
|
|
2788
|
-
const
|
|
2789
|
-
const previous = readGenerated(
|
|
3221
|
+
const path2 = unitPath(s);
|
|
3222
|
+
const previous = readGenerated(path2, MARKER);
|
|
2790
3223
|
if (previous === null)
|
|
2791
|
-
return { status: "foreign", unit:
|
|
3224
|
+
return { status: "foreign", unit: path2 };
|
|
2792
3225
|
if (previous === undefined)
|
|
2793
3226
|
return { status: "absent" };
|
|
2794
3227
|
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2795
|
-
return { status: "unavailable", unit:
|
|
3228
|
+
return { status: "unavailable", unit: path2 };
|
|
2796
3229
|
return {
|
|
2797
3230
|
status: s.systemctl(["stop", s.unit]).status === 0 ? "stopped" : "failed",
|
|
2798
|
-
unit:
|
|
3231
|
+
unit: path2,
|
|
2799
3232
|
workspace: unitWorkspace(previous)
|
|
2800
3233
|
};
|
|
2801
3234
|
}
|
|
2802
3235
|
function removeAutostart(s) {
|
|
2803
3236
|
if (s.platform !== "linux")
|
|
2804
3237
|
return { status: "unsupported" };
|
|
2805
|
-
const
|
|
2806
|
-
const previous = readGenerated(
|
|
3238
|
+
const path2 = unitPath(s);
|
|
3239
|
+
const previous = readGenerated(path2, MARKER);
|
|
2807
3240
|
if (previous === null)
|
|
2808
|
-
return { status: "foreign", unit:
|
|
3241
|
+
return { status: "foreign", unit: path2 };
|
|
2809
3242
|
if (previous === undefined)
|
|
2810
3243
|
return { status: "absent" };
|
|
2811
3244
|
if (s.systemctl(["show-environment"]).status !== 0)
|
|
2812
|
-
return { status: "unavailable", unit:
|
|
3245
|
+
return { status: "unavailable", unit: path2 };
|
|
2813
3246
|
if (s.systemctl(["disable", "--now", s.unit]).status !== 0)
|
|
2814
|
-
return { status: "failed", unit:
|
|
2815
|
-
rmSync(
|
|
3247
|
+
return { status: "failed", unit: path2 };
|
|
3248
|
+
rmSync(path2);
|
|
2816
3249
|
s.systemctl(["daemon-reload"]);
|
|
2817
3250
|
s.systemctl(["reset-failed", s.unit]);
|
|
2818
|
-
return { status: "removed", unit:
|
|
3251
|
+
return { status: "removed", unit: path2 };
|
|
2819
3252
|
}
|
|
2820
3253
|
function autostartStatus(s) {
|
|
2821
3254
|
if (s.platform !== "linux")
|
|
2822
3255
|
return { status: "unsupported" };
|
|
2823
|
-
const
|
|
2824
|
-
const previous = readGenerated(
|
|
3256
|
+
const path2 = unitPath(s);
|
|
3257
|
+
const previous = readGenerated(path2, MARKER);
|
|
2825
3258
|
if (previous === null)
|
|
2826
|
-
return { status: "foreign", unit:
|
|
3259
|
+
return { status: "foreign", unit: path2 };
|
|
2827
3260
|
if (previous === undefined)
|
|
2828
3261
|
return { status: "absent" };
|
|
2829
3262
|
return {
|
|
2830
3263
|
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
2831
3264
|
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
2832
|
-
unit:
|
|
3265
|
+
unit: path2,
|
|
2833
3266
|
workspace: unitWorkspace(previous),
|
|
2834
3267
|
...bootStart(s, false)
|
|
2835
3268
|
};
|
|
2836
3269
|
}
|
|
2837
3270
|
|
|
2838
3271
|
// src/service/coordination.ts
|
|
2839
|
-
import { randomBytes, randomUUID as randomUUID3 } from "node:crypto";
|
|
3272
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
2840
3273
|
import { opendirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
2841
3274
|
import { hostname as hostname2 } from "node:os";
|
|
2842
|
-
import { dirname as
|
|
2843
|
-
|
|
2844
|
-
// src/service/private-files.ts
|
|
2845
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
2846
|
-
import {
|
|
2847
|
-
closeSync as closeSync2,
|
|
2848
|
-
constants as constants2,
|
|
2849
|
-
fstatSync as fstatSync2,
|
|
2850
|
-
fsyncSync as fsyncSync2,
|
|
2851
|
-
lstatSync as lstatSync3,
|
|
2852
|
-
mkdirSync as mkdirSync3,
|
|
2853
|
-
openSync as openSync2,
|
|
2854
|
-
readSync as readSync2,
|
|
2855
|
-
renameSync as renameSync3,
|
|
2856
|
-
unlinkSync as unlinkSync2,
|
|
2857
|
-
writeFileSync as writeFileSync3
|
|
2858
|
-
} from "node:fs";
|
|
2859
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
2860
|
-
var METADATA_BYTES = 16 * 1024;
|
|
2861
|
-
var REGISTRY_BYTES = 1024 * 1024;
|
|
2862
|
-
var REGISTRY_ENTRIES = 128;
|
|
2863
|
-
function refused() {
|
|
2864
|
-
throw new ContractError("corrupt-selected");
|
|
2865
|
-
}
|
|
2866
|
-
function hash(bytes) {
|
|
2867
|
-
return createHash2("sha256").update(bytes).digest("hex");
|
|
2868
|
-
}
|
|
2869
|
-
function privateMode(st, ownerOnly = true) {
|
|
2870
|
-
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2871
|
-
refused();
|
|
2872
|
-
}
|
|
2873
|
-
function same(a, b) {
|
|
2874
|
-
return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.ctimeMs === b.ctimeMs && a.mtimeMs === b.mtimeMs && a.mode === b.mode && a.uid === b.uid && a.nlink === b.nlink;
|
|
2875
|
-
}
|
|
2876
|
-
function missing(error) {
|
|
2877
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2878
|
-
}
|
|
2879
|
-
var job = 0;
|
|
2880
|
-
var advancing = false;
|
|
2881
|
-
function currentJob() {
|
|
2882
|
-
if (!advancing) {
|
|
2883
|
-
advancing = true;
|
|
2884
|
-
queueMicrotask(() => {
|
|
2885
|
-
job++;
|
|
2886
|
-
advancing = false;
|
|
2887
|
-
});
|
|
2888
|
-
}
|
|
2889
|
-
return job;
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
class DirectoryWalks {
|
|
2893
|
-
job = currentJob();
|
|
2894
|
-
walked = new Map;
|
|
2895
|
-
open() {
|
|
2896
|
-
if (this.job === job)
|
|
2897
|
-
return true;
|
|
2898
|
-
this.walked.clear();
|
|
2899
|
-
return false;
|
|
2900
|
-
}
|
|
2901
|
-
remember(path, st) {
|
|
2902
|
-
if (this.open())
|
|
2903
|
-
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2904
|
-
}
|
|
2905
|
-
reuse(path, ownerOnly = true) {
|
|
2906
|
-
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2907
|
-
if (!walked)
|
|
2908
|
-
return false;
|
|
2909
|
-
let st;
|
|
2910
|
-
try {
|
|
2911
|
-
st = lstatSync3(path);
|
|
2912
|
-
} catch (error) {
|
|
2913
|
-
if (!missing(error))
|
|
2914
|
-
throw error;
|
|
2915
|
-
this.walked.delete(path);
|
|
2916
|
-
return false;
|
|
2917
|
-
}
|
|
2918
|
-
if (!st.isDirectory() || st.isSymbolicLink() || st.dev !== walked.dev || st.ino !== walked.ino) {
|
|
2919
|
-
this.walked.delete(path);
|
|
2920
|
-
return false;
|
|
2921
|
-
}
|
|
2922
|
-
privateMode(st, ownerOnly);
|
|
2923
|
-
return true;
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
|
-
class PrivateFiles {
|
|
2928
|
-
namespace;
|
|
2929
|
-
ownerOnly;
|
|
2930
|
-
constructor(namespace, ownerOnly = true) {
|
|
2931
|
-
this.namespace = namespace;
|
|
2932
|
-
this.ownerOnly = ownerOnly;
|
|
2933
|
-
}
|
|
2934
|
-
directory(path, create = false, walks) {
|
|
2935
|
-
const tail = relative2(this.namespace, path);
|
|
2936
|
-
if (isAbsolute3(tail) || tail === ".." || tail.startsWith(`..${sep3}`))
|
|
2937
|
-
refused();
|
|
2938
|
-
if (!create) {
|
|
2939
|
-
const full = resolve3(path);
|
|
2940
|
-
if (walks?.reuse(full, this.ownerOnly))
|
|
2941
|
-
return true;
|
|
2942
|
-
const namespace = resolve3(this.namespace);
|
|
2943
|
-
let inside = false, last;
|
|
2944
|
-
const found = safeDirectory(path, false, (at2, st) => {
|
|
2945
|
-
if (at2 === namespace)
|
|
2946
|
-
inside = true;
|
|
2947
|
-
if (inside)
|
|
2948
|
-
privateMode(st, this.ownerOnly);
|
|
2949
|
-
last = st;
|
|
2950
|
-
});
|
|
2951
|
-
if (found && last)
|
|
2952
|
-
walks?.remember(full, last);
|
|
2953
|
-
return found;
|
|
2954
|
-
}
|
|
2955
|
-
if (!safeDirectory(this.namespace, true))
|
|
2956
|
-
return false;
|
|
2957
|
-
privateMode(lstatSync3(this.namespace), this.ownerOnly);
|
|
2958
|
-
let at = this.namespace;
|
|
2959
|
-
for (const part of tail.split(sep3).filter(Boolean)) {
|
|
2960
|
-
at = join3(at, part);
|
|
2961
|
-
let st;
|
|
2962
|
-
try {
|
|
2963
|
-
st = lstatSync3(at);
|
|
2964
|
-
} catch (error) {
|
|
2965
|
-
if (!missing(error))
|
|
2966
|
-
throw error;
|
|
2967
|
-
try {
|
|
2968
|
-
mkdirSync3(at, { mode: 448 });
|
|
2969
|
-
} catch (mkdirError) {
|
|
2970
|
-
if (!(mkdirError instanceof Error && ("code" in mkdirError) && mkdirError.code === "EEXIST"))
|
|
2971
|
-
throw mkdirError;
|
|
2972
|
-
}
|
|
2973
|
-
st = lstatSync3(at);
|
|
2974
|
-
}
|
|
2975
|
-
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2976
|
-
throw new Error("Unsafe directory");
|
|
2977
|
-
privateMode(st, this.ownerOnly);
|
|
2978
|
-
}
|
|
2979
|
-
return true;
|
|
2980
|
-
}
|
|
2981
|
-
read(path, maximum = METADATA_BYTES, walks) {
|
|
2982
|
-
if (!this.directory(dirname3(path), false, walks))
|
|
2983
|
-
return;
|
|
2984
|
-
let before;
|
|
2985
|
-
try {
|
|
2986
|
-
before = lstatSync3(path);
|
|
2987
|
-
} catch (error) {
|
|
2988
|
-
if (missing(error))
|
|
2989
|
-
return;
|
|
2990
|
-
return refused();
|
|
2991
|
-
}
|
|
2992
|
-
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2993
|
-
refused();
|
|
2994
|
-
privateMode(before, this.ownerOnly);
|
|
2995
|
-
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2996
|
-
try {
|
|
2997
|
-
const opened = fstatSync2(fd);
|
|
2998
|
-
if (!same(before, opened))
|
|
2999
|
-
refused();
|
|
3000
|
-
const buffer = Buffer.alloc(opened.size + 1);
|
|
3001
|
-
let size = 0;
|
|
3002
|
-
while (size < buffer.length) {
|
|
3003
|
-
const n = readSync2(fd, buffer, size, buffer.length - size, null);
|
|
3004
|
-
if (!n)
|
|
3005
|
-
break;
|
|
3006
|
-
size += n;
|
|
3007
|
-
}
|
|
3008
|
-
const bytes = buffer.subarray(0, size);
|
|
3009
|
-
if (size !== opened.size || !same(opened, fstatSync2(fd)) || !same(opened, lstatSync3(path)))
|
|
3010
|
-
refused();
|
|
3011
|
-
return { bytes, identity: hash(bytes), stamp: stamp(opened) };
|
|
3012
|
-
} finally {
|
|
3013
|
-
closeSync2(fd);
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
assert(path, expected, maximum = METADATA_BYTES, walks) {
|
|
3017
|
-
const current = this.read(path, maximum, walks);
|
|
3018
|
-
if (current?.identity !== expected?.identity || current?.stamp !== expected?.stamp)
|
|
3019
|
-
throw new ContractError("conflict");
|
|
3020
|
-
}
|
|
3021
|
-
create(path, bytes, maximum = METADATA_BYTES) {
|
|
3022
|
-
if (Buffer.byteLength(bytes) > maximum)
|
|
3023
|
-
throw new ContractError("oversize");
|
|
3024
|
-
this.directory(dirname3(path), true);
|
|
3025
|
-
const fd = retryWindowsFileOperation(() => openSync2(path, "wx", 384));
|
|
3026
|
-
try {
|
|
3027
|
-
writeFileSync3(fd, bytes);
|
|
3028
|
-
fsyncSync2(fd);
|
|
3029
|
-
} finally {
|
|
3030
|
-
closeSync2(fd);
|
|
3031
|
-
}
|
|
3032
|
-
this.syncDirectory(dirname3(path));
|
|
3033
|
-
const result = this.read(path, maximum);
|
|
3034
|
-
if (!result || result.identity !== hash(bytes))
|
|
3035
|
-
refused();
|
|
3036
|
-
return result;
|
|
3037
|
-
}
|
|
3038
|
-
replace(path, expected, bytes, maximum = METADATA_BYTES) {
|
|
3039
|
-
this.assert(path, expected, maximum);
|
|
3040
|
-
const temporary = join3(dirname3(path), `pending-${randomUUID2()}.json`);
|
|
3041
|
-
this.create(temporary, bytes, maximum);
|
|
3042
|
-
this.assert(path, expected, maximum);
|
|
3043
|
-
retryWindowsFileOperation(() => renameSync3(temporary, path), () => this.assert(path, expected, maximum));
|
|
3044
|
-
this.syncDirectory(dirname3(path));
|
|
3045
|
-
const result = this.read(path, maximum);
|
|
3046
|
-
if (!result || result.identity !== hash(bytes))
|
|
3047
|
-
refused();
|
|
3048
|
-
return result;
|
|
3049
|
-
}
|
|
3050
|
-
remove(path, expected, maximum = METADATA_BYTES) {
|
|
3051
|
-
this.assert(path, expected, maximum);
|
|
3052
|
-
retryWindowsFileOperation(() => unlinkSync2(path), () => this.assert(path, expected, maximum));
|
|
3053
|
-
this.syncDirectory(dirname3(path));
|
|
3054
|
-
}
|
|
3055
|
-
syncDirectory(path) {
|
|
3056
|
-
if (process.platform === "win32")
|
|
3057
|
-
return;
|
|
3058
|
-
const fd = openSync2(path, constants2.O_RDONLY | constants2.O_DIRECTORY);
|
|
3059
|
-
try {
|
|
3060
|
-
fsyncSync2(fd);
|
|
3061
|
-
} finally {
|
|
3062
|
-
closeSync2(fd);
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
function stamp(st) {
|
|
3067
|
-
return [
|
|
3068
|
-
st.dev,
|
|
3069
|
-
st.ino,
|
|
3070
|
-
st.size,
|
|
3071
|
-
st.mode,
|
|
3072
|
-
st.uid,
|
|
3073
|
-
st.nlink,
|
|
3074
|
-
st.ctimeMs,
|
|
3075
|
-
st.mtimeMs
|
|
3076
|
-
].join(":");
|
|
3077
|
-
}
|
|
3078
|
-
function json(snapshot, maximum = METADATA_BYTES) {
|
|
3079
|
-
try {
|
|
3080
|
-
return parseWireJson(new TextDecoder("utf-8", { fatal: true }).decode(snapshot.bytes), maximum);
|
|
3081
|
-
} catch {
|
|
3082
|
-
return refused();
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
function encoded(value) {
|
|
3086
|
-
return `${JSON.stringify(value)}
|
|
3087
|
-
`;
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
// src/service/coordination.ts
|
|
3275
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join5, relative as relative3, sep as sep4 } from "node:path";
|
|
3091
3276
|
var registrationKeys = [
|
|
3092
3277
|
"schema",
|
|
3093
3278
|
"root",
|
|
@@ -3103,10 +3288,10 @@ var registrationKeys = [
|
|
|
3103
3288
|
];
|
|
3104
3289
|
var reservationKeys = registrationKeys.filter((k) => !["schema", "phase", "checkoutToken"].includes(k));
|
|
3105
3290
|
function pathValue(value) {
|
|
3106
|
-
const
|
|
3107
|
-
if (!isAbsolute4(
|
|
3291
|
+
const path2 = text(value, 4096, true);
|
|
3292
|
+
if (!isAbsolute4(path2))
|
|
3108
3293
|
refused();
|
|
3109
|
-
return
|
|
3294
|
+
return path2;
|
|
3110
3295
|
}
|
|
3111
3296
|
function endpoint(value, allowZero = false) {
|
|
3112
3297
|
const s = text(value, 128, true);
|
|
@@ -3165,8 +3350,8 @@ function rootsOverlap(a, b) {
|
|
|
3165
3350
|
};
|
|
3166
3351
|
return within(a, b) || within(b, a);
|
|
3167
3352
|
}
|
|
3168
|
-
function names(
|
|
3169
|
-
const dir = opendirSync(
|
|
3353
|
+
function names(path2, allowed) {
|
|
3354
|
+
const dir = opendirSync(path2);
|
|
3170
3355
|
try {
|
|
3171
3356
|
let count = 0;
|
|
3172
3357
|
for (let entry = dir.readSync();entry; entry = dir.readSync()) {
|
|
@@ -3213,17 +3398,17 @@ class ServiceCoordination {
|
|
|
3213
3398
|
stateHome;
|
|
3214
3399
|
constructor(workspace, stateHome) {
|
|
3215
3400
|
this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
3216
|
-
const namespace =
|
|
3401
|
+
const namespace = dirname5(this.store.privateRoot);
|
|
3217
3402
|
this.files = new PrivateFiles(namespace);
|
|
3218
|
-
this.stateHome =
|
|
3219
|
-
this.directory =
|
|
3220
|
-
this.registryDirectory =
|
|
3403
|
+
this.stateHome = dirname5(namespace);
|
|
3404
|
+
this.directory = join5(this.store.privateRoot, "service-v1");
|
|
3405
|
+
this.registryDirectory = join5(namespace, "service-registry-v1");
|
|
3221
3406
|
}
|
|
3222
3407
|
path(name) {
|
|
3223
|
-
return
|
|
3408
|
+
return join5(this.directory, name);
|
|
3224
3409
|
}
|
|
3225
3410
|
registryPath(name) {
|
|
3226
|
-
return
|
|
3411
|
+
return join5(this.registryDirectory, name);
|
|
3227
3412
|
}
|
|
3228
3413
|
registry() {
|
|
3229
3414
|
if (!this.files.directory(this.registryDirectory))
|
|
@@ -3271,7 +3456,7 @@ class ServiceCoordination {
|
|
|
3271
3456
|
"host-config.json",
|
|
3272
3457
|
"history-v2"
|
|
3273
3458
|
]);
|
|
3274
|
-
this.files.directory(
|
|
3459
|
+
this.files.directory(join5(this.directory, "history-v2"), false, walks);
|
|
3275
3460
|
}
|
|
3276
3461
|
selected() {
|
|
3277
3462
|
const walks = new DirectoryWalks;
|
|
@@ -3431,7 +3616,7 @@ class ServiceCoordination {
|
|
|
3431
3616
|
throw error;
|
|
3432
3617
|
}
|
|
3433
3618
|
}
|
|
3434
|
-
acquire(input, resolvedEndpoint = input.endpoint, initializeNew) {
|
|
3619
|
+
acquire(input, resolvedEndpoint = input.endpoint, initializeNew, reselectExisting) {
|
|
3435
3620
|
const config = decodeOwnerConfig(input);
|
|
3436
3621
|
const bound = endpoint(resolvedEndpoint);
|
|
3437
3622
|
if (!endpointMatches(config.endpoint, bound))
|
|
@@ -3446,7 +3631,8 @@ class ServiceCoordination {
|
|
|
3446
3631
|
throw new ContractError("quarantine");
|
|
3447
3632
|
if (this.store.inspectLock())
|
|
3448
3633
|
throw new ContractError("conflict");
|
|
3449
|
-
|
|
3634
|
+
const reselect = !!old && old.config.identity !== hash(encoded(config));
|
|
3635
|
+
if (reselect && (!reselectExisting || !old?.hostConfig))
|
|
3450
3636
|
throw new ContractError("conflict");
|
|
3451
3637
|
const row = {
|
|
3452
3638
|
root: this.store.root,
|
|
@@ -3463,7 +3649,17 @@ class ServiceCoordination {
|
|
|
3463
3649
|
...registry.value,
|
|
3464
3650
|
entries: [...registry.value.entries, row]
|
|
3465
3651
|
}), REGISTRY_BYTES);
|
|
3466
|
-
|
|
3652
|
+
if (old?.hostConfig && reselect && reselectExisting) {
|
|
3653
|
+
reselectExisting(old.hostConfig);
|
|
3654
|
+
const saved = this.files.replace(this.path("config.json"), old.config, encoded(config));
|
|
3655
|
+
this.files.replace(this.path("format.json"), old.manifest, encoded({
|
|
3656
|
+
schema: "cadence-service-format/1",
|
|
3657
|
+
root: this.store.root,
|
|
3658
|
+
stateHome: this.stateHome,
|
|
3659
|
+
configIdentity: saved.identity
|
|
3660
|
+
}));
|
|
3661
|
+
} else
|
|
3662
|
+
this.initialize(config);
|
|
3467
3663
|
if (!old)
|
|
3468
3664
|
initializeNew?.();
|
|
3469
3665
|
const checkout = this.store.acquire();
|
|
@@ -3480,7 +3676,7 @@ class ServiceCoordination {
|
|
|
3480
3676
|
const capability = this.files.create(this.path("local-capability.json"), encoded({
|
|
3481
3677
|
schema: "cadence-service-local-capability/1",
|
|
3482
3678
|
epoch: row.epoch,
|
|
3483
|
-
capability:
|
|
3679
|
+
capability: randomBytes2(32).toString("base64url")
|
|
3484
3680
|
}));
|
|
3485
3681
|
const value = {
|
|
3486
3682
|
schema: "cadence-service-registration/1",
|
|
@@ -3693,12 +3889,12 @@ class OwnerLease {
|
|
|
3693
3889
|
|
|
3694
3890
|
// src/service/launchd.ts
|
|
3695
3891
|
import { rmSync as rmSync2 } from "node:fs";
|
|
3696
|
-
import { join as
|
|
3892
|
+
import { join as join6 } from "node:path";
|
|
3697
3893
|
var LABEL = "com.abelxiaoxing.cadence";
|
|
3698
3894
|
var MARKER2 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3699
3895
|
<!-- Generated by @abelxiaoxing/cadence`;
|
|
3700
3896
|
function plistPath(s) {
|
|
3701
|
-
return
|
|
3897
|
+
return join6(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
3702
3898
|
}
|
|
3703
3899
|
var xml = (value) => checked(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
3704
3900
|
function buildPlist(u) {
|
|
@@ -3713,7 +3909,16 @@ function buildPlist(u) {
|
|
|
3713
3909
|
` <string>${LABEL}</string>`,
|
|
3714
3910
|
"\t<key>ProgramArguments</key>",
|
|
3715
3911
|
"\t<array>",
|
|
3716
|
-
...[
|
|
3912
|
+
...[
|
|
3913
|
+
u.node,
|
|
3914
|
+
u.cli,
|
|
3915
|
+
"start",
|
|
3916
|
+
"--foreground",
|
|
3917
|
+
"--workspace",
|
|
3918
|
+
u.workspace,
|
|
3919
|
+
"--server-config",
|
|
3920
|
+
u.serverConfig
|
|
3921
|
+
].map(string),
|
|
3717
3922
|
"\t</array>",
|
|
3718
3923
|
"\t<key>WorkingDirectory</key>",
|
|
3719
3924
|
` <string>${xml(u.workspace)}</string>`,
|
|
@@ -3751,10 +3956,10 @@ function disabled(s) {
|
|
|
3751
3956
|
async function configureLaunchAgent(s, mode, requested) {
|
|
3752
3957
|
if (s.uid === undefined)
|
|
3753
3958
|
return { status: "unavailable" };
|
|
3754
|
-
const
|
|
3755
|
-
const previous = readGenerated(
|
|
3959
|
+
const path2 = plistPath(s);
|
|
3960
|
+
const previous = readGenerated(path2, MARKER2);
|
|
3756
3961
|
if (previous === null)
|
|
3757
|
-
return { status: "foreign", unit:
|
|
3962
|
+
return { status: "foreign", unit: path2 };
|
|
3758
3963
|
if (mode === "restart" && previous === undefined)
|
|
3759
3964
|
return { status: "absent" };
|
|
3760
3965
|
const sdk = sdkPackages(s.root);
|
|
@@ -3764,18 +3969,19 @@ async function configureLaunchAgent(s, mode, requested) {
|
|
|
3764
3969
|
const { workspace, redirectedFrom } = selectWorkspace(s, requested, kept);
|
|
3765
3970
|
const refused2 = await settle(s, kept);
|
|
3766
3971
|
if (refused2)
|
|
3767
|
-
return { ...refused2, unit:
|
|
3972
|
+
return { ...refused2, unit: path2 };
|
|
3768
3973
|
if (loaded(s))
|
|
3769
3974
|
s.launchctl(["bootout", target(s)]);
|
|
3770
|
-
write(
|
|
3975
|
+
write(path2, buildPlist({
|
|
3771
3976
|
node: s.node,
|
|
3772
|
-
cli:
|
|
3977
|
+
cli: join6(s.root, "src", "service-cli.mjs"),
|
|
3773
3978
|
workspace,
|
|
3979
|
+
serverConfig: serverConfigPath(s.home),
|
|
3774
3980
|
environment: serviceEnvironment(s),
|
|
3775
|
-
log:
|
|
3981
|
+
log: join6(s.home, "Library", "Logs", "cadence.log")
|
|
3776
3982
|
}));
|
|
3777
3983
|
const result = {
|
|
3778
|
-
unit:
|
|
3984
|
+
unit: path2,
|
|
3779
3985
|
workspace,
|
|
3780
3986
|
...redirectedFrom ? { redirectedFrom } : {}
|
|
3781
3987
|
};
|
|
@@ -3783,63 +3989,63 @@ async function configureLaunchAgent(s, mode, requested) {
|
|
|
3783
3989
|
s.launchctl(["enable", target(s)]);
|
|
3784
3990
|
else if (disabled(s))
|
|
3785
3991
|
return { status: "disabled", ...result };
|
|
3786
|
-
if (!regularFile(
|
|
3992
|
+
if (!regularFile(join6(agentDirectory(s), "auth.json")))
|
|
3787
3993
|
return { status: "waiting-for-pi-login", ...result };
|
|
3788
|
-
const started = s.launchctl(["bootstrap", domain(s),
|
|
3994
|
+
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
3789
3995
|
return { status: started ? "started" : "failed", ...result };
|
|
3790
3996
|
}
|
|
3791
3997
|
async function stopLaunchAgent(s) {
|
|
3792
|
-
const
|
|
3793
|
-
const previous = readGenerated(
|
|
3998
|
+
const path2 = plistPath(s);
|
|
3999
|
+
const previous = readGenerated(path2, MARKER2);
|
|
3794
4000
|
if (previous === null)
|
|
3795
|
-
return { status: "foreign", unit:
|
|
4001
|
+
return { status: "foreign", unit: path2 };
|
|
3796
4002
|
if (previous === undefined)
|
|
3797
4003
|
return { status: "absent" };
|
|
3798
4004
|
const workspace = plistWorkspace(previous);
|
|
3799
4005
|
const refused2 = await settle(s, workspace);
|
|
3800
|
-
return { ...refused2 ?? { status: "stopped", workspace }, unit:
|
|
4006
|
+
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
3801
4007
|
}
|
|
3802
4008
|
async function removeLaunchAgent(s) {
|
|
3803
|
-
const
|
|
3804
|
-
const previous = readGenerated(
|
|
4009
|
+
const path2 = plistPath(s);
|
|
4010
|
+
const previous = readGenerated(path2, MARKER2);
|
|
3805
4011
|
if (previous === null)
|
|
3806
|
-
return { status: "foreign", unit:
|
|
4012
|
+
return { status: "foreign", unit: path2 };
|
|
3807
4013
|
if (previous === undefined)
|
|
3808
4014
|
return { status: "absent" };
|
|
3809
4015
|
const refused2 = await settle(s, plistWorkspace(previous));
|
|
3810
4016
|
if (refused2)
|
|
3811
|
-
return { ...refused2, unit:
|
|
4017
|
+
return { ...refused2, unit: path2 };
|
|
3812
4018
|
if (s.uid !== undefined && loaded(s))
|
|
3813
4019
|
s.launchctl(["bootout", target(s)]);
|
|
3814
|
-
rmSync2(
|
|
3815
|
-
return { status: "removed", unit:
|
|
4020
|
+
rmSync2(path2);
|
|
4021
|
+
return { status: "removed", unit: path2 };
|
|
3816
4022
|
}
|
|
3817
4023
|
function launchAgentStatus(s) {
|
|
3818
|
-
const
|
|
3819
|
-
const previous = readGenerated(
|
|
4024
|
+
const path2 = plistPath(s);
|
|
4025
|
+
const previous = readGenerated(path2, MARKER2);
|
|
3820
4026
|
if (previous === null)
|
|
3821
|
-
return { status: "foreign", unit:
|
|
4027
|
+
return { status: "foreign", unit: path2 };
|
|
3822
4028
|
if (previous === undefined)
|
|
3823
4029
|
return { status: "absent" };
|
|
3824
4030
|
const printed = s.uid === undefined ? undefined : s.launchctl(["print", target(s)]);
|
|
3825
4031
|
return {
|
|
3826
4032
|
status: printed?.status !== 0 ? "unloaded" : /^\s*state = running$/m.test(printed.stdout) ? "active" : "inactive",
|
|
3827
4033
|
enabled: s.uid !== undefined && disabled(s) ? "disabled" : "enabled",
|
|
3828
|
-
unit:
|
|
4034
|
+
unit: path2,
|
|
3829
4035
|
workspace: plistWorkspace(previous)
|
|
3830
4036
|
};
|
|
3831
4037
|
}
|
|
3832
4038
|
|
|
3833
4039
|
// src/service/login-run.ts
|
|
3834
4040
|
import { rmSync as rmSync3 } from "node:fs";
|
|
3835
|
-
import { isAbsolute as isAbsolute5, join as
|
|
4041
|
+
import { isAbsolute as isAbsolute5, join as join7 } from "node:path";
|
|
3836
4042
|
var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
3837
4043
|
var APPROVED_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run";
|
|
3838
4044
|
var RUN_VALUE = "Cadence";
|
|
3839
4045
|
var MARKER3 = "' Generated by @abelxiaoxing/cadence";
|
|
3840
4046
|
function launcherPath(s) {
|
|
3841
4047
|
const local = s.env.LOCALAPPDATA;
|
|
3842
|
-
return
|
|
4048
|
+
return join7(local && isAbsolute5(local) ? local : join7(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
|
|
3843
4049
|
}
|
|
3844
4050
|
var literal2 = (value) => `"${checked(value).replaceAll('"', '""')}"`;
|
|
3845
4051
|
function argument2(value) {
|
|
@@ -3848,7 +4054,7 @@ function argument2(value) {
|
|
|
3848
4054
|
return `"${value}"`;
|
|
3849
4055
|
}
|
|
3850
4056
|
function buildLauncher(u) {
|
|
3851
|
-
const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)}`;
|
|
4057
|
+
const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)} --server-config ${argument2(u.serverConfig)}`;
|
|
3852
4058
|
return [
|
|
3853
4059
|
`${MARKER3}; \`cadence server install\` and \`restart\` rewrite this file.`,
|
|
3854
4060
|
`' Started at login by ${RUN_KEY}\\${RUN_VALUE}; window style 0 hides the console.`,
|
|
@@ -3865,7 +4071,7 @@ var utf16 = (text2) => Buffer.concat([Buffer.from([255, 254]), Buffer.from(text2
|
|
|
3865
4071
|
function launcherWorkspace(text2) {
|
|
3866
4072
|
return text2?.match(/^shell\.CurrentDirectory = "((?:[^"]|"")*)"\r?$/m)?.[1].replaceAll('""', '"');
|
|
3867
4073
|
}
|
|
3868
|
-
var runEntry = (
|
|
4074
|
+
var runEntry = (path2, s) => `"${wscript(s.env)}" "${path2}"`;
|
|
3869
4075
|
var present = (s, key) => s.reg(["query", key, "/v", RUN_VALUE]).status === 0;
|
|
3870
4076
|
function approvedOff(s) {
|
|
3871
4077
|
const r = s.reg(["query", APPROVED_KEY, "/v", RUN_VALUE]);
|
|
@@ -3873,10 +4079,10 @@ function approvedOff(s) {
|
|
|
3873
4079
|
return !!first && (Number.parseInt(first[1], 16) & 1) === 1;
|
|
3874
4080
|
}
|
|
3875
4081
|
async function configureLoginRun(s, mode, requested) {
|
|
3876
|
-
const
|
|
3877
|
-
const previous = readGenerated(
|
|
4082
|
+
const path2 = launcherPath(s);
|
|
4083
|
+
const previous = readGenerated(path2, MARKER3);
|
|
3878
4084
|
if (previous === null)
|
|
3879
|
-
return { status: "foreign", unit:
|
|
4085
|
+
return { status: "foreign", unit: path2 };
|
|
3880
4086
|
if (previous === undefined && present(s, RUN_KEY))
|
|
3881
4087
|
return { status: "foreign", unit: `${RUN_KEY}\\${RUN_VALUE}` };
|
|
3882
4088
|
if (mode === "restart" && previous === undefined)
|
|
@@ -3888,61 +4094,62 @@ async function configureLoginRun(s, mode, requested) {
|
|
|
3888
4094
|
const { workspace, redirectedFrom } = selectWorkspace(s, requested, kept);
|
|
3889
4095
|
const refused2 = await settle(s, kept);
|
|
3890
4096
|
if (refused2)
|
|
3891
|
-
return { ...refused2, unit:
|
|
4097
|
+
return { ...refused2, unit: path2 };
|
|
3892
4098
|
const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
|
|
3893
|
-
write(
|
|
4099
|
+
write(path2, utf16(buildLauncher({
|
|
3894
4100
|
node: s.node,
|
|
3895
|
-
cli:
|
|
4101
|
+
cli: join7(s.root, "src", "service-cli.mjs"),
|
|
3896
4102
|
workspace,
|
|
4103
|
+
serverConfig: serverConfigPath(s.home),
|
|
3897
4104
|
environment
|
|
3898
4105
|
})));
|
|
3899
4106
|
const result = {
|
|
3900
|
-
unit:
|
|
4107
|
+
unit: path2,
|
|
3901
4108
|
workspace,
|
|
3902
4109
|
...redirectedFrom ? { redirectedFrom } : {}
|
|
3903
4110
|
};
|
|
3904
4111
|
if (mode === "install") {
|
|
3905
|
-
const entry = ["/v", RUN_VALUE, "/t", "REG_SZ", "/d", runEntry(
|
|
4112
|
+
const entry = ["/v", RUN_VALUE, "/t", "REG_SZ", "/d", runEntry(path2, s)];
|
|
3906
4113
|
if (s.reg(["add", RUN_KEY, ...entry, "/f"]).status !== 0)
|
|
3907
4114
|
return { status: "failed", ...result };
|
|
3908
4115
|
s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
|
|
3909
4116
|
}
|
|
3910
|
-
if (!regularFile(
|
|
4117
|
+
if (!regularFile(join7(agentDirectory(s), "auth.json")))
|
|
3911
4118
|
return { status: "waiting-for-pi-login", ...result };
|
|
3912
|
-
return { status: s.launch(
|
|
4119
|
+
return { status: s.launch(path2) ? "started" : "failed", ...result };
|
|
3913
4120
|
}
|
|
3914
4121
|
async function stopLoginRun(s) {
|
|
3915
|
-
const
|
|
3916
|
-
const previous = readGenerated(
|
|
4122
|
+
const path2 = launcherPath(s);
|
|
4123
|
+
const previous = readGenerated(path2, MARKER3);
|
|
3917
4124
|
if (previous === null)
|
|
3918
|
-
return { status: "foreign", unit:
|
|
4125
|
+
return { status: "foreign", unit: path2 };
|
|
3919
4126
|
if (previous === undefined)
|
|
3920
4127
|
return { status: "absent" };
|
|
3921
4128
|
const workspace = launcherWorkspace(previous);
|
|
3922
4129
|
const refused2 = await settle(s, workspace);
|
|
3923
|
-
return { ...refused2 ?? { status: "stopped", workspace }, unit:
|
|
4130
|
+
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
3924
4131
|
}
|
|
3925
4132
|
async function removeLoginRun(s) {
|
|
3926
|
-
const
|
|
3927
|
-
const previous = readGenerated(
|
|
4133
|
+
const path2 = launcherPath(s);
|
|
4134
|
+
const previous = readGenerated(path2, MARKER3);
|
|
3928
4135
|
if (previous === null)
|
|
3929
|
-
return { status: "foreign", unit:
|
|
4136
|
+
return { status: "foreign", unit: path2 };
|
|
3930
4137
|
if (previous === undefined)
|
|
3931
4138
|
return { status: "absent" };
|
|
3932
4139
|
const refused2 = await settle(s, launcherWorkspace(previous));
|
|
3933
4140
|
if (refused2)
|
|
3934
|
-
return { ...refused2, unit:
|
|
4141
|
+
return { ...refused2, unit: path2 };
|
|
3935
4142
|
for (const key of [RUN_KEY, APPROVED_KEY])
|
|
3936
4143
|
if (present(s, key))
|
|
3937
4144
|
s.reg(["delete", key, "/v", RUN_VALUE, "/f"]);
|
|
3938
|
-
rmSync3(
|
|
3939
|
-
return { status: "removed", unit:
|
|
4145
|
+
rmSync3(path2);
|
|
4146
|
+
return { status: "removed", unit: path2 };
|
|
3940
4147
|
}
|
|
3941
4148
|
async function loginRunStatus(s) {
|
|
3942
|
-
const
|
|
3943
|
-
const previous = readGenerated(
|
|
4149
|
+
const path2 = launcherPath(s);
|
|
4150
|
+
const previous = readGenerated(path2, MARKER3);
|
|
3944
4151
|
if (previous === null)
|
|
3945
|
-
return { status: "foreign", unit:
|
|
4152
|
+
return { status: "foreign", unit: path2 };
|
|
3946
4153
|
if (previous === undefined)
|
|
3947
4154
|
return { status: "absent" };
|
|
3948
4155
|
const workspace = launcherWorkspace(previous);
|
|
@@ -3950,7 +4157,7 @@ async function loginRunStatus(s) {
|
|
|
3950
4157
|
return {
|
|
3951
4158
|
status: owner === "alive" ? "active" : "inactive",
|
|
3952
4159
|
enabled: present(s, RUN_KEY) && !approvedOff(s) ? "enabled" : "disabled",
|
|
3953
|
-
unit:
|
|
4160
|
+
unit: path2,
|
|
3954
4161
|
workspace
|
|
3955
4162
|
};
|
|
3956
4163
|
}
|
|
@@ -3961,7 +4168,7 @@ import { isatty } from "node:tty";
|
|
|
3961
4168
|
// src/history/recovery.ts
|
|
3962
4169
|
import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
|
|
3963
4170
|
import { lstatSync as lstatSync6, opendirSync as opendirSync3 } from "node:fs";
|
|
3964
|
-
import { join as
|
|
4171
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
3965
4172
|
|
|
3966
4173
|
// src/service/recovery.ts
|
|
3967
4174
|
async function inspectRecovery(coordination, history) {
|
|
@@ -4009,19 +4216,19 @@ async function resolveRecovery(coordination, confirmation, history) {
|
|
|
4009
4216
|
}
|
|
4010
4217
|
|
|
4011
4218
|
// src/history/files.ts
|
|
4012
|
-
import { createHash as createHash4, randomBytes as
|
|
4219
|
+
import { createHash as createHash4, randomBytes as randomBytes3, randomUUID as randomUUID4 } from "node:crypto";
|
|
4013
4220
|
import {
|
|
4014
|
-
closeSync as
|
|
4015
|
-
constants as
|
|
4221
|
+
closeSync as closeSync5,
|
|
4222
|
+
constants as constants5,
|
|
4016
4223
|
fstatSync as fstatSync4,
|
|
4017
4224
|
fsyncSync as fsyncSync3,
|
|
4018
4225
|
lstatSync as lstatSync5,
|
|
4019
4226
|
opendirSync as opendirSync2,
|
|
4020
|
-
openSync as
|
|
4227
|
+
openSync as openSync5,
|
|
4021
4228
|
statfsSync
|
|
4022
4229
|
} from "node:fs";
|
|
4023
4230
|
import { open } from "node:fs/promises";
|
|
4024
|
-
import { dirname as
|
|
4231
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
4025
4232
|
|
|
4026
4233
|
// src/history/admission.ts
|
|
4027
4234
|
function decodeRunAdmission(value) {
|
|
@@ -4042,44 +4249,44 @@ function decodeRunAdmission(value) {
|
|
|
4042
4249
|
|
|
4043
4250
|
// src/history/read-io.ts
|
|
4044
4251
|
import {
|
|
4045
|
-
closeSync as
|
|
4046
|
-
constants as
|
|
4252
|
+
closeSync as closeSync4,
|
|
4253
|
+
constants as constants4,
|
|
4047
4254
|
fstatSync as fstatSync3,
|
|
4048
4255
|
lstatSync as lstatSync4,
|
|
4049
|
-
openSync as
|
|
4256
|
+
openSync as openSync4,
|
|
4050
4257
|
readSync as readSync3
|
|
4051
4258
|
} from "node:fs";
|
|
4052
|
-
import { dirname as
|
|
4259
|
+
import { dirname as dirname6 } from "node:path";
|
|
4053
4260
|
function bad() {
|
|
4054
4261
|
throw new ContractError("corrupt-selected");
|
|
4055
4262
|
}
|
|
4056
4263
|
var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
|
|
4057
|
-
function selected(files,
|
|
4058
|
-
if (!files.files.directory(
|
|
4264
|
+
function selected(files, path2, read, walks) {
|
|
4265
|
+
if (!files.files.directory(dirname6(path2), false, walks))
|
|
4059
4266
|
bad();
|
|
4060
|
-
const before = lstatSync4(
|
|
4267
|
+
const before = lstatSync4(path2);
|
|
4061
4268
|
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
4062
4269
|
bad();
|
|
4063
|
-
const fd =
|
|
4270
|
+
const fd = openSync4(path2, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
4064
4271
|
try {
|
|
4065
4272
|
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
4066
4273
|
bad();
|
|
4067
4274
|
const result = read(fd, before.size, stamp2(before));
|
|
4068
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(
|
|
4275
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync4(path2)) !== stamp2(before))
|
|
4069
4276
|
bad();
|
|
4070
4277
|
return result;
|
|
4071
4278
|
} finally {
|
|
4072
|
-
|
|
4279
|
+
closeSync4(fd);
|
|
4073
4280
|
}
|
|
4074
4281
|
}
|
|
4075
|
-
function assertIncarnation(files,
|
|
4076
|
-
selected(files,
|
|
4282
|
+
function assertIncarnation(files, path2, expected) {
|
|
4283
|
+
selected(files, path2, (_fd, _size, current) => {
|
|
4077
4284
|
if (current !== expected)
|
|
4078
4285
|
bad();
|
|
4079
4286
|
});
|
|
4080
4287
|
}
|
|
4081
|
-
function statSize(files,
|
|
4082
|
-
return selected(files,
|
|
4288
|
+
function statSize(files, path2, maximum) {
|
|
4289
|
+
return selected(files, path2, (_fd, size) => {
|
|
4083
4290
|
if (size > maximum)
|
|
4084
4291
|
bad();
|
|
4085
4292
|
return size;
|
|
@@ -4552,14 +4759,14 @@ class HistoryFiles {
|
|
|
4552
4759
|
this.coordination = coordination;
|
|
4553
4760
|
this.format = format;
|
|
4554
4761
|
this.formatSnapshot = formatSnapshot;
|
|
4555
|
-
this.directory =
|
|
4762
|
+
this.directory = join8(coordination.directory, "history-v2");
|
|
4556
4763
|
this.files = coordination.files;
|
|
4557
4764
|
}
|
|
4558
4765
|
static open(coordination) {
|
|
4559
|
-
const dir =
|
|
4766
|
+
const dir = join8(coordination.directory, "history-v2");
|
|
4560
4767
|
if (!coordination.files.directory(dir))
|
|
4561
4768
|
return;
|
|
4562
|
-
const file = coordination.files.read(
|
|
4769
|
+
const file = coordination.files.read(join8(dir, "format.json"));
|
|
4563
4770
|
if (!file)
|
|
4564
4771
|
corrupt();
|
|
4565
4772
|
const format = decodeFormat(json(file));
|
|
@@ -4575,7 +4782,7 @@ class HistoryFiles {
|
|
|
4575
4782
|
existing.assertWritable();
|
|
4576
4783
|
return existing;
|
|
4577
4784
|
}
|
|
4578
|
-
const dir =
|
|
4785
|
+
const dir = join8(c.directory, "history-v2");
|
|
4579
4786
|
const format = {
|
|
4580
4787
|
schema: FORMAT,
|
|
4581
4788
|
storeId: randomUUID4(),
|
|
@@ -4585,19 +4792,19 @@ class HistoryFiles {
|
|
|
4585
4792
|
c.files.directory(dir, true);
|
|
4586
4793
|
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
4587
4794
|
lease.assert();
|
|
4588
|
-
c.files.directory(
|
|
4795
|
+
c.files.directory(join8(dir, name), true);
|
|
4589
4796
|
}
|
|
4590
4797
|
lease.assert();
|
|
4591
|
-
const keyPath =
|
|
4798
|
+
const keyPath = join8(dir, "read-proof-key.json");
|
|
4592
4799
|
c.files.create(keyPath, encoded({
|
|
4593
4800
|
schema: "cadence-history-read-key/1",
|
|
4594
4801
|
storeId: format.storeId,
|
|
4595
|
-
key:
|
|
4802
|
+
key: randomBytes3(32).toString("base64url")
|
|
4596
4803
|
}));
|
|
4597
4804
|
lease.assert();
|
|
4598
|
-
const snapshot = c.files.create(
|
|
4805
|
+
const snapshot = c.files.create(join8(dir, "format.json"), encoded(format));
|
|
4599
4806
|
const result = new HistoryFiles(c, format, snapshot);
|
|
4600
|
-
result.syncParents(
|
|
4807
|
+
result.syncParents(join8(dir, "format.json"));
|
|
4601
4808
|
result.assertWritable();
|
|
4602
4809
|
return result;
|
|
4603
4810
|
}
|
|
@@ -4632,7 +4839,7 @@ class HistoryFiles {
|
|
|
4632
4839
|
throw new ContractError("quarantine");
|
|
4633
4840
|
}
|
|
4634
4841
|
assertFormat() {
|
|
4635
|
-
this.files.assert(
|
|
4842
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot);
|
|
4636
4843
|
}
|
|
4637
4844
|
readProofMaterial(budget) {
|
|
4638
4845
|
if (this.format.schema !== FORMAT)
|
|
@@ -4641,7 +4848,7 @@ class HistoryFiles {
|
|
|
4641
4848
|
this.assertReadFormat(budget);
|
|
4642
4849
|
else
|
|
4643
4850
|
this.assertFormat();
|
|
4644
|
-
const selected2 = this.readBudgeted(
|
|
4851
|
+
const selected2 = this.readBudgeted(join8(this.directory, "read-proof-key.json"), 512, budget);
|
|
4645
4852
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
4646
4853
|
corrupt();
|
|
4647
4854
|
try {
|
|
@@ -4669,17 +4876,17 @@ class HistoryFiles {
|
|
|
4669
4876
|
return;
|
|
4670
4877
|
}
|
|
4671
4878
|
this.assertFormat();
|
|
4672
|
-
assertIncarnation(this,
|
|
4879
|
+
assertIncarnation(this, join8(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
4673
4880
|
}
|
|
4674
4881
|
segmentPath(segment) {
|
|
4675
|
-
return
|
|
4882
|
+
return join8(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
4676
4883
|
}
|
|
4677
4884
|
artifactPath(ref) {
|
|
4678
|
-
return
|
|
4885
|
+
return join8(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
4679
4886
|
}
|
|
4680
4887
|
admissionPath(runId) {
|
|
4681
4888
|
decodeId(runId);
|
|
4682
|
-
return
|
|
4889
|
+
return join8(this.directory, "runs", runId, "admission.json");
|
|
4683
4890
|
}
|
|
4684
4891
|
writeAdmission(lease, input) {
|
|
4685
4892
|
lease.assert();
|
|
@@ -4688,24 +4895,24 @@ class HistoryFiles {
|
|
|
4688
4895
|
this.files.create(this.admissionPath(value.runId), encoded(value), RECORD_BYTES);
|
|
4689
4896
|
this.syncParents(this.admissionPath(value.runId));
|
|
4690
4897
|
}
|
|
4691
|
-
readBudgeted(
|
|
4898
|
+
readBudgeted(path2, maximum, budget) {
|
|
4692
4899
|
if (!budget)
|
|
4693
|
-
return this.files.read(
|
|
4900
|
+
return this.files.read(path2, maximum);
|
|
4694
4901
|
let size;
|
|
4695
4902
|
try {
|
|
4696
|
-
size = lstatSync5(
|
|
4903
|
+
size = lstatSync5(path2).size;
|
|
4697
4904
|
} catch {
|
|
4698
|
-
return this.files.read(
|
|
4905
|
+
return this.files.read(path2, 0, budget.walks);
|
|
4699
4906
|
}
|
|
4700
4907
|
if (size > maximum)
|
|
4701
4908
|
corrupt();
|
|
4702
4909
|
budget.reserve(size);
|
|
4703
|
-
return this.files.read(
|
|
4910
|
+
return this.files.read(path2, size, budget.walks);
|
|
4704
4911
|
}
|
|
4705
4912
|
readCommand(commandId, budget) {
|
|
4706
4913
|
decodeId(commandId);
|
|
4707
4914
|
this.assertReadFormat(budget);
|
|
4708
|
-
return this.readBudgeted(
|
|
4915
|
+
return this.readBudgeted(join8(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
4709
4916
|
}
|
|
4710
4917
|
readAdmission(runId, identity, budget) {
|
|
4711
4918
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -4726,24 +4933,24 @@ class HistoryFiles {
|
|
|
4726
4933
|
syncParents(file) {
|
|
4727
4934
|
if (process.platform === "win32")
|
|
4728
4935
|
return;
|
|
4729
|
-
for (let
|
|
4730
|
-
if (!this.files.directory(
|
|
4936
|
+
for (let path2 = dirname7(file);; path2 = dirname7(path2)) {
|
|
4937
|
+
if (!this.files.directory(path2))
|
|
4731
4938
|
corrupt();
|
|
4732
|
-
const fd =
|
|
4939
|
+
const fd = openSync5(path2, constants5.O_RDONLY | constants5.O_DIRECTORY);
|
|
4733
4940
|
try {
|
|
4734
4941
|
fsyncSync3(fd);
|
|
4735
4942
|
} finally {
|
|
4736
|
-
|
|
4943
|
+
closeSync5(fd);
|
|
4737
4944
|
}
|
|
4738
|
-
if (
|
|
4945
|
+
if (path2 === this.coordination.directory)
|
|
4739
4946
|
break;
|
|
4740
|
-
if (
|
|
4947
|
+
if (path2 === dirname7(path2))
|
|
4741
4948
|
corrupt();
|
|
4742
4949
|
}
|
|
4743
4950
|
}
|
|
4744
4951
|
assertReadFormat(budget) {
|
|
4745
4952
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
4746
|
-
this.files.assert(
|
|
4953
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
4747
4954
|
}
|
|
4748
4955
|
assertPublished(segment, end, seq) {
|
|
4749
4956
|
const fence = publicationFences.get(this.directory);
|
|
@@ -4774,14 +4981,14 @@ class HistoryFiles {
|
|
|
4774
4981
|
this.syncParents(this.artifactPath(ref));
|
|
4775
4982
|
}
|
|
4776
4983
|
usage() {
|
|
4777
|
-
const walk = (
|
|
4778
|
-
if (depth > 8 || !this.files.directory(
|
|
4984
|
+
const walk = (path2, depth) => {
|
|
4985
|
+
if (depth > 8 || !this.files.directory(path2))
|
|
4779
4986
|
corrupt();
|
|
4780
4987
|
let bytes = 0;
|
|
4781
|
-
const dir = opendirSync2(
|
|
4988
|
+
const dir = opendirSync2(path2);
|
|
4782
4989
|
try {
|
|
4783
4990
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
4784
|
-
const p =
|
|
4991
|
+
const p = join8(path2, e.name), st = lstatSync5(p);
|
|
4785
4992
|
if (st.isDirectory())
|
|
4786
4993
|
bytes += walk(p, depth + 1);
|
|
4787
4994
|
else {
|
|
@@ -4816,7 +5023,7 @@ class HistoryFiles {
|
|
|
4816
5023
|
state.bytes = 0;
|
|
4817
5024
|
state.identity = null;
|
|
4818
5025
|
let count = 0;
|
|
4819
|
-
const ledger =
|
|
5026
|
+
const ledger = join8(this.directory, "ledger");
|
|
4820
5027
|
if (!this.files.directory(ledger))
|
|
4821
5028
|
corrupt();
|
|
4822
5029
|
const dir = opendirSync2(ledger);
|
|
@@ -4864,9 +5071,9 @@ class HistoryFiles {
|
|
|
4864
5071
|
const line = file.bytes.subarray(at, end + 1);
|
|
4865
5072
|
if (line.length > RECORD_BYTES)
|
|
4866
5073
|
corrupt();
|
|
4867
|
-
let
|
|
5074
|
+
let decoded2;
|
|
4868
5075
|
try {
|
|
4869
|
-
|
|
5076
|
+
decoded2 = decodeJournalText(new TextDecoder("utf-8", { fatal: true }).decode(line));
|
|
4870
5077
|
} catch {
|
|
4871
5078
|
corrupt();
|
|
4872
5079
|
}
|
|
@@ -4874,29 +5081,29 @@ class HistoryFiles {
|
|
|
4874
5081
|
state.records++;
|
|
4875
5082
|
if (state.records > LIMITS.segmentRecords)
|
|
4876
5083
|
corrupt();
|
|
4877
|
-
if (
|
|
5084
|
+
if (decoded2.kind === "record") {
|
|
4878
5085
|
let base = state.committedSeq;
|
|
4879
|
-
if (
|
|
4880
|
-
if (pending.length || at !== line.length || n !==
|
|
5086
|
+
if (decoded2.value.kind === "recovery") {
|
|
5087
|
+
if (pending.length || at !== line.length || n !== decoded2.value.previous.segment + 1)
|
|
4881
5088
|
corrupt();
|
|
4882
|
-
if (
|
|
5089
|
+
if (decoded2.value.previous.segment !== preceding.segment || decoded2.value.previous.bytes !== preceding.bytes || decoded2.value.previous.sha256 !== (preceding.identity ?? hash("")) || decoded2.value.previous.committedSeq !== preceding.committedSeq || decoded2.value.previous.highSeq !== (preceding.highSeq ?? preceding.committedSeq) || decoded2.value.previous.committedBytes !== (preceding.committedBytes ?? 0))
|
|
4883
5090
|
corrupt();
|
|
4884
|
-
base =
|
|
5091
|
+
base = decoded2.value.previous.highSeq;
|
|
4885
5092
|
} else if (preceding.tail && state.committedBytes === 0)
|
|
4886
5093
|
corrupt();
|
|
4887
5094
|
const next = BigInt(base) + BigInt(pending.length) + 1n;
|
|
4888
|
-
if (
|
|
5095
|
+
if (decoded2.seq !== String(next))
|
|
4889
5096
|
corrupt();
|
|
4890
|
-
state.highSeq =
|
|
5097
|
+
state.highSeq = decoded2.seq;
|
|
4891
5098
|
pendingBytes += line.length;
|
|
4892
5099
|
if (pendingBytes > 2 * RECORD_BYTES)
|
|
4893
5100
|
corrupt();
|
|
4894
|
-
pending.push(
|
|
5101
|
+
pending.push(decoded2);
|
|
4895
5102
|
checksum.update(line);
|
|
4896
5103
|
continue;
|
|
4897
5104
|
}
|
|
4898
5105
|
const artifacts = batchArtifacts(pending);
|
|
4899
|
-
if (!pending.length || !validBatchSequence(pending,
|
|
5106
|
+
if (!pending.length || !validBatchSequence(pending, decoded2.previous) || decoded2.previous !== state.committedSeq || decoded2.first !== pending[0].seq || decoded2.last !== pending.at(-1)?.seq || decoded2.count !== pending.length || decoded2.sha256 !== checksum.digest("hex") || canonicalJson(decoded2.artifacts, RECORD_BYTES) !== canonicalJson(artifacts, RECORD_BYTES))
|
|
4900
5107
|
corrupt();
|
|
4901
5108
|
if (verifyArtifacts)
|
|
4902
5109
|
for (const artifact of artifacts)
|
|
@@ -4909,12 +5116,12 @@ class HistoryFiles {
|
|
|
4909
5116
|
if (e.kind === "owner" && e.value.kind === "fact" && e.value.fact.kind === "parent-admitted")
|
|
4910
5117
|
this.readAdmission(e.value.fact.runId, e.value.fact.identity, budget);
|
|
4911
5118
|
}
|
|
4912
|
-
state.committedSeq =
|
|
5119
|
+
state.committedSeq = decoded2.last;
|
|
4913
5120
|
state.committedBytes = at;
|
|
4914
|
-
state.navigationRoot =
|
|
5121
|
+
state.navigationRoot = decoded2.navigationRoot;
|
|
4915
5122
|
yield {
|
|
4916
5123
|
rows: pending,
|
|
4917
|
-
marker:
|
|
5124
|
+
marker: decoded2,
|
|
4918
5125
|
segment: n,
|
|
4919
5126
|
identity: file.identity,
|
|
4920
5127
|
segmentBytes: file.bytes.length
|
|
@@ -4974,17 +5181,17 @@ class HistoryFiles {
|
|
|
4974
5181
|
this.assertRecoveryIdle();
|
|
4975
5182
|
this.validateRecovery(row.value);
|
|
4976
5183
|
const { body, trailer, marker } = this.encodeBatch(state, [row]);
|
|
4977
|
-
const segment = state.segment + 1,
|
|
5184
|
+
const segment = state.segment + 1, path2 = this.segmentPath(segment);
|
|
4978
5185
|
publicationFences.set(this.directory, { ...state });
|
|
4979
5186
|
this.activity().generation++;
|
|
4980
|
-
const before = this.files.create(
|
|
5187
|
+
const before = this.files.create(path2, "", LIMITS.segmentBytes);
|
|
4981
5188
|
let committed;
|
|
4982
|
-
const fd = await open(
|
|
5189
|
+
const fd = await open(path2, constants5.O_WRONLY | constants5.O_APPEND | (constants5.O_NOFOLLOW ?? 0));
|
|
4983
5190
|
const assertOriginal = (bytes) => {
|
|
4984
|
-
const original = fstatSync4(fd.fd), selected2 = lstatSync5(
|
|
5191
|
+
const original = fstatSync4(fd.fd), selected2 = lstatSync5(path2);
|
|
4985
5192
|
if (original.dev !== selected2.dev || original.ino !== selected2.ino || original.nlink !== 1)
|
|
4986
5193
|
corrupt();
|
|
4987
|
-
const snapshot = this.files.read(
|
|
5194
|
+
const snapshot = this.files.read(path2, LIMITS.segmentBytes);
|
|
4988
5195
|
if (!snapshot || snapshot.identity !== hash(bytes))
|
|
4989
5196
|
corrupt();
|
|
4990
5197
|
return snapshot;
|
|
@@ -4992,7 +5199,7 @@ class HistoryFiles {
|
|
|
4992
5199
|
try {
|
|
4993
5200
|
await assertCurrent();
|
|
4994
5201
|
this.assertWritable();
|
|
4995
|
-
this.files.assert(
|
|
5202
|
+
this.files.assert(path2, before, LIMITS.segmentBytes);
|
|
4996
5203
|
assertOriginal("");
|
|
4997
5204
|
await fd.writeFile(body);
|
|
4998
5205
|
await observer?.("journal");
|
|
@@ -5001,7 +5208,7 @@ class HistoryFiles {
|
|
|
5001
5208
|
assertOriginal(body);
|
|
5002
5209
|
await fd.writeFile(trailer);
|
|
5003
5210
|
await fd.sync();
|
|
5004
|
-
this.syncParents(
|
|
5211
|
+
this.syncParents(path2);
|
|
5005
5212
|
await observer?.("commit");
|
|
5006
5213
|
await assertCurrent();
|
|
5007
5214
|
this.assertWritable();
|
|
@@ -5013,7 +5220,7 @@ class HistoryFiles {
|
|
|
5013
5220
|
this.assertWritable();
|
|
5014
5221
|
if (!committed)
|
|
5015
5222
|
corrupt();
|
|
5016
|
-
this.files.assert(
|
|
5223
|
+
this.files.assert(path2, committed, LIMITS.segmentBytes);
|
|
5017
5224
|
publicationFences.delete(this.directory);
|
|
5018
5225
|
return {
|
|
5019
5226
|
snapshot: committed,
|
|
@@ -5085,20 +5292,20 @@ class HistoryFiles {
|
|
|
5085
5292
|
if (state.bytes + Buffer.byteLength(body + trailer) > LIMITS.segmentBytes || state.records + rows.length + 1 > LIMITS.segmentRecords)
|
|
5086
5293
|
segment++;
|
|
5087
5294
|
lease.assert();
|
|
5088
|
-
const
|
|
5089
|
-
let before = this.files.read(
|
|
5295
|
+
const path2 = this.segmentPath(segment);
|
|
5296
|
+
let before = this.files.read(path2, LIMITS.segmentBytes);
|
|
5090
5297
|
if (segment !== state.segment) {
|
|
5091
5298
|
if (before)
|
|
5092
5299
|
corrupt();
|
|
5093
|
-
before = this.files.create(
|
|
5300
|
+
before = this.files.create(path2, "", LIMITS.segmentBytes);
|
|
5094
5301
|
}
|
|
5095
5302
|
if (!before || segment === state.segment && (before.bytes.length !== state.bytes || before.identity !== state.identity))
|
|
5096
5303
|
corrupt();
|
|
5097
|
-
const fd = await open(
|
|
5304
|
+
const fd = await open(path2, constants5.O_WRONLY | constants5.O_APPEND | (constants5.O_NOFOLLOW ?? 0));
|
|
5098
5305
|
try {
|
|
5099
5306
|
lease.assert();
|
|
5100
|
-
this.files.assert(
|
|
5101
|
-
const opened = await fd.stat(), selected2 = lstatSync5(
|
|
5307
|
+
this.files.assert(path2, before, LIMITS.segmentBytes);
|
|
5308
|
+
const opened = await fd.stat(), selected2 = lstatSync5(path2);
|
|
5102
5309
|
if (opened.ino !== selected2.ino || opened.dev !== selected2.dev || opened.nlink !== 1)
|
|
5103
5310
|
corrupt();
|
|
5104
5311
|
beforeEffect?.();
|
|
@@ -5112,7 +5319,7 @@ class HistoryFiles {
|
|
|
5112
5319
|
await observer?.("commit");
|
|
5113
5320
|
lease.assert();
|
|
5114
5321
|
this.assertWritable();
|
|
5115
|
-
const after = this.files.read(
|
|
5322
|
+
const after = this.files.read(path2, LIMITS.segmentBytes);
|
|
5116
5323
|
if (!after || after.identity !== createHash4("sha256").update(before.bytes).update(body).update(trailer).digest("hex"))
|
|
5117
5324
|
corrupt();
|
|
5118
5325
|
return {
|
|
@@ -5133,7 +5340,7 @@ class HistoryFiles {
|
|
|
5133
5340
|
checkpoint(lease, state) {
|
|
5134
5341
|
lease.assert();
|
|
5135
5342
|
this.assertWritable();
|
|
5136
|
-
const
|
|
5343
|
+
const path2 = join8(this.directory, "checkpoint.json"), old = this.files.read(path2);
|
|
5137
5344
|
const body = encoded({
|
|
5138
5345
|
schema: "cadence-history-checkpoint/2",
|
|
5139
5346
|
storeId: this.format.storeId,
|
|
@@ -5141,9 +5348,9 @@ class HistoryFiles {
|
|
|
5141
5348
|
segment: state.segment
|
|
5142
5349
|
});
|
|
5143
5350
|
if (old)
|
|
5144
|
-
this.files.replace(
|
|
5351
|
+
this.files.replace(path2, old, body);
|
|
5145
5352
|
else
|
|
5146
|
-
this.files.create(
|
|
5353
|
+
this.files.create(path2, body);
|
|
5147
5354
|
}
|
|
5148
5355
|
}
|
|
5149
5356
|
function emptyScan() {
|
|
@@ -5543,13 +5750,13 @@ function digestHistory(files, exclude) {
|
|
|
5543
5750
|
files.assertFormat();
|
|
5544
5751
|
const digest = createHash5("sha256");
|
|
5545
5752
|
let count = 0;
|
|
5546
|
-
const walk = (
|
|
5547
|
-
if (depth > 8 || !files.files.directory(
|
|
5753
|
+
const walk = (path2, depth) => {
|
|
5754
|
+
if (depth > 8 || !files.files.directory(path2))
|
|
5548
5755
|
throw new ContractError("corrupt-selected");
|
|
5549
|
-
const directory2 = lstatSync6(
|
|
5756
|
+
const directory2 = lstatSync6(path2);
|
|
5550
5757
|
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
5551
5758
|
`);
|
|
5552
|
-
const dir = opendirSync3(
|
|
5759
|
+
const dir = opendirSync3(path2), names2 = [];
|
|
5553
5760
|
try {
|
|
5554
5761
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
5555
5762
|
if (++count > LIMITS.dataFrames)
|
|
@@ -5560,7 +5767,7 @@ function digestHistory(files, exclude) {
|
|
|
5560
5767
|
dir.closeSync();
|
|
5561
5768
|
}
|
|
5562
5769
|
for (const name of names2.sort()) {
|
|
5563
|
-
const selected2 =
|
|
5770
|
+
const selected2 = join9(path2, name);
|
|
5564
5771
|
if (selected2 === exclude)
|
|
5565
5772
|
continue;
|
|
5566
5773
|
const rel = relative4(files.directory, selected2);
|
|
@@ -5700,13 +5907,13 @@ class HistoryRecovery {
|
|
|
5700
5907
|
},
|
|
5701
5908
|
acknowledgments: { processes: true, credentials: true, storage: true }
|
|
5702
5909
|
};
|
|
5703
|
-
const
|
|
5910
|
+
const path2 = files.segmentPath(state.segment + 1);
|
|
5704
5911
|
const assertCurrent = async () => {
|
|
5705
5912
|
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
5706
5913
|
const selected2 = await inspectRecovery(this.coordination);
|
|
5707
5914
|
if (selected2.owner !== "absent")
|
|
5708
5915
|
throw new ContractError("quarantine");
|
|
5709
|
-
if (selected2.epoch !== expected.epoch || selected2.coordinationDigest !== expected.coordinationDigest || digestHistory(files,
|
|
5916
|
+
if (selected2.epoch !== expected.epoch || selected2.coordinationDigest !== expected.coordinationDigest || digestHistory(files, path2) !== expected.history?.digest || this.coordination.recoverySnapshot().digest !== expected.coordinationDigest)
|
|
5710
5917
|
throw new ContractError("conflict");
|
|
5711
5918
|
};
|
|
5712
5919
|
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
@@ -5730,8 +5937,8 @@ class HistoryRecovery {
|
|
|
5730
5937
|
const assert = () => {
|
|
5731
5938
|
files.assertWritable();
|
|
5732
5939
|
files.assertRecoveryIdle();
|
|
5733
|
-
files.files.assert(
|
|
5734
|
-
if (digestHistory(files,
|
|
5940
|
+
files.files.assert(path2, after.snapshot, LIMITS.segmentBytes);
|
|
5941
|
+
if (digestHistory(files, path2) !== expected.history?.digest)
|
|
5735
5942
|
throw new ContractError("conflict");
|
|
5736
5943
|
};
|
|
5737
5944
|
assert();
|
|
@@ -5870,8 +6077,8 @@ async function localRecovery(c, operation) {
|
|
|
5870
6077
|
}
|
|
5871
6078
|
|
|
5872
6079
|
// src/service/runtime.ts
|
|
5873
|
-
import { homedir as
|
|
5874
|
-
import { dirname as
|
|
6080
|
+
import { homedir as homedir4 } from "node:os";
|
|
6081
|
+
import { dirname as dirname8, isAbsolute as isAbsolute6, join as join10, resolve as resolve4 } from "node:path";
|
|
5875
6082
|
|
|
5876
6083
|
// src/service/config.ts
|
|
5877
6084
|
import { isIP } from "node:net";
|
|
@@ -5941,10 +6148,11 @@ function decodeHostConfig(value) {
|
|
|
5941
6148
|
const tokenSource = oneOf(o.tokenSource, [
|
|
5942
6149
|
"generated",
|
|
5943
6150
|
"environment",
|
|
5944
|
-
"file"
|
|
6151
|
+
"file",
|
|
6152
|
+
"config"
|
|
5945
6153
|
]);
|
|
5946
6154
|
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
5947
|
-
if (
|
|
6155
|
+
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
5948
6156
|
throw new ContractError("invalid");
|
|
5949
6157
|
return {
|
|
5950
6158
|
schema: literal(o.schema, "cadence-service-host/1"),
|
|
@@ -6005,13 +6213,13 @@ function selectStart(options, ambient) {
|
|
|
6005
6213
|
throw new ContractError("invalid");
|
|
6006
6214
|
let fromFile;
|
|
6007
6215
|
if (options.config) {
|
|
6008
|
-
const
|
|
6009
|
-
const bytes = f.read(
|
|
6216
|
+
const path2 = resolve4(options.config), f = new PrivateFiles(dirname8(path2));
|
|
6217
|
+
const bytes = f.read(path2);
|
|
6010
6218
|
if (!bytes)
|
|
6011
6219
|
throw new ContractError("corrupt-selected");
|
|
6012
6220
|
fromFile = decodeHostConfig(json(bytes));
|
|
6013
6221
|
}
|
|
6014
|
-
const
|
|
6222
|
+
const base = old ?? fromFile ?? decodeHostConfig({
|
|
6015
6223
|
schema: "cadence-service-host/1",
|
|
6016
6224
|
cwd: workspace.cwd,
|
|
6017
6225
|
http: {
|
|
@@ -6020,10 +6228,23 @@ function selectStart(options, ambient) {
|
|
|
6020
6228
|
port: options.port ?? 17423,
|
|
6021
6229
|
proxy: null
|
|
6022
6230
|
},
|
|
6023
|
-
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ??
|
|
6231
|
+
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join10(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
6024
6232
|
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
6025
6233
|
tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
|
|
6026
6234
|
});
|
|
6235
|
+
const server = options.serverConfig !== undefined ? resolve4(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
|
|
6236
|
+
let selected2 = base;
|
|
6237
|
+
if (server) {
|
|
6238
|
+
if (options.tokenFile !== undefined)
|
|
6239
|
+
throw new ContractError("conflict");
|
|
6240
|
+
const { port } = readServerConfig(server).config;
|
|
6241
|
+
selected2 = decodeHostConfig({
|
|
6242
|
+
...base,
|
|
6243
|
+
http: { ...base.http, port },
|
|
6244
|
+
tokenSource: "config",
|
|
6245
|
+
tokenFile: server
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6027
6248
|
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)
|
|
6028
6249
|
throw new ContractError("conflict");
|
|
6029
6250
|
return {
|
|
@@ -6174,6 +6395,10 @@ async function stopService(c) {
|
|
|
6174
6395
|
function failedStatus(result) {
|
|
6175
6396
|
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
|
|
6176
6397
|
}
|
|
6398
|
+
function serverConfigHint(path2) {
|
|
6399
|
+
return `Cadence changed nothing: ${path2} must be your own private regular file (chmod 600, in a chmod 700 directory on Linux and macOS) in KEY=value lines with exactly one ${PORT_KEY}=<1-65535> and one ${TOKEN_KEY}=<at least 32 characters>.
|
|
6400
|
+
`;
|
|
6401
|
+
}
|
|
6177
6402
|
function startupGuidance(result, input) {
|
|
6178
6403
|
let report;
|
|
6179
6404
|
try {
|
|
@@ -6192,10 +6417,10 @@ function startupGuidance(result, input) {
|
|
|
6192
6417
|
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
6193
6418
|
`;
|
|
6194
6419
|
case "pi-auth":
|
|
6195
|
-
return `${
|
|
6420
|
+
return `${join11(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
6196
6421
|
`;
|
|
6197
6422
|
case "pi-models":
|
|
6198
|
-
return `${
|
|
6423
|
+
return `${join11(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.
|
|
6199
6424
|
`;
|
|
6200
6425
|
case "pi-runtime":
|
|
6201
6426
|
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
@@ -6208,15 +6433,26 @@ ${inspect}` : "";
|
|
|
6208
6433
|
async function startService(options, foreground, hostUrl) {
|
|
6209
6434
|
const network = networkSnapshot(process.env);
|
|
6210
6435
|
const env = hostEnvironment(process.env);
|
|
6211
|
-
|
|
6436
|
+
let input;
|
|
6437
|
+
try {
|
|
6438
|
+
input = decodeHostInput(selectStart(options, process.env));
|
|
6439
|
+
} catch (error) {
|
|
6440
|
+
if (error instanceof ServerConfigRefusal)
|
|
6441
|
+
process.stderr.write(serverConfigHint(error.path));
|
|
6442
|
+
throw error;
|
|
6443
|
+
}
|
|
6212
6444
|
const serialized = `${JSON.stringify(input)}
|
|
6213
6445
|
`;
|
|
6214
6446
|
if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
|
|
6215
6447
|
throw new ContractError("oversize");
|
|
6216
6448
|
const c = new ServiceCoordination(input.workspace, input.stateHome);
|
|
6217
6449
|
const existing = await serviceStatus(c);
|
|
6218
|
-
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive")
|
|
6450
|
+
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
|
|
6451
|
+
const running = readHostConfig(c);
|
|
6452
|
+
if (running && encoded(running) !== encoded(input.config))
|
|
6453
|
+
throw new ContractError("conflict");
|
|
6219
6454
|
return existing;
|
|
6455
|
+
}
|
|
6220
6456
|
if (!sameNetwork(network, networkSnapshot(process.env)))
|
|
6221
6457
|
throw new ContractError("stale");
|
|
6222
6458
|
const child = spawn(process.execPath, [
|
|
@@ -6321,6 +6557,14 @@ async function observeStarted(workspace) {
|
|
|
6321
6557
|
}
|
|
6322
6558
|
return { service };
|
|
6323
6559
|
}
|
|
6560
|
+
async function privateLink(workspace) {
|
|
6561
|
+
try {
|
|
6562
|
+
const value = closed(await nativeCall(new ServiceCoordination(workspace), "token-link", true), ["link"]);
|
|
6563
|
+
return text(value.link, 4096, true);
|
|
6564
|
+
} catch {
|
|
6565
|
+
return;
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6324
6568
|
var ownerControl = {
|
|
6325
6569
|
async ownerStatus(workspace) {
|
|
6326
6570
|
try {
|
|
@@ -6369,11 +6613,11 @@ async function serverAction(s, action, requested, linger) {
|
|
|
6369
6613
|
return { status: "unsupported" };
|
|
6370
6614
|
}
|
|
6371
6615
|
}
|
|
6372
|
-
function serverGuidance(s, action, result) {
|
|
6373
|
-
const cli = piUserInstall(s) ? `node ${JSON.stringify(
|
|
6616
|
+
function serverGuidance(s, action, result, link) {
|
|
6617
|
+
const cli = piUserInstall(s) ? `node ${JSON.stringify(join11(s.root, "src", "service-cli.mjs"))}` : "cadence";
|
|
6374
6618
|
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
6375
6619
|
` : "";
|
|
6376
|
-
return note + serverStatusGuidance(s, action, result, cli);
|
|
6620
|
+
return note + serverStatusGuidance(s, action, result, cli, link);
|
|
6377
6621
|
}
|
|
6378
6622
|
function lingerGuidance(linger) {
|
|
6379
6623
|
switch (linger) {
|
|
@@ -6400,10 +6644,11 @@ function managerDetail(s) {
|
|
|
6400
6644
|
return `run its launcher to see why: wscript ${JSON.stringify(launcherPath(s))}`;
|
|
6401
6645
|
}
|
|
6402
6646
|
}
|
|
6403
|
-
function serverStatusGuidance(s, action, result, cli) {
|
|
6647
|
+
function serverStatusGuidance(s, action, result, cli, link) {
|
|
6404
6648
|
const workspace = JSON.stringify(result.workspace ?? "");
|
|
6405
6649
|
const manage = `Manage: ${cli} server status|restart|stop|uninstall`;
|
|
6406
6650
|
const when = s.platform === "linux" ? "at boot or login" : "at login";
|
|
6651
|
+
const config = `Port and token: ${PORT_KEY} and ${TOKEN_KEY} in ${serverConfigPath(s.home)} (edit them, then: ${cli} server restart)`;
|
|
6407
6652
|
switch (result.status) {
|
|
6408
6653
|
case "started": {
|
|
6409
6654
|
const linger = lingerGuidance(result.linger);
|
|
@@ -6412,14 +6657,22 @@ function serverStatusGuidance(s, action, result, cli) {
|
|
|
6412
6657
|
Inspect: ${cli} diagnose --workspace ${workspace}
|
|
6413
6658
|
${manage}
|
|
6414
6659
|
${linger}`;
|
|
6415
|
-
|
|
6660
|
+
if (!link)
|
|
6661
|
+
return `Cadence WebUI: ${result.endpoint} (workspace ${result.workspace}; starts ${when})
|
|
6416
6662
|
Enter with your private link: ${cli} token-link --workspace ${workspace} --reveal
|
|
6663
|
+
${config}
|
|
6664
|
+
${manage}
|
|
6665
|
+
${linger}`;
|
|
6666
|
+
return `Cadence WebUI: ${link}
|
|
6667
|
+
Open this private link (workspace ${result.workspace}; starts ${when}); its token admits anyone who has it.
|
|
6668
|
+
${config}
|
|
6417
6669
|
${manage}
|
|
6418
6670
|
${linger}`;
|
|
6419
6671
|
}
|
|
6420
6672
|
case "waiting-for-pi-login":
|
|
6421
|
-
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${
|
|
6673
|
+
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join11(agentDirectory(s), "auth.json")}.
|
|
6422
6674
|
Log in to Pi, then: ${cli} server restart
|
|
6675
|
+
${config}
|
|
6423
6676
|
${lingerGuidance(result.linger)}`;
|
|
6424
6677
|
case "stopped":
|
|
6425
6678
|
return action === "stop" ? `Cadence stopped; it starts again ${when}, or now with: ${cli} server restart
|
|
@@ -6457,6 +6710,16 @@ ${lingerGuidance(result.linger)}`;
|
|
|
6457
6710
|
return "";
|
|
6458
6711
|
}
|
|
6459
6712
|
}
|
|
6713
|
+
function researchTemplate(home) {
|
|
6714
|
+
try {
|
|
6715
|
+
const result = ensureUserConfig(home);
|
|
6716
|
+
return result.created ? `Cadence created the research configuration template ${result.path}
|
|
6717
|
+
` : "";
|
|
6718
|
+
} catch (error) {
|
|
6719
|
+
return `Cadence could not create the research configuration template: ${error instanceof Error ? error.message : String(error)}
|
|
6720
|
+
`;
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6460
6723
|
var SERVER_ACTIONS = [
|
|
6461
6724
|
"install",
|
|
6462
6725
|
"status",
|
|
@@ -6484,7 +6747,21 @@ async function serverMain(argv, hostUrl) {
|
|
|
6484
6747
|
}
|
|
6485
6748
|
if (argv.length)
|
|
6486
6749
|
throw new ContractError("invalid");
|
|
6487
|
-
const s = hostSystem(
|
|
6750
|
+
const s = hostSystem(dirname9(dirname9(fileURLToPath(hostUrl))), ownerControl);
|
|
6751
|
+
if (action === "install")
|
|
6752
|
+
process.stderr.write(researchTemplate(s.home));
|
|
6753
|
+
if (action === "install" || action === "restart") {
|
|
6754
|
+
try {
|
|
6755
|
+
const { added } = ensureServerConfig(s.home);
|
|
6756
|
+
if (added.length)
|
|
6757
|
+
process.stderr.write(`Cadence added ${added.map((key) => key === PORT_KEY ? `${key}=${DEFAULT_PORT}` : `a random private ${key}`).join(" and ")} to ${serverConfigPath(s.home)}
|
|
6758
|
+
`);
|
|
6759
|
+
} catch (error) {
|
|
6760
|
+
if (error instanceof ServerConfigRefusal)
|
|
6761
|
+
process.stderr.write(serverConfigHint(error.path));
|
|
6762
|
+
throw error;
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6488
6765
|
let result;
|
|
6489
6766
|
try {
|
|
6490
6767
|
result = await serverAction(s, action, requested, linger);
|
|
@@ -6494,9 +6771,13 @@ async function serverMain(argv, hostUrl) {
|
|
|
6494
6771
|
`);
|
|
6495
6772
|
throw error;
|
|
6496
6773
|
}
|
|
6497
|
-
|
|
6774
|
+
let link;
|
|
6775
|
+
if (result.status === "started" && result.workspace) {
|
|
6498
6776
|
Object.assign(result, await observeStarted(result.workspace));
|
|
6499
|
-
|
|
6777
|
+
if (result.service === "alive")
|
|
6778
|
+
link = await privateLink(result.workspace);
|
|
6779
|
+
}
|
|
6780
|
+
process.stderr.write(serverGuidance(s, action, result, link));
|
|
6500
6781
|
process.stdout.write(`${JSON.stringify(result)}
|
|
6501
6782
|
`);
|
|
6502
6783
|
if ([
|
|
@@ -6518,7 +6799,7 @@ async function managementMain(argv, hostUrl) {
|
|
|
6518
6799
|
if (command === "--help" && !argv.length) {
|
|
6519
6800
|
process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
6520
6801
|
cadence server install [--workspace PATH] [--no-linger]|status|restart|stop|uninstall
|
|
6521
|
-
Start is explicit, or the background service that server install registers: a systemd user unit on Linux, started at boot through linger (--no-linger or CADENCE_LINGER=0 keeps it to login), a LaunchAgent on macOS and a Run entry on Windows, both started at login. Server restart, stop and uninstall use the orderly Stop. cadence-service is the same command. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
|
|
6802
|
+
Start is explicit, or the background service that server install registers: a systemd user unit on Linux, started at boot through linger (--no-linger or CADENCE_LINGER=0 keeps it to login), a LaunchAgent on macOS and a Run entry on Windows, both started at login. Server restart, stop and uninstall use the orderly Stop. cadence-service is the same command. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file, --server-config (the .env whose CADENCE_PORT and CADENCE_TOKEN the background service uses).
|
|
6522
6803
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
6523
6804
|
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.
|
|
6524
6805
|
`);
|
|
@@ -6578,6 +6859,8 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
6578
6859
|
options.agentDir = value;
|
|
6579
6860
|
else if (flag === "--token-file")
|
|
6580
6861
|
options.tokenFile = value;
|
|
6862
|
+
else if (flag === "--server-config")
|
|
6863
|
+
options.serverConfig = value;
|
|
6581
6864
|
else if (flag === "--port")
|
|
6582
6865
|
options.port = portValue(value);
|
|
6583
6866
|
else if (flag === "--host" && (value === "127.0.0.1" || value === "::1"))
|
|
@@ -6592,7 +6875,7 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
6592
6875
|
`);
|
|
6593
6876
|
options.workspace = landing.workspace;
|
|
6594
6877
|
if (command === "start")
|
|
6595
|
-
|
|
6878
|
+
mkdirSync5(landing.workspace, { recursive: true });
|
|
6596
6879
|
}
|
|
6597
6880
|
let result;
|
|
6598
6881
|
if (command === "start")
|