@abelxiaoxing/cadence 2.0.13 → 2.0.15
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 +163 -70
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/docs/remote-access.md +13 -0
- package/docs/reuse-and-distribution.md +13 -2
- package/docs/service-management.md +72 -15
- package/licenses/pi-web-ui-reuse.md +2 -0
- package/package.json +5 -2
- package/src/conversation-context.mjs +147 -0
- package/src/conversation-resources.mjs +47 -0
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +2 -2
- package/src/operator-cli.mjs +4 -7
- package/src/sdk-worker.mjs +41 -8
- package/src/service/contracts/workbench.ts +303 -0
- package/src/service/coordination.ts +276 -44
- package/src/service-cli.mjs +1437 -568
- package/src/service-client.ts +49 -0
- package/src/service-host.mjs +15969 -13027
- package/src/service-http-client.ts +50 -24
- package/src/service-wire.ts +26 -4
- package/src/task-store.ts +12 -11
- package/src/worker-public.mjs +1 -0
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +13 -7
- package/web/dist/manifest.json +7 -7
package/src/service-cli.mjs
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
// src/service/cli.ts
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
6
|
+
import { mkdirSync as mkdirSync6 } from "node:fs";
|
|
7
7
|
import { request } from "node:http";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { homedir as homedir6 } from "node:os";
|
|
9
|
+
import { dirname as dirname13, join as join15, resolve as resolve7 } from "node:path";
|
|
10
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10
11
|
|
|
11
12
|
// src/install-config.mjs
|
|
12
13
|
import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
|
|
@@ -326,6 +327,7 @@ var ERROR_CODES = [
|
|
|
326
327
|
"quarantine",
|
|
327
328
|
"capacity",
|
|
328
329
|
"unavailable",
|
|
330
|
+
"registry-locked",
|
|
329
331
|
"auth-unavailable",
|
|
330
332
|
"recording-exhausted",
|
|
331
333
|
"receipt-inconclusive"
|
|
@@ -2303,13 +2305,10 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
2303
2305
|
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
2304
2306
|
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
2305
2307
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
2311
|
-
return { workspace: requested, redirected: false };
|
|
2312
|
-
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
2308
|
+
function effectiveWorkspace(requested, stateHome, _home) {
|
|
2309
|
+
if (privateWorkspace(requested, stateHome) || stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
2310
|
+
throw new Error("Choose a project outside Cadence private state; no default workspace was created");
|
|
2311
|
+
return { workspace: requested, redirected: false };
|
|
2313
2312
|
}
|
|
2314
2313
|
|
|
2315
2314
|
class TaskStore {
|
|
@@ -2993,6 +2992,14 @@ function readServerConfig(path2) {
|
|
|
2993
2992
|
return { ...file, config: decode(file.source) };
|
|
2994
2993
|
});
|
|
2995
2994
|
}
|
|
2995
|
+
function withToken(file, token) {
|
|
2996
|
+
const { parts, token: line } = settings(decoded(file.snapshot));
|
|
2997
|
+
if (!line)
|
|
2998
|
+
throw new ContractError("invalid");
|
|
2999
|
+
const indent = /^\s*/.exec(parts[line.at])?.[0] ?? "";
|
|
3000
|
+
parts[line.at] = `${indent}${TOKEN_KEY}=${accessToken(token)}`;
|
|
3001
|
+
return parts.join("");
|
|
3002
|
+
}
|
|
2996
3003
|
function ensureServerConfig(home, options = {}) {
|
|
2997
3004
|
const path2 = serverConfigPath(home, options.name);
|
|
2998
3005
|
if (options.port !== undefined && (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535))
|
|
@@ -3210,18 +3217,19 @@ function buildUnit(u) {
|
|
|
3210
3217
|
"Description=Cadence shared task service and WebUI",
|
|
3211
3218
|
"Documentation=https://github.com/abelxiaoxing/cadence/blob/main/docs/service-management.md",
|
|
3212
3219
|
`ConditionPathExists=${specifiers(u.cli)}`,
|
|
3213
|
-
`ConditionPathExists=${specifiers(join4(u.agentDir, "auth.json"))}
|
|
3214
|
-
...u.sdk.map((path2) => `ConditionPathExists=${specifiers(path2)}`),
|
|
3220
|
+
...u.workspace === null ? [] : [`ConditionPathExists=${specifiers(join4(u.agentDir, "auth.json"))}`],
|
|
3221
|
+
...(u.workspace === null ? [] : u.sdk).map((path2) => `ConditionPathExists=${specifiers(path2)}`),
|
|
3215
3222
|
"StartLimitIntervalSec=300",
|
|
3216
3223
|
"StartLimitBurst=3",
|
|
3217
3224
|
"",
|
|
3218
3225
|
"[Service]",
|
|
3219
3226
|
"Type=simple",
|
|
3220
3227
|
...u.system ? ["User=root"] : [],
|
|
3221
|
-
|
|
3228
|
+
...u.workspace === null ? ["# Cadence workbench gateway; no project workspace"] : [],
|
|
3229
|
+
`WorkingDirectory=${specifiers(u.workspace ?? dirname4(dirname4(u.cli)))}`,
|
|
3222
3230
|
...u.environment.map(([k, v]) => `Environment=${quoted(`${k}=${v}`)}`),
|
|
3223
|
-
`ExecStart=${command(["start", "--foreground", "--workspace", u.workspace, "--server-config", u.serverConfig])}`,
|
|
3224
|
-
`ExecStop=-${command(["stop", "--workspace", u.workspace])}`,
|
|
3231
|
+
`ExecStart=${command([...u.workspace === null ? ["workbench", "start", "--foreground"] : ["start", "--foreground", "--workspace", u.workspace], "--server-config", u.serverConfig])}`,
|
|
3232
|
+
`ExecStop=-${command(u.workspace === null ? ["workbench", "stop", "--server-config", u.serverConfig] : ["stop", "--workspace", u.workspace])}`,
|
|
3225
3233
|
"KillMode=mixed",
|
|
3226
3234
|
"TimeoutStopSec=90",
|
|
3227
3235
|
"Restart=on-failure",
|
|
@@ -3247,6 +3255,8 @@ function readGenerated(path2, marker) {
|
|
|
3247
3255
|
}
|
|
3248
3256
|
}
|
|
3249
3257
|
function unitWorkspace(text2) {
|
|
3258
|
+
if (text2?.includes("# Cadence workbench gateway; no project workspace"))
|
|
3259
|
+
return null;
|
|
3250
3260
|
return text2?.match(/^WorkingDirectory=(.+)$/m)?.[1].replaceAll("%%", "%");
|
|
3251
3261
|
}
|
|
3252
3262
|
function directory(path2) {
|
|
@@ -3260,19 +3270,15 @@ function serviceStateHome(s) {
|
|
|
3260
3270
|
const state = s.platform === "win32" ? s.env.LOCALAPPDATA : s.env.XDG_STATE_HOME;
|
|
3261
3271
|
return state && isAbsolute3(state) ? state : join4(s.home, ...s.platform === "win32" ? ["AppData", "Local"] : [".local", "state"]);
|
|
3262
3272
|
}
|
|
3263
|
-
function selectWorkspace(s, requested, kept,
|
|
3264
|
-
|
|
3265
|
-
if (
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
const landing = chosen ? effectiveWorkspace(chosen, serviceStateHome(s), s.home) : { workspace: join4(s.home, LANDING_WORKSPACE), redirected: false };
|
|
3273
|
-
if (create && landing.workspace !== chosen)
|
|
3274
|
-
mkdirSync5(landing.workspace, { recursive: true });
|
|
3275
|
-
return landing.redirected ? { workspace: landing.workspace, redirectedFrom: chosen } : { workspace: landing.workspace };
|
|
3273
|
+
function selectWorkspace(s, requested, kept, _create = false) {
|
|
3274
|
+
const chosen = requested ? expandHome(requested, s.home) : kept;
|
|
3275
|
+
if (!chosen)
|
|
3276
|
+
return { workspace: null };
|
|
3277
|
+
if (!directory(chosen))
|
|
3278
|
+
throw new ContractError("invalid");
|
|
3279
|
+
return {
|
|
3280
|
+
workspace: effectiveWorkspace(resolve3(chosen), serviceStateHome(s)).workspace
|
|
3281
|
+
};
|
|
3276
3282
|
}
|
|
3277
3283
|
function write(path2, content) {
|
|
3278
3284
|
mkdirSync5(dirname4(path2), { recursive: true });
|
|
@@ -3292,9 +3298,9 @@ function regularFile(path2) {
|
|
|
3292
3298
|
}
|
|
3293
3299
|
}
|
|
3294
3300
|
async function settle(s, workspace) {
|
|
3295
|
-
if (
|
|
3301
|
+
if (workspace === undefined)
|
|
3296
3302
|
return;
|
|
3297
|
-
const status = await s.stopOwner(workspace);
|
|
3303
|
+
const status = await s.stopOwner(workspace, serviceConfigPath(s));
|
|
3298
3304
|
if (status === "stopped")
|
|
3299
3305
|
return;
|
|
3300
3306
|
return {
|
|
@@ -3326,9 +3332,9 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
3326
3332
|
if (mode === "restart" && previous === undefined)
|
|
3327
3333
|
return { status: "absent" };
|
|
3328
3334
|
const sdk = sdkPackages(s.root);
|
|
3329
|
-
|
|
3335
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : unitWorkspace(previous));
|
|
3336
|
+
if (workspace !== null && !sdk)
|
|
3330
3337
|
return { status: "missing-pi-sdk" };
|
|
3331
|
-
const { workspace, redirectedFrom } = selectWorkspace(s, requested, unitWorkspace(previous));
|
|
3332
3338
|
const agentDir = agentDirectory(s);
|
|
3333
3339
|
const content = buildUnit({
|
|
3334
3340
|
node: s.node,
|
|
@@ -3336,7 +3342,7 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
3336
3342
|
workspace,
|
|
3337
3343
|
serverConfig: serviceConfigPath(s),
|
|
3338
3344
|
agentDir,
|
|
3339
|
-
sdk,
|
|
3345
|
+
sdk: sdk ?? [],
|
|
3340
3346
|
environment: serviceEnvironment(s)
|
|
3341
3347
|
});
|
|
3342
3348
|
const enabled = s.systemctl(["is-enabled", s.unit]).stdout.trim();
|
|
@@ -3359,7 +3365,7 @@ function configureAutostart(s, mode, requested, linger = false) {
|
|
|
3359
3365
|
if (mode === "install" && enabled !== "enabled" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
3360
3366
|
return { status: "failed", ...result };
|
|
3361
3367
|
const boot = () => mode === "install" ? bootStart(s, linger) : {};
|
|
3362
|
-
if (!regularFile(join4(agentDir, "auth.json")))
|
|
3368
|
+
if (workspace !== null && !regularFile(join4(agentDir, "auth.json")))
|
|
3363
3369
|
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
3364
3370
|
s.systemctl(["reset-failed", s.unit]);
|
|
3365
3371
|
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
@@ -3512,6 +3518,21 @@ function names(path2, allowed) {
|
|
|
3512
3518
|
dir.closeSync();
|
|
3513
3519
|
}
|
|
3514
3520
|
}
|
|
3521
|
+
function decodeRegistryLock(snapshot) {
|
|
3522
|
+
const o = closed(json(snapshot), ["schema", "token", "pid", "host"]);
|
|
3523
|
+
return {
|
|
3524
|
+
schema: literal(o.schema, "cadence-service-registry-lock/1"),
|
|
3525
|
+
token: decodeId(o.token),
|
|
3526
|
+
pid: integer(o.pid, 1, 2147483647),
|
|
3527
|
+
host: text(o.host, 255, true)
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
var registryNames = [
|
|
3531
|
+
"format.json",
|
|
3532
|
+
"roots.json",
|
|
3533
|
+
"registry.lock",
|
|
3534
|
+
"recovery-v1"
|
|
3535
|
+
];
|
|
3515
3536
|
function recoveryOwnerStatus(pid, host) {
|
|
3516
3537
|
if (host !== hostname2())
|
|
3517
3538
|
return "ambiguous";
|
|
@@ -3563,11 +3584,7 @@ class ServiceCoordination {
|
|
|
3563
3584
|
registry() {
|
|
3564
3585
|
if (!this.files.directory(this.registryDirectory))
|
|
3565
3586
|
return;
|
|
3566
|
-
names(this.registryDirectory,
|
|
3567
|
-
"format.json",
|
|
3568
|
-
"roots.json",
|
|
3569
|
-
"registry.lock"
|
|
3570
|
-
]);
|
|
3587
|
+
names(this.registryDirectory, registryNames);
|
|
3571
3588
|
const format = this.files.read(this.registryPath("format.json"));
|
|
3572
3589
|
const snapshot = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
|
|
3573
3590
|
if (!format || !snapshot)
|
|
@@ -3592,10 +3609,10 @@ class ServiceCoordination {
|
|
|
3592
3609
|
}
|
|
3593
3610
|
};
|
|
3594
3611
|
}
|
|
3595
|
-
assertNamespace(walks) {
|
|
3596
|
-
if (!this.files.directory(
|
|
3612
|
+
assertNamespace(walks, directory2 = this.directory) {
|
|
3613
|
+
if (!this.files.directory(directory2, false, walks))
|
|
3597
3614
|
refused();
|
|
3598
|
-
names(
|
|
3615
|
+
names(directory2, [
|
|
3599
3616
|
"format.json",
|
|
3600
3617
|
"config.json",
|
|
3601
3618
|
"registration.json",
|
|
@@ -3604,17 +3621,19 @@ class ServiceCoordination {
|
|
|
3604
3621
|
"access-config.json",
|
|
3605
3622
|
"access.json",
|
|
3606
3623
|
"host-config.json",
|
|
3607
|
-
"history-v2"
|
|
3624
|
+
"history-v2",
|
|
3625
|
+
"conversations-v1"
|
|
3608
3626
|
]);
|
|
3609
|
-
this.files.directory(join5(
|
|
3627
|
+
this.files.directory(join5(directory2, "history-v2"), false, walks);
|
|
3610
3628
|
}
|
|
3611
|
-
selected() {
|
|
3629
|
+
selected(directory2 = this.directory, root = this.store.root, stoppedOnly = false) {
|
|
3612
3630
|
const walks = new DirectoryWalks;
|
|
3613
|
-
if (!this.files.directory(
|
|
3631
|
+
if (!this.files.directory(directory2, false, walks))
|
|
3614
3632
|
return;
|
|
3615
|
-
this.assertNamespace(walks);
|
|
3616
|
-
const
|
|
3617
|
-
const
|
|
3633
|
+
this.assertNamespace(walks, directory2);
|
|
3634
|
+
const path2 = (name) => join5(directory2, name);
|
|
3635
|
+
const manifest = this.files.read(path2("format.json"), undefined, walks);
|
|
3636
|
+
const config = this.files.read(path2("config.json"), undefined, walks);
|
|
3618
3637
|
if (!manifest || !config)
|
|
3619
3638
|
refused();
|
|
3620
3639
|
const f = closed(json(manifest), [
|
|
@@ -3624,20 +3643,22 @@ class ServiceCoordination {
|
|
|
3624
3643
|
"configIdentity"
|
|
3625
3644
|
]);
|
|
3626
3645
|
literal(f.schema, "cadence-service-format/1");
|
|
3627
|
-
literal(f.root,
|
|
3646
|
+
literal(f.root, root);
|
|
3628
3647
|
literal(f.stateHome, this.stateHome);
|
|
3629
3648
|
literal(f.configIdentity, config.identity);
|
|
3630
3649
|
const c = decodeOwnerConfig(json(config));
|
|
3631
|
-
const hostConfig = this.files.read(
|
|
3650
|
+
const hostConfig = this.files.read(path2("host-config.json"), undefined, walks);
|
|
3632
3651
|
if (hostConfig && hostConfig.identity !== c.policyIdentity)
|
|
3633
3652
|
refused();
|
|
3634
|
-
const registration = this.files.read(
|
|
3635
|
-
const owner = this.files.read(
|
|
3636
|
-
const capability = this.files.read(
|
|
3637
|
-
const checkout = this.files.read(
|
|
3653
|
+
const registration = this.files.read(path2("registration.json"), undefined, walks);
|
|
3654
|
+
const owner = this.files.read(path2("owner.lock"), undefined, walks);
|
|
3655
|
+
const capability = this.files.read(path2("local-capability.json"), undefined, walks);
|
|
3656
|
+
const checkout = this.files.read(join5(dirname5(directory2), "checkout.lock"), 8192, walks);
|
|
3638
3657
|
const value = registration ? decodeRegistration(json(registration)) : undefined;
|
|
3639
|
-
if (value && (value.root !==
|
|
3658
|
+
if (value && (value.root !== root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
|
|
3640
3659
|
refused();
|
|
3660
|
+
if (stoppedOnly && (checkout || value && value.phase !== "stopped"))
|
|
3661
|
+
throw new ContractError("quarantine");
|
|
3641
3662
|
if (value && value.phase !== "stopped") {
|
|
3642
3663
|
if (!owner || !checkout || !capability)
|
|
3643
3664
|
refused();
|
|
@@ -3717,18 +3738,21 @@ class ServiceCoordination {
|
|
|
3717
3738
|
}
|
|
3718
3739
|
withRegistry(operation) {
|
|
3719
3740
|
this.files.directory(this.registryDirectory, true);
|
|
3720
|
-
names(this.registryDirectory,
|
|
3721
|
-
"format.json",
|
|
3722
|
-
"roots.json",
|
|
3723
|
-
"registry.lock"
|
|
3724
|
-
]);
|
|
3741
|
+
names(this.registryDirectory, registryNames);
|
|
3725
3742
|
const lockPath = this.registryPath("registry.lock");
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3743
|
+
let lock;
|
|
3744
|
+
try {
|
|
3745
|
+
lock = this.files.create(lockPath, encoded({
|
|
3746
|
+
schema: "cadence-service-registry-lock/1",
|
|
3747
|
+
token: randomUUID3(),
|
|
3748
|
+
pid: process.pid,
|
|
3749
|
+
host: hostname2()
|
|
3750
|
+
}));
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST")
|
|
3753
|
+
throw new ContractError("registry-locked");
|
|
3754
|
+
throw error;
|
|
3755
|
+
}
|
|
3732
3756
|
let dirty = false;
|
|
3733
3757
|
try {
|
|
3734
3758
|
let format = this.files.read(this.registryPath("format.json"));
|
|
@@ -3752,11 +3776,7 @@ class ServiceCoordination {
|
|
|
3752
3776
|
}, () => {
|
|
3753
3777
|
this.files.assert(lockPath, lock);
|
|
3754
3778
|
this.files.assert(this.registryPath("format.json"), format);
|
|
3755
|
-
names(this.registryDirectory,
|
|
3756
|
-
"format.json",
|
|
3757
|
-
"roots.json",
|
|
3758
|
-
"registry.lock"
|
|
3759
|
-
]);
|
|
3779
|
+
names(this.registryDirectory, registryNames);
|
|
3760
3780
|
});
|
|
3761
3781
|
this.files.remove(lockPath, lock);
|
|
3762
3782
|
return result;
|
|
@@ -3888,6 +3908,154 @@ class ServiceCoordination {
|
|
|
3888
3908
|
});
|
|
3889
3909
|
});
|
|
3890
3910
|
}
|
|
3911
|
+
registryLockDiagnostic() {
|
|
3912
|
+
try {
|
|
3913
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
3914
|
+
if (!lock)
|
|
3915
|
+
return;
|
|
3916
|
+
const value = decodeRegistryLock(lock);
|
|
3917
|
+
return {
|
|
3918
|
+
reason: "registry-locked",
|
|
3919
|
+
owner: recoveryOwnerStatus(value.pid, value.host)
|
|
3920
|
+
};
|
|
3921
|
+
} catch {
|
|
3922
|
+
return { reason: "registry-locked", owner: "unreadable" };
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
registryRecoveryState() {
|
|
3926
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
3927
|
+
if (!lock)
|
|
3928
|
+
throw new ContractError("conflict");
|
|
3929
|
+
const value = decodeRegistryLock(lock);
|
|
3930
|
+
names(this.registryDirectory, registryNames);
|
|
3931
|
+
const format = this.files.read(this.registryPath("format.json"));
|
|
3932
|
+
const roots = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
|
|
3933
|
+
const registry = !format && !roots ? undefined : this.registry();
|
|
3934
|
+
const entries = registry?.value.entries ?? [];
|
|
3935
|
+
const protectedRoots = new Set(entries.map((row) => hash(row.root)));
|
|
3936
|
+
const proofs = [];
|
|
3937
|
+
const directory2 = opendirSync(this.files.namespace);
|
|
3938
|
+
try {
|
|
3939
|
+
let count = 0;
|
|
3940
|
+
for (let entry = directory2.readSync();entry; entry = directory2.readSync()) {
|
|
3941
|
+
if (++count > 1024)
|
|
3942
|
+
throw new ContractError("capacity");
|
|
3943
|
+
if (!/^[a-f0-9]{64}$/.test(entry.name) || protectedRoots.has(entry.name))
|
|
3944
|
+
continue;
|
|
3945
|
+
const service = join5(this.files.namespace, entry.name, "service-v1");
|
|
3946
|
+
if (!this.files.directory(service))
|
|
3947
|
+
continue;
|
|
3948
|
+
const manifest = this.files.read(join5(service, "format.json"));
|
|
3949
|
+
if (!manifest)
|
|
3950
|
+
refused();
|
|
3951
|
+
const f = closed(json(manifest), [
|
|
3952
|
+
"schema",
|
|
3953
|
+
"root",
|
|
3954
|
+
"stateHome",
|
|
3955
|
+
"configIdentity"
|
|
3956
|
+
]);
|
|
3957
|
+
const root = pathValue(f.root);
|
|
3958
|
+
if (hash(root) !== entry.name)
|
|
3959
|
+
refused();
|
|
3960
|
+
const selected = this.selected(service, root, true);
|
|
3961
|
+
if (!selected)
|
|
3962
|
+
throw new ContractError("quarantine");
|
|
3963
|
+
proofs.push([
|
|
3964
|
+
entry.name,
|
|
3965
|
+
...[
|
|
3966
|
+
selected.manifest,
|
|
3967
|
+
selected.config,
|
|
3968
|
+
selected.hostConfig,
|
|
3969
|
+
selected.registration
|
|
3970
|
+
].map((file) => file ? [file.identity, file.stamp] : null)
|
|
3971
|
+
]);
|
|
3972
|
+
}
|
|
3973
|
+
} finally {
|
|
3974
|
+
directory2.closeSync();
|
|
3975
|
+
}
|
|
3976
|
+
proofs.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en"));
|
|
3977
|
+
const stateDigest = hash(encoded([
|
|
3978
|
+
lock.identity,
|
|
3979
|
+
lock.stamp,
|
|
3980
|
+
format ? [format.identity, format.stamp] : null,
|
|
3981
|
+
roots ? [roots.identity, roots.stamp] : null,
|
|
3982
|
+
proofs
|
|
3983
|
+
]));
|
|
3984
|
+
const epoch = hash(encoded([lock.identity, lock.stamp]));
|
|
3985
|
+
const archive = this.registryPath(join5("recovery-v1", epoch));
|
|
3986
|
+
const attempts = [];
|
|
3987
|
+
let previousResolver = null;
|
|
3988
|
+
if (this.files.directory(archive)) {
|
|
3989
|
+
names(archive, Array.from({ length: 128 }, (_, n) => `${n}.json`));
|
|
3990
|
+
for (let n = 0;n < 128; n++) {
|
|
3991
|
+
const attempt = this.files.read(join5(archive, `${n}.json`), 32768);
|
|
3992
|
+
if (!attempt) {
|
|
3993
|
+
names(archive, attempts.map((_, i) => `${i}.json`));
|
|
3994
|
+
break;
|
|
3995
|
+
}
|
|
3996
|
+
const o = closed(json(attempt, 32768), [
|
|
3997
|
+
"schema",
|
|
3998
|
+
"lock",
|
|
3999
|
+
"pid",
|
|
4000
|
+
"host",
|
|
4001
|
+
"digest"
|
|
4002
|
+
]);
|
|
4003
|
+
literal(o.schema, "cadence-registry-recovery/1");
|
|
4004
|
+
literal(o.lock, lock.bytes.toString("utf8"));
|
|
4005
|
+
decodeHash(o.digest);
|
|
4006
|
+
previousResolver = recoveryOwnerStatus(integer(o.pid, 1, 2147483647), text(o.host, 255, true));
|
|
4007
|
+
attempts.push(attempt);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
const inspection = {
|
|
4011
|
+
scope: "global-registry",
|
|
4012
|
+
epoch,
|
|
4013
|
+
coordinationDigest: hash(encoded([stateDigest, attempts.map((a) => [a.identity, a.stamp])])),
|
|
4014
|
+
owner: recoveryOwnerStatus(value.pid, value.host),
|
|
4015
|
+
process: { pid: value.pid, host: value.host },
|
|
4016
|
+
reservations: entries.length,
|
|
4017
|
+
previousResolver,
|
|
4018
|
+
resolution: "local-interactive-only"
|
|
4019
|
+
};
|
|
4020
|
+
return { lock, archive, attempts, stateDigest, inspection };
|
|
4021
|
+
}
|
|
4022
|
+
inspectRegistryRecovery() {
|
|
4023
|
+
return this.registryRecoveryState().inspection;
|
|
4024
|
+
}
|
|
4025
|
+
resolveRegistryRecovery(expected, assertAuthority) {
|
|
4026
|
+
assertAuthority();
|
|
4027
|
+
const state = this.registryRecoveryState();
|
|
4028
|
+
if (encoded(state.inspection) !== encoded(expected))
|
|
4029
|
+
throw new ContractError("conflict");
|
|
4030
|
+
if (expected.owner !== "absent" || expected.previousResolver !== null && expected.previousResolver !== "absent")
|
|
4031
|
+
throw new ContractError("quarantine");
|
|
4032
|
+
if (state.attempts.length >= 128)
|
|
4033
|
+
throw new ContractError("capacity");
|
|
4034
|
+
assertAuthority();
|
|
4035
|
+
const receiptPath = join5(state.archive, `${state.attempts.length}.json`);
|
|
4036
|
+
const receipt = this.files.create(receiptPath, encoded({
|
|
4037
|
+
schema: "cadence-registry-recovery/1",
|
|
4038
|
+
lock: state.lock.bytes.toString("utf8"),
|
|
4039
|
+
pid: process.pid,
|
|
4040
|
+
host: hostname2(),
|
|
4041
|
+
digest: expected.coordinationDigest
|
|
4042
|
+
}), 32768);
|
|
4043
|
+
assertAuthority();
|
|
4044
|
+
const current = this.registryRecoveryState();
|
|
4045
|
+
if (current.stateDigest !== state.stateDigest || current.inspection.owner !== "absent" || current.attempts.length !== state.attempts.length + 1)
|
|
4046
|
+
throw new ContractError("conflict");
|
|
4047
|
+
for (const [n, attempt] of state.attempts.entries())
|
|
4048
|
+
this.files.assert(join5(state.archive, `${n}.json`), attempt, 32768);
|
|
4049
|
+
this.files.assert(receiptPath, receipt, 32768);
|
|
4050
|
+
assertAuthority();
|
|
4051
|
+
this.files.remove(this.registryPath("registry.lock"), state.lock);
|
|
4052
|
+
return {
|
|
4053
|
+
status: "resolved",
|
|
4054
|
+
scope: "global-registry",
|
|
4055
|
+
recoveryId: expected.epoch,
|
|
4056
|
+
notice: "registry-only-project-fences-preserved"
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
3891
4059
|
prepareRecoveryRelease(expected, assertAuthority) {
|
|
3892
4060
|
assertAuthority();
|
|
3893
4061
|
if (this.recoverySnapshot().digest !== expected)
|
|
@@ -4037,165 +4205,1016 @@ class OwnerLease {
|
|
|
4037
4205
|
}
|
|
4038
4206
|
}
|
|
4039
4207
|
|
|
4040
|
-
// src/service/
|
|
4041
|
-
import {
|
|
4042
|
-
import { join as
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
"
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
string(v)
|
|
4080
|
-
]),
|
|
4081
|
-
"\t</dict>",
|
|
4082
|
-
"\t<key>RunAtLoad</key>",
|
|
4083
|
-
"\t<true/>",
|
|
4084
|
-
"\t<key>ProcessType</key>",
|
|
4085
|
-
"\t<string>Background</string>",
|
|
4086
|
-
"\t<key>StandardOutPath</key>",
|
|
4087
|
-
` <string>${xml(u.log)}</string>`,
|
|
4088
|
-
"\t<key>StandardErrorPath</key>",
|
|
4089
|
-
` <string>${xml(u.log)}</string>`,
|
|
4090
|
-
"</dict>",
|
|
4091
|
-
"</plist>",
|
|
4092
|
-
""
|
|
4093
|
-
].join(`
|
|
4094
|
-
`);
|
|
4095
|
-
}
|
|
4096
|
-
function plistWorkspace(text2) {
|
|
4097
|
-
const m = text2?.match(/^\t<key>WorkingDirectory<\/key>\n\t<string>([^<]*)<\/string>$/m);
|
|
4098
|
-
return m?.[1].replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
4208
|
+
// src/service/gateway-cli.ts
|
|
4209
|
+
import { homedir as homedir5 } from "node:os";
|
|
4210
|
+
import { dirname as dirname8, join as join9, resolve as resolve6 } from "node:path";
|
|
4211
|
+
import { fileURLToPath } from "node:url";
|
|
4212
|
+
|
|
4213
|
+
// src/service/config.ts
|
|
4214
|
+
import { isIP } from "node:net";
|
|
4215
|
+
function decodeHttpPolicy(value) {
|
|
4216
|
+
const o = closed(value, ["schema", "host", "port", "proxy"]);
|
|
4217
|
+
if (o.host !== "127.0.0.1" && o.host !== "::1")
|
|
4218
|
+
throw new ContractError("invalid");
|
|
4219
|
+
let proxy = null;
|
|
4220
|
+
if (o.proxy !== null) {
|
|
4221
|
+
const p = closed(o.proxy, ["origin", "trustedAddresses"]);
|
|
4222
|
+
const origin = text(p.origin, 256, true);
|
|
4223
|
+
let url;
|
|
4224
|
+
try {
|
|
4225
|
+
url = new URL(origin);
|
|
4226
|
+
} catch {
|
|
4227
|
+
throw new ContractError("invalid");
|
|
4228
|
+
}
|
|
4229
|
+
if (url.protocol !== "https:" || url.origin !== origin || url.username || url.password)
|
|
4230
|
+
throw new ContractError("invalid");
|
|
4231
|
+
const trustedAddresses = list(p.trustedAddresses, 8, (v) => {
|
|
4232
|
+
const address = text(v, 64, true);
|
|
4233
|
+
if (!isIP(address) || !loopbackAddress(address))
|
|
4234
|
+
throw new ContractError("invalid");
|
|
4235
|
+
return address;
|
|
4236
|
+
});
|
|
4237
|
+
if (!trustedAddresses.length || new Set(trustedAddresses).size !== trustedAddresses.length)
|
|
4238
|
+
throw new ContractError("invalid");
|
|
4239
|
+
proxy = { origin, trustedAddresses };
|
|
4240
|
+
}
|
|
4241
|
+
return {
|
|
4242
|
+
schema: literal(o.schema, "cadence-service-http/1"),
|
|
4243
|
+
host: o.host,
|
|
4244
|
+
port: integer(o.port, 0, 65535),
|
|
4245
|
+
proxy
|
|
4246
|
+
};
|
|
4099
4247
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4248
|
+
function loopbackAddress(host) {
|
|
4249
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
|
|
4250
|
+
const h = mapped ? mapped[1] : host;
|
|
4251
|
+
if (h.includes(":"))
|
|
4252
|
+
return h === "::1";
|
|
4253
|
+
const parts = h.split(".");
|
|
4254
|
+
return parts.length === 4 && parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255) && Number(parts[0]) === 127;
|
|
4105
4255
|
}
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
const path2 = plistPath(s);
|
|
4110
|
-
const previous = readGenerated(path2, MARKER2);
|
|
4111
|
-
if (previous === null)
|
|
4112
|
-
return { status: "foreign", unit: path2 };
|
|
4113
|
-
if (mode === "restart" && previous === undefined)
|
|
4114
|
-
return { status: "absent" };
|
|
4115
|
-
const sdk = sdkPackages(s.root);
|
|
4116
|
-
if (!sdk)
|
|
4117
|
-
return { status: "missing-pi-sdk" };
|
|
4118
|
-
const kept = plistWorkspace(previous);
|
|
4119
|
-
const { workspace, redirectedFrom } = selectWorkspace(s, requested, kept);
|
|
4120
|
-
const refused2 = await settle(s, kept);
|
|
4121
|
-
if (refused2)
|
|
4122
|
-
return { ...refused2, unit: path2 };
|
|
4123
|
-
if (loaded(s))
|
|
4124
|
-
s.launchctl(["bootout", target(s)]);
|
|
4125
|
-
write(path2, buildPlist({
|
|
4126
|
-
node: s.node,
|
|
4127
|
-
cli: join6(s.root, "src", "service-cli.mjs"),
|
|
4128
|
-
workspace,
|
|
4129
|
-
serverConfig: serverConfigPath(s.home),
|
|
4130
|
-
environment: serviceEnvironment(s),
|
|
4131
|
-
log: join6(s.home, "Library", "Logs", "cadence.log")
|
|
4132
|
-
}));
|
|
4133
|
-
const result = {
|
|
4134
|
-
unit: path2,
|
|
4135
|
-
workspace,
|
|
4136
|
-
...redirectedFrom ? { redirectedFrom } : {}
|
|
4137
|
-
};
|
|
4138
|
-
if (mode === "install")
|
|
4139
|
-
s.launchctl(["enable", target(s)]);
|
|
4140
|
-
else if (disabled(s))
|
|
4141
|
-
return { status: "disabled", ...result };
|
|
4142
|
-
if (!regularFile(join6(agentDirectory(s), "auth.json")))
|
|
4143
|
-
return { status: "waiting-for-pi-login", ...result };
|
|
4144
|
-
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
4145
|
-
return { status: started ? "started" : "failed", ...result };
|
|
4256
|
+
function localOrigin(policy, actualPort) {
|
|
4257
|
+
integer(actualPort, 1, 65535);
|
|
4258
|
+
return `http://${policy.host === "::1" ? "[::1]" : policy.host}:${actualPort}`;
|
|
4146
4259
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4260
|
+
|
|
4261
|
+
// src/service/gateway-store.ts
|
|
4262
|
+
import { randomBytes as randomBytes4, randomUUID as randomUUID5 } from "node:crypto";
|
|
4263
|
+
import { opendirSync as opendirSync2 } from "node:fs";
|
|
4264
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
4265
|
+
|
|
4266
|
+
// src/service/auth.ts
|
|
4267
|
+
import {
|
|
4268
|
+
createHash as createHash3,
|
|
4269
|
+
randomBytes as randomBytes3,
|
|
4270
|
+
randomUUID as randomUUID4,
|
|
4271
|
+
timingSafeEqual
|
|
4272
|
+
} from "node:crypto";
|
|
4273
|
+
import { dirname as dirname6, isAbsolute as isAbsolute5, join as join6 } from "node:path";
|
|
4274
|
+
function sameSecret(candidate, expected) {
|
|
4275
|
+
const a = createHash3("sha256").update(candidate).digest();
|
|
4276
|
+
const b = createHash3("sha256").update(expected).digest();
|
|
4277
|
+
return timingSafeEqual(a, b);
|
|
4278
|
+
}
|
|
4279
|
+
function buildTokenCookie(name, encodedValue, maxAge, secure) {
|
|
4280
|
+
return `${name}=${encodedValue}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
|
|
4281
|
+
}
|
|
4282
|
+
function decodeCookieToken(raw) {
|
|
4283
|
+
if (!raw)
|
|
4284
|
+
return "";
|
|
4285
|
+
try {
|
|
4286
|
+
return decodeURIComponent(raw);
|
|
4287
|
+
} catch {
|
|
4288
|
+
return raw;
|
|
4289
|
+
}
|
|
4157
4290
|
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4291
|
+
function cookieTokens(header, name) {
|
|
4292
|
+
if (!header)
|
|
4293
|
+
return [];
|
|
4294
|
+
let found = false;
|
|
4295
|
+
const out = [];
|
|
4296
|
+
for (const part of header.split(";")) {
|
|
4297
|
+
const [key, ...rest] = part.trim().split("=");
|
|
4298
|
+
if (key !== name)
|
|
4299
|
+
continue;
|
|
4300
|
+
if (found)
|
|
4301
|
+
throw new ContractError("invalid");
|
|
4302
|
+
found = true;
|
|
4303
|
+
const raw = rest.join("=").trim();
|
|
4304
|
+
out.push(raw, decodeCookieToken(raw));
|
|
4305
|
+
}
|
|
4306
|
+
return out;
|
|
4172
4307
|
}
|
|
4173
|
-
function
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4308
|
+
function sourceConfig(source) {
|
|
4309
|
+
const given = [source.environment, source.tokenFile, source.serverConfig];
|
|
4310
|
+
if (given.filter((v) => v !== undefined).length > 1)
|
|
4311
|
+
throw new ContractError("invalid");
|
|
4312
|
+
if (source.environment !== undefined)
|
|
4313
|
+
accessToken(source.environment);
|
|
4314
|
+
if (source.serverConfig !== undefined)
|
|
4315
|
+
accessToken(source.token);
|
|
4316
|
+
const path2 = source.tokenFile ?? source.serverConfig;
|
|
4317
|
+
if (path2 !== undefined && (!isAbsolute5(path2) || path2.includes("\x00")))
|
|
4318
|
+
throw new ContractError("invalid");
|
|
4181
4319
|
return {
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
workspace: plistWorkspace(previous)
|
|
4320
|
+
schema: "cadence-service-access-config/1",
|
|
4321
|
+
source: source.environment !== undefined ? "environment" : source.tokenFile !== undefined ? "file" : source.serverConfig !== undefined ? "config" : "generated",
|
|
4322
|
+
path: path2 ?? null
|
|
4186
4323
|
};
|
|
4187
4324
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4325
|
+
function readSource(config) {
|
|
4326
|
+
if (config.source === "file" && config.path)
|
|
4327
|
+
readExternal(config.path);
|
|
4328
|
+
}
|
|
4329
|
+
function selectedMaterial(files, directory2, source) {
|
|
4330
|
+
const config = sourceConfig(source);
|
|
4331
|
+
if (config.source !== "config")
|
|
4332
|
+
throw new ContractError("invalid");
|
|
4333
|
+
const marker = join6(directory2, "access-config.json"), access = join6(directory2, "access.json");
|
|
4334
|
+
const markerFile = files.read(marker), accessFile = files.read(access);
|
|
4335
|
+
if (!markerFile || !accessFile)
|
|
4336
|
+
throw new ContractError("corrupt-selected");
|
|
4337
|
+
const previous = closed(json(markerFile), ["schema", "source", "path"]);
|
|
4338
|
+
literal(previous.schema, "cadence-service-access-config/1");
|
|
4339
|
+
oneOf(previous.source, ["generated", "environment", "file", "config"]);
|
|
4340
|
+
const r = closed(json(accessFile), ["schema", "generation", "token"]);
|
|
4341
|
+
literal(r.schema, "cadence-service-access/1");
|
|
4342
|
+
const generation = integer(r.generation, 1);
|
|
4343
|
+
if (previous.source === "generated" !== (r.token !== null))
|
|
4344
|
+
throw new ContractError("corrupt-selected");
|
|
4345
|
+
return {
|
|
4346
|
+
config,
|
|
4347
|
+
previous,
|
|
4348
|
+
generation,
|
|
4349
|
+
marker,
|
|
4350
|
+
markerFile,
|
|
4351
|
+
access,
|
|
4352
|
+
accessFile
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
class AccessMaterial {
|
|
4357
|
+
files;
|
|
4358
|
+
directory;
|
|
4359
|
+
assertOwner;
|
|
4360
|
+
configFile;
|
|
4361
|
+
accessFile;
|
|
4362
|
+
externalFile;
|
|
4363
|
+
external;
|
|
4364
|
+
record;
|
|
4365
|
+
secret;
|
|
4366
|
+
fenced = false;
|
|
4367
|
+
listeners = new Set;
|
|
4368
|
+
config;
|
|
4369
|
+
static validateSource(source) {
|
|
4370
|
+
readSource(sourceConfig(source));
|
|
4371
|
+
}
|
|
4372
|
+
static initialize(files, directory2, source = {}) {
|
|
4373
|
+
const config = sourceConfig(source);
|
|
4374
|
+
const marker = join6(directory2, "access-config.json"), access = join6(directory2, "access.json");
|
|
4375
|
+
if (files.read(marker) || files.read(access) || files.read(join6(directory2, "registration.json")) || files.read(join6(directory2, "owner.lock")))
|
|
4376
|
+
throw new ContractError("conflict");
|
|
4377
|
+
readSource(config);
|
|
4378
|
+
files.create(marker, encoded(config));
|
|
4379
|
+
files.create(access, encoded({
|
|
4380
|
+
schema: "cadence-service-access/1",
|
|
4381
|
+
generation: 1,
|
|
4382
|
+
token: config.source === "generated" ? randomBytes3(32).toString("base64url") : null
|
|
4383
|
+
}));
|
|
4384
|
+
}
|
|
4385
|
+
static validateReselection(files, directory2, source) {
|
|
4386
|
+
selectedMaterial(files, directory2, source);
|
|
4387
|
+
}
|
|
4388
|
+
static reselect(files, directory2, source) {
|
|
4389
|
+
const m = selectedMaterial(files, directory2, source);
|
|
4390
|
+
if (encoded(m.previous) === encoded(m.config))
|
|
4391
|
+
return;
|
|
4392
|
+
files.replace(m.marker, m.markerFile, encoded(m.config));
|
|
4393
|
+
files.replace(m.access, m.accessFile, encoded({
|
|
4394
|
+
schema: "cadence-service-access/1",
|
|
4395
|
+
generation: integer(m.generation + 1, 1),
|
|
4396
|
+
token: null
|
|
4397
|
+
}));
|
|
4398
|
+
}
|
|
4399
|
+
constructor(files, directory2, source = {}, assertOwner) {
|
|
4400
|
+
this.files = files;
|
|
4401
|
+
this.directory = directory2;
|
|
4402
|
+
this.assertOwner = assertOwner;
|
|
4403
|
+
this.config = sourceConfig(source);
|
|
4404
|
+
const configFile = files.read(join6(directory2, "access-config.json"));
|
|
4405
|
+
const accessFile = files.read(join6(directory2, "access.json"));
|
|
4406
|
+
if (!configFile || !accessFile)
|
|
4407
|
+
throw new ContractError("corrupt-selected");
|
|
4408
|
+
const c = closed(json(configFile), ["schema", "source", "path"]);
|
|
4409
|
+
if (encoded(c) !== encoded(this.config))
|
|
4410
|
+
throw new ContractError("conflict");
|
|
4411
|
+
const r = closed(json(accessFile), ["schema", "generation", "token"]);
|
|
4412
|
+
this.record = {
|
|
4413
|
+
schema: literal(r.schema, "cadence-service-access/1"),
|
|
4414
|
+
generation: integer(r.generation, 1),
|
|
4415
|
+
token: r.token === null ? null : accessToken(r.token)
|
|
4416
|
+
};
|
|
4417
|
+
if (this.config.source === "generated" !== (this.record.token !== null))
|
|
4418
|
+
throw new ContractError("corrupt-selected");
|
|
4419
|
+
if (this.config.source === "file" && this.config.path) {
|
|
4420
|
+
const loaded = readExternal(this.config.path);
|
|
4421
|
+
this.external = loaded.files;
|
|
4422
|
+
this.externalFile = loaded.file;
|
|
4423
|
+
this.secret = loaded.token;
|
|
4424
|
+
} else
|
|
4425
|
+
this.secret = accessToken(this.record.token ?? source.environment ?? source.token);
|
|
4426
|
+
this.configFile = configFile;
|
|
4427
|
+
this.accessFile = accessFile;
|
|
4428
|
+
}
|
|
4429
|
+
get generation() {
|
|
4430
|
+
return this.record.generation;
|
|
4431
|
+
}
|
|
4432
|
+
get available() {
|
|
4433
|
+
return !this.fenced;
|
|
4434
|
+
}
|
|
4435
|
+
matches(candidate) {
|
|
4436
|
+
return !this.fenced && utf8Bytes(candidate) <= TOKEN_BYTES && sameSecret(candidate, this.secret);
|
|
4437
|
+
}
|
|
4438
|
+
onRevoke(listener) {
|
|
4439
|
+
this.listeners.add(listener);
|
|
4440
|
+
return () => {
|
|
4441
|
+
this.listeners.delete(listener);
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
revoke() {
|
|
4445
|
+
for (const listener of this.listeners) {
|
|
4446
|
+
try {
|
|
4447
|
+
listener();
|
|
4448
|
+
} catch {
|
|
4449
|
+
this.fenced = true;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
assertCurrent() {
|
|
4454
|
+
try {
|
|
4455
|
+
this.assert();
|
|
4456
|
+
} catch {
|
|
4457
|
+
this.fenced = true;
|
|
4458
|
+
this.revoke();
|
|
4459
|
+
throw new ContractError("quarantine");
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
assert() {
|
|
4463
|
+
if (this.fenced)
|
|
4464
|
+
throw new ContractError("quarantine");
|
|
4465
|
+
this.files.assert(join6(this.directory, "access-config.json"), this.configFile);
|
|
4466
|
+
this.files.assert(join6(this.directory, "access.json"), this.accessFile);
|
|
4467
|
+
if (this.config.source === "file" && this.config.path)
|
|
4468
|
+
this.external?.assert(this.config.path, this.externalFile, TOKEN_BYTES);
|
|
4469
|
+
}
|
|
4470
|
+
redactionLiterals() {
|
|
4471
|
+
this.assertCurrent();
|
|
4472
|
+
return [this.secret];
|
|
4473
|
+
}
|
|
4474
|
+
tokenLink(origin) {
|
|
4475
|
+
this.assert();
|
|
4476
|
+
const url = new URL(origin);
|
|
4477
|
+
if (url.origin !== origin)
|
|
4478
|
+
throw new ContractError("invalid");
|
|
4479
|
+
return `${origin}/?token=${encodeURIComponent(this.secret)}`;
|
|
4480
|
+
}
|
|
4481
|
+
assertRotation() {
|
|
4482
|
+
this.assertOwner?.();
|
|
4483
|
+
this.assert();
|
|
4484
|
+
}
|
|
4485
|
+
rotate() {
|
|
4486
|
+
if (this.config.source === "environment")
|
|
4487
|
+
throw new ContractError("unsupported-context");
|
|
4488
|
+
let configured;
|
|
4489
|
+
if (this.config.source === "config" && this.config.path) {
|
|
4490
|
+
configured = readServerConfig(this.config.path);
|
|
4491
|
+
if (!sameSecret(configured.config.token, this.secret))
|
|
4492
|
+
throw new ContractError("conflict");
|
|
4493
|
+
}
|
|
4494
|
+
try {
|
|
4495
|
+
this.assertRotation();
|
|
4496
|
+
const secret = randomBytes3(32).toString("base64url");
|
|
4497
|
+
const generation = integer(this.record.generation + 1, 1);
|
|
4498
|
+
if (this.config.path && this.external && this.externalFile)
|
|
4499
|
+
this.externalFile = this.external.replace(this.config.path, this.externalFile, secret, TOKEN_BYTES);
|
|
4500
|
+
if (configured)
|
|
4501
|
+
configured.files.replace(configured.real, configured.snapshot, withToken(configured, secret), CONFIG_BYTES);
|
|
4502
|
+
const record = {
|
|
4503
|
+
...this.record,
|
|
4504
|
+
generation,
|
|
4505
|
+
token: this.config.source === "generated" ? secret : null
|
|
4506
|
+
};
|
|
4507
|
+
this.assertRotation();
|
|
4508
|
+
this.accessFile = this.files.replace(join6(this.directory, "access.json"), this.accessFile, encoded(record));
|
|
4509
|
+
this.assertRotation();
|
|
4510
|
+
this.record = record;
|
|
4511
|
+
this.secret = secret;
|
|
4512
|
+
this.revoke();
|
|
4513
|
+
if (this.fenced)
|
|
4514
|
+
throw new ContractError("quarantine");
|
|
4515
|
+
} catch {
|
|
4516
|
+
this.fenced = true;
|
|
4517
|
+
this.revoke();
|
|
4518
|
+
throw new ContractError("quarantine");
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
function readExternal(path2) {
|
|
4523
|
+
const files = new PrivateFiles(dirname6(path2));
|
|
4524
|
+
const file = files.read(path2, TOKEN_BYTES);
|
|
4525
|
+
if (!file)
|
|
4526
|
+
throw new ContractError("corrupt-selected");
|
|
4527
|
+
try {
|
|
4528
|
+
return {
|
|
4529
|
+
files,
|
|
4530
|
+
file,
|
|
4531
|
+
token: accessToken(new TextDecoder("utf-8", { fatal: true }).decode(file.bytes))
|
|
4532
|
+
};
|
|
4533
|
+
} catch {
|
|
4534
|
+
throw new ContractError("corrupt-selected");
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
class HttpAuthentication {
|
|
4538
|
+
material;
|
|
4539
|
+
epoch;
|
|
4540
|
+
localCapability;
|
|
4541
|
+
csrf = randomUUID4();
|
|
4542
|
+
cookieName;
|
|
4543
|
+
constructor(material, epoch, workspaceId, localCapability) {
|
|
4544
|
+
this.material = material;
|
|
4545
|
+
this.epoch = epoch;
|
|
4546
|
+
this.localCapability = localCapability;
|
|
4547
|
+
decodeId(epoch);
|
|
4548
|
+
decodeId(workspaceId);
|
|
4549
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(localCapability))
|
|
4550
|
+
throw new ContractError("invalid");
|
|
4551
|
+
this.cookieName = `cadence_${workspaceId.replaceAll("-", "")}`;
|
|
4552
|
+
}
|
|
4553
|
+
bootstrap(value, boundary) {
|
|
4554
|
+
this.material.assertCurrent();
|
|
4555
|
+
const h = boundary.headers;
|
|
4556
|
+
const site = h.get("sec-fetch-site");
|
|
4557
|
+
if (site !== undefined && !["same-origin", "none"].includes(site) && (!["same-site", "cross-site"].includes(site) || h.get("sec-fetch-mode") !== "navigate" || h.get("sec-fetch-dest") !== "document"))
|
|
4558
|
+
throw new ContractError("forbidden");
|
|
4559
|
+
if (!this.material.matches(value))
|
|
4560
|
+
throw new ContractError("unauthenticated");
|
|
4561
|
+
return buildTokenCookie(this.cookieName, encodeURIComponent(value), LIMITS.browserLifetimeMs / 1000, boundary.secure);
|
|
4562
|
+
}
|
|
4563
|
+
authenticate(boundary, attachmentId, mutation = false, websocket = false) {
|
|
4564
|
+
this.material.assertCurrent();
|
|
4565
|
+
if (!this.material.available)
|
|
4566
|
+
throw new ContractError("quarantine");
|
|
4567
|
+
const h = boundary.headers;
|
|
4568
|
+
const local = h.get("x-cadence-local");
|
|
4569
|
+
if (local !== undefined) {
|
|
4570
|
+
if (!boundary.local || h.has("cookie") || [...h.keys()].some((name) => name.startsWith("sec-fetch-") && (name !== "sec-fetch-mode" || h.get(name) !== "cors")) || !sameSecret(local, this.localCapability))
|
|
4571
|
+
throw new ContractError("forbidden");
|
|
4572
|
+
} else {
|
|
4573
|
+
if (h.has("authorization") || h.has("x-cadence-token"))
|
|
4574
|
+
throw new ContractError("forbidden");
|
|
4575
|
+
if (!cookieTokens(h.get("cookie"), this.cookieName).some((v) => this.material.matches(v)))
|
|
4576
|
+
throw new ContractError("unauthenticated");
|
|
4577
|
+
if (h.has("sec-fetch-site") && !["same-origin", "none"].includes(h.get("sec-fetch-site") ?? ""))
|
|
4578
|
+
throw new ContractError("forbidden");
|
|
4579
|
+
if ((mutation || websocket) && h.get("origin") !== boundary.origin)
|
|
4580
|
+
throw new ContractError("forbidden");
|
|
4581
|
+
if (mutation && h.get("x-cadence-csrf") !== this.csrf)
|
|
4582
|
+
throw new ContractError("forbidden");
|
|
4583
|
+
}
|
|
4584
|
+
return {
|
|
4585
|
+
attachmentId: decodeId(attachmentId),
|
|
4586
|
+
authority: local === undefined ? "browser" : "local",
|
|
4587
|
+
accessGeneration: this.material.generation
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
current(caller) {
|
|
4591
|
+
this.material.assertCurrent();
|
|
4592
|
+
if (!this.material.available || caller.accessGeneration !== this.material.generation)
|
|
4593
|
+
throw new ContractError("unauthenticated");
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
// src/service/contracts/gateway.ts
|
|
4598
|
+
function decodeProjectEntry(value) {
|
|
4599
|
+
const o = closed(value, ["id", "path", "cwd", "epoch"]);
|
|
4600
|
+
return {
|
|
4601
|
+
id: decodeHash(o.id),
|
|
4602
|
+
path: text(o.path, 4096, true),
|
|
4603
|
+
cwd: text(o.cwd, 4096, true),
|
|
4604
|
+
epoch: decodeId(o.epoch)
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
// src/service/gateway-store.ts
|
|
4609
|
+
function decodeGatewayInput(value) {
|
|
4610
|
+
const o = closed(value, [
|
|
4611
|
+
"kind",
|
|
4612
|
+
"serverConfig",
|
|
4613
|
+
"stateHome",
|
|
4614
|
+
"agentDir",
|
|
4615
|
+
"policy"
|
|
4616
|
+
]);
|
|
4617
|
+
const absolute = (v) => {
|
|
4618
|
+
const p = text(v, 4096, true);
|
|
4619
|
+
if (resolve4(p) !== p)
|
|
4620
|
+
throw new ContractError("invalid");
|
|
4621
|
+
return p;
|
|
4622
|
+
};
|
|
4623
|
+
return {
|
|
4624
|
+
kind: literal(o.kind, "workbench"),
|
|
4625
|
+
serverConfig: absolute(o.serverConfig),
|
|
4626
|
+
stateHome: absolute(o.stateHome),
|
|
4627
|
+
agentDir: absolute(o.agentDir),
|
|
4628
|
+
policy: decodeHttpPolicy(o.policy)
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
function decodeGatewayProject(value) {
|
|
4632
|
+
const project = decodeProjectEntry(value);
|
|
4633
|
+
if (resolve4(project.path) !== project.path || resolve4(project.cwd) !== project.cwd)
|
|
4634
|
+
throw new ContractError("corrupt-selected");
|
|
4635
|
+
return project;
|
|
4636
|
+
}
|
|
4637
|
+
function registration(value) {
|
|
4638
|
+
const o = closed(value, [
|
|
4639
|
+
"schema",
|
|
4640
|
+
"epoch",
|
|
4641
|
+
"id",
|
|
4642
|
+
"endpoint",
|
|
4643
|
+
"capability",
|
|
4644
|
+
"selection",
|
|
4645
|
+
"pid",
|
|
4646
|
+
"phase"
|
|
4647
|
+
]);
|
|
4648
|
+
const endpoint2 = text(o.endpoint, 128, true), capability = text(o.capability, 43, true);
|
|
4649
|
+
if (!/^http:\/\/(?:127\.0\.0\.1|\[::1\]):[1-9][0-9]{0,4}$/.test(endpoint2) || !/^[A-Za-z0-9_-]{43}$/.test(capability))
|
|
4650
|
+
throw new ContractError("corrupt-selected");
|
|
4651
|
+
return {
|
|
4652
|
+
schema: literal(o.schema, "cadence-workbench-registration/1"),
|
|
4653
|
+
epoch: decodeId(o.epoch),
|
|
4654
|
+
id: decodeId(o.id),
|
|
4655
|
+
endpoint: endpoint2,
|
|
4656
|
+
capability,
|
|
4657
|
+
selection: decodeHash(o.selection),
|
|
4658
|
+
pid: integer(o.pid, 1),
|
|
4659
|
+
phase: oneOf(o.phase, ["alive", "stopping", "stopped", "quarantined"])
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
class GatewayStore {
|
|
4664
|
+
serverConfig;
|
|
4665
|
+
files;
|
|
4666
|
+
directory;
|
|
4667
|
+
stateHome;
|
|
4668
|
+
lock;
|
|
4669
|
+
selected;
|
|
4670
|
+
inputFile;
|
|
4671
|
+
value;
|
|
4672
|
+
constructor(serverConfig, stateHome = defaultStateHome()) {
|
|
4673
|
+
this.serverConfig = serverConfig;
|
|
4674
|
+
this.stateHome = canonicalAmbientRoot(stateHome);
|
|
4675
|
+
this.files = new PrivateFiles(join7(this.stateHome, "cadence-assistant-v1"));
|
|
4676
|
+
this.directory = join7(this.files.namespace, "workbenches-v1", hash(resolve4(serverConfig)));
|
|
4677
|
+
}
|
|
4678
|
+
path(name) {
|
|
4679
|
+
return join7(this.directory, name);
|
|
4680
|
+
}
|
|
4681
|
+
read() {
|
|
4682
|
+
const file = this.files.read(this.path("gateway.json"));
|
|
4683
|
+
if (!file) {
|
|
4684
|
+
if (this.files.directory(this.directory))
|
|
4685
|
+
throw new ContractError("corrupt-selected");
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
const value = registration(json(file));
|
|
4689
|
+
const selected = this.files.read(this.path("input.json"));
|
|
4690
|
+
if (!selected || hash(selected.bytes) !== value.selection)
|
|
4691
|
+
throw new ContractError("corrupt-selected");
|
|
4692
|
+
decodeGatewayInput(json(selected));
|
|
4693
|
+
return value;
|
|
4694
|
+
}
|
|
4695
|
+
localAccess() {
|
|
4696
|
+
const value = this.read();
|
|
4697
|
+
if (!value || value.phase === "stopped")
|
|
4698
|
+
throw new ContractError("unavailable");
|
|
4699
|
+
return {
|
|
4700
|
+
endpoint: value.endpoint,
|
|
4701
|
+
epoch: value.epoch,
|
|
4702
|
+
capability: value.capability
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
inspect() {
|
|
4706
|
+
const value = this.read();
|
|
4707
|
+
if (!value)
|
|
4708
|
+
return { status: "absent" };
|
|
4709
|
+
return {
|
|
4710
|
+
status: value.phase === "stopped" && this.files.read(this.path("gateway.lock")) ? "stopping" : value.phase,
|
|
4711
|
+
epoch: value.epoch
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4714
|
+
acquire(endpoint2, token, input) {
|
|
4715
|
+
const previous = this.read();
|
|
4716
|
+
if (previous && previous.phase !== "stopped")
|
|
4717
|
+
throw new ContractError("quarantine");
|
|
4718
|
+
this.lock = this.files.create(this.path("gateway.lock"), encoded({ epoch: randomUUID5() }));
|
|
4719
|
+
const source = { serverConfig: this.serverConfig, token };
|
|
4720
|
+
if (!previous)
|
|
4721
|
+
AccessMaterial.initialize(this.files, this.directory, source);
|
|
4722
|
+
const access = new AccessMaterial(this.files, this.directory, source, () => this.assert());
|
|
4723
|
+
this.selected = this.files.read(this.path("gateway.json"));
|
|
4724
|
+
if (previous && (!this.selected || encoded(registration(json(this.selected))) !== encoded(previous)))
|
|
4725
|
+
throw new ContractError("conflict");
|
|
4726
|
+
const beforeInput = this.files.read(this.path("input.json"));
|
|
4727
|
+
const inputBytes = encoded(decodeGatewayInput(input));
|
|
4728
|
+
this.inputFile = beforeInput ? this.files.replace(this.path("input.json"), beforeInput, inputBytes) : this.files.create(this.path("input.json"), inputBytes);
|
|
4729
|
+
this.value = {
|
|
4730
|
+
schema: "cadence-workbench-registration/1",
|
|
4731
|
+
epoch: randomUUID5(),
|
|
4732
|
+
id: previous?.id ?? randomUUID5(),
|
|
4733
|
+
endpoint: endpoint2,
|
|
4734
|
+
capability: randomBytes4(32).toString("base64url"),
|
|
4735
|
+
selection: hash(inputBytes),
|
|
4736
|
+
pid: process.pid,
|
|
4737
|
+
phase: "alive"
|
|
4738
|
+
};
|
|
4739
|
+
this.selected = this.selected ? this.files.replace(this.path("gateway.json"), this.selected, encoded(this.value)) : this.files.create(this.path("gateway.json"), encoded(this.value));
|
|
4740
|
+
return { registration: this.value, access };
|
|
4741
|
+
}
|
|
4742
|
+
assert() {
|
|
4743
|
+
if (!this.lock || !this.selected)
|
|
4744
|
+
throw new ContractError("unavailable");
|
|
4745
|
+
this.files.assert(this.path("gateway.lock"), this.lock);
|
|
4746
|
+
this.files.assert(this.path("input.json"), this.inputFile);
|
|
4747
|
+
this.files.assert(this.path("gateway.json"), this.selected);
|
|
4748
|
+
}
|
|
4749
|
+
phase(phase) {
|
|
4750
|
+
this.assert();
|
|
4751
|
+
this.value = { ...this.value, phase };
|
|
4752
|
+
this.selected = this.files.replace(this.path("gateway.json"), this.selected, encoded(this.value));
|
|
4753
|
+
}
|
|
4754
|
+
release() {
|
|
4755
|
+
this.phase("stopped");
|
|
4756
|
+
this.files.remove(this.path("gateway.lock"), this.lock);
|
|
4757
|
+
}
|
|
4758
|
+
projects() {
|
|
4759
|
+
const directory2 = this.path("projects");
|
|
4760
|
+
if (!this.files.directory(directory2))
|
|
4761
|
+
return [];
|
|
4762
|
+
const stream = opendirSync2(directory2), out = [];
|
|
4763
|
+
try {
|
|
4764
|
+
for (let entry = stream.readSync();entry; entry = stream.readSync()) {
|
|
4765
|
+
if (!entry.isFile() || !/^[a-f0-9]{64}\.json$/.test(entry.name) || out.length >= 128)
|
|
4766
|
+
throw new ContractError("corrupt-selected");
|
|
4767
|
+
const file = this.files.read(join7(directory2, entry.name));
|
|
4768
|
+
if (!file)
|
|
4769
|
+
throw new ContractError("corrupt-selected");
|
|
4770
|
+
const project = decodeGatewayProject(json(file));
|
|
4771
|
+
if (entry.name !== `${project.id}.json` || hash(project.path) !== project.id)
|
|
4772
|
+
throw new ContractError("corrupt-selected");
|
|
4773
|
+
out.push(project);
|
|
4774
|
+
}
|
|
4775
|
+
} finally {
|
|
4776
|
+
stream.closeSync();
|
|
4777
|
+
}
|
|
4778
|
+
return out;
|
|
4779
|
+
}
|
|
4780
|
+
saveProject(project) {
|
|
4781
|
+
this.assert();
|
|
4782
|
+
const p = decodeGatewayProject(project), path2 = this.path(`projects/${p.id}.json`), before = this.files.read(path2);
|
|
4783
|
+
if (!before && this.projects().length >= 128)
|
|
4784
|
+
throw new ContractError("capacity");
|
|
4785
|
+
if (before) {
|
|
4786
|
+
const old = decodeGatewayProject(json(before));
|
|
4787
|
+
if (old.id !== p.id || old.path !== p.path)
|
|
4788
|
+
throw new ContractError("conflict");
|
|
4789
|
+
this.files.replace(path2, before, encoded(p));
|
|
4790
|
+
} else
|
|
4791
|
+
this.files.create(path2, encoded(p));
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
// src/service/runtime.ts
|
|
4796
|
+
import { homedir as homedir4 } from "node:os";
|
|
4797
|
+
import { dirname as dirname7, isAbsolute as isAbsolute6, join as join8, resolve as resolve5 } from "node:path";
|
|
4798
|
+
var STARTUP_INPUT_BYTES = 16 * 1024;
|
|
4799
|
+
function absolute(value) {
|
|
4800
|
+
const result = text(value, 4096, true);
|
|
4801
|
+
if (!isAbsolute6(result))
|
|
4802
|
+
throw new ContractError("invalid");
|
|
4803
|
+
return result;
|
|
4804
|
+
}
|
|
4805
|
+
function decodeHostConfig(value) {
|
|
4806
|
+
const o = closed(value, [
|
|
4807
|
+
"schema",
|
|
4808
|
+
"http",
|
|
4809
|
+
"cwd",
|
|
4810
|
+
"agentDir",
|
|
4811
|
+
"tokenSource",
|
|
4812
|
+
"tokenFile"
|
|
4813
|
+
]);
|
|
4814
|
+
const tokenSource = oneOf(o.tokenSource, [
|
|
4815
|
+
"generated",
|
|
4816
|
+
"environment",
|
|
4817
|
+
"file",
|
|
4818
|
+
"config"
|
|
4819
|
+
]);
|
|
4820
|
+
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
4821
|
+
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
4822
|
+
throw new ContractError("invalid");
|
|
4823
|
+
return {
|
|
4824
|
+
schema: literal(o.schema, "cadence-service-host/1"),
|
|
4825
|
+
http: decodeHttpPolicy(o.http),
|
|
4826
|
+
cwd: absolute(o.cwd),
|
|
4827
|
+
agentDir: absolute(o.agentDir),
|
|
4828
|
+
tokenSource,
|
|
4829
|
+
tokenFile
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
function readHostConfig(c) {
|
|
4833
|
+
const owner = c.configuration();
|
|
4834
|
+
if (!owner)
|
|
4835
|
+
return;
|
|
4836
|
+
const snapshot = c.files.read(c.path("host-config.json"));
|
|
4837
|
+
if (!snapshot)
|
|
4838
|
+
throw new ContractError("corrupt-selected");
|
|
4839
|
+
const config = decodeHostConfig(json(snapshot));
|
|
4840
|
+
if (!config.http.port || owner.endpoint !== localOrigin(config.http, config.http.port) || owner.policyIdentity !== hash(encoded(config)))
|
|
4841
|
+
throw new ContractError("conflict");
|
|
4842
|
+
return config;
|
|
4843
|
+
}
|
|
4844
|
+
function hostEnvironment(ambient) {
|
|
4845
|
+
let route;
|
|
4846
|
+
const facts = networkSnapshot(ambient);
|
|
4847
|
+
try {
|
|
4848
|
+
if (facts.NODE_OPTIONS.present)
|
|
4849
|
+
throw new Error;
|
|
4850
|
+
route = sdkNetworkEnvironment(facts, networkSnapshot(undefined));
|
|
4851
|
+
} catch {
|
|
4852
|
+
throw new ContractError("unsupported-context");
|
|
4853
|
+
}
|
|
4854
|
+
const result = Object.create(null);
|
|
4855
|
+
for (const key of Object.keys(ambient)) {
|
|
4856
|
+
const d = Object.getOwnPropertyDescriptor(ambient, key);
|
|
4857
|
+
if (!d || !("value" in d) || typeof d.value !== "string")
|
|
4858
|
+
throw new ContractError("invalid");
|
|
4859
|
+
if (/^(?:CADENCE_|PI_|ABEL_|TRELLIS_|GIT_)/i.test(key) || networkKeys.some((name) => name === key))
|
|
4860
|
+
continue;
|
|
4861
|
+
result[key] = d.value;
|
|
4862
|
+
}
|
|
4863
|
+
if (route.redactionLiterals.length) {
|
|
4864
|
+
result.HTTP_PROXY = route.env.HTTP_PROXY;
|
|
4865
|
+
result.HTTPS_PROXY = route.env.HTTPS_PROXY;
|
|
4866
|
+
result.NODE_USE_ENV_PROXY = "1";
|
|
4867
|
+
}
|
|
4868
|
+
result.PI_OFFLINE = "1";
|
|
4869
|
+
result.NO_COLOR = "1";
|
|
4870
|
+
if (!sameNetwork(facts, networkSnapshot(ambient)))
|
|
4871
|
+
throw new ContractError("stale");
|
|
4872
|
+
return result;
|
|
4873
|
+
}
|
|
4874
|
+
function selectStart(options, ambient) {
|
|
4875
|
+
const workspace = resolveWorkspace(options.workspace), c = new ServiceCoordination(workspace.root, options.stateHome);
|
|
4876
|
+
const old = readHostConfig(c);
|
|
4877
|
+
const env = Object.getOwnPropertyDescriptor(ambient, "CADENCE_WEB_TOKEN");
|
|
4878
|
+
if (env && (!("value" in env) || typeof env.value !== "string"))
|
|
4879
|
+
throw new ContractError("invalid");
|
|
4880
|
+
let fromFile;
|
|
4881
|
+
if (options.config) {
|
|
4882
|
+
const path2 = resolve5(options.config), f = new PrivateFiles(dirname7(path2));
|
|
4883
|
+
const bytes = f.read(path2);
|
|
4884
|
+
if (!bytes)
|
|
4885
|
+
throw new ContractError("corrupt-selected");
|
|
4886
|
+
fromFile = decodeHostConfig(json(bytes));
|
|
4887
|
+
}
|
|
4888
|
+
const base = old ?? fromFile ?? decodeHostConfig({
|
|
4889
|
+
schema: "cadence-service-host/1",
|
|
4890
|
+
cwd: workspace.cwd,
|
|
4891
|
+
http: {
|
|
4892
|
+
schema: "cadence-service-http/1",
|
|
4893
|
+
host: options.host ?? "127.0.0.1",
|
|
4894
|
+
port: options.port ?? 17423,
|
|
4895
|
+
proxy: null
|
|
4896
|
+
},
|
|
4897
|
+
agentDir: resolve5(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join8(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
4898
|
+
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
4899
|
+
tokenFile: options.tokenFile ? resolve5(options.tokenFile) : null
|
|
4900
|
+
});
|
|
4901
|
+
const server = options.serverConfig !== undefined ? resolve5(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
|
|
4902
|
+
let selected = base;
|
|
4903
|
+
if (server) {
|
|
4904
|
+
if (options.tokenFile !== undefined)
|
|
4905
|
+
throw new ContractError("conflict");
|
|
4906
|
+
const { port } = readServerConfig(server).config;
|
|
4907
|
+
selected = decodeHostConfig({
|
|
4908
|
+
...base,
|
|
4909
|
+
http: { ...base.http, port },
|
|
4910
|
+
tokenSource: "config",
|
|
4911
|
+
tokenFile: server
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
if (fromFile && encoded(fromFile) !== encoded(selected) || options.port !== undefined && options.port !== selected.http.port || options.host !== undefined && options.host !== selected.http.host || options.agentDir !== undefined && resolve5(options.agentDir) !== selected.agentDir || options.tokenFile !== undefined && resolve5(options.tokenFile) !== selected.tokenFile || env && selected.tokenSource !== "environment" || selected.tokenSource === "environment" && !env || resolveWorkspace(selected.cwd).root !== workspace.root)
|
|
4915
|
+
throw new ContractError("conflict");
|
|
4916
|
+
return {
|
|
4917
|
+
workspace: workspace.root,
|
|
4918
|
+
stateHome: c.stateHome,
|
|
4919
|
+
config: selected,
|
|
4920
|
+
environmentToken: env?.value ?? null
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4923
|
+
function decodeHostInput(value) {
|
|
4924
|
+
const o = closed(value, [
|
|
4925
|
+
"workspace",
|
|
4926
|
+
"stateHome",
|
|
4927
|
+
"config",
|
|
4928
|
+
"environmentToken"
|
|
4929
|
+
]);
|
|
4930
|
+
const config = decodeHostConfig(o.config);
|
|
4931
|
+
const environmentToken = o.environmentToken === null ? null : text(o.environmentToken, 1024, true);
|
|
4932
|
+
if (config.tokenSource === "environment" !== (environmentToken !== null))
|
|
4933
|
+
throw new ContractError("invalid");
|
|
4934
|
+
return {
|
|
4935
|
+
workspace: absolute(o.workspace),
|
|
4936
|
+
stateHome: absolute(o.stateHome),
|
|
4937
|
+
config,
|
|
4938
|
+
environmentToken
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
function portValue(value) {
|
|
4942
|
+
if (!/^(0|[1-9][0-9]{0,4})$/.test(value))
|
|
4943
|
+
throw new ContractError("invalid");
|
|
4944
|
+
return integer(Number(value), 0, 65535);
|
|
4945
|
+
}
|
|
4946
|
+
var STARTUP_STAGES = [
|
|
4947
|
+
"package",
|
|
4948
|
+
"pi-agent-directory",
|
|
4949
|
+
"pi-auth",
|
|
4950
|
+
"pi-models",
|
|
4951
|
+
"pi-runtime",
|
|
4952
|
+
"service"
|
|
4953
|
+
];
|
|
4954
|
+
function decodeStartupReport(value) {
|
|
4955
|
+
const o = closed(value, ["status", "error", "stage"]);
|
|
4956
|
+
return {
|
|
4957
|
+
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
4958
|
+
error: oneOf(o.error, ERROR_CODES),
|
|
4959
|
+
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
// src/service/gateway-cli.ts
|
|
4964
|
+
async function gatewayMain(argv, hostUrl) {
|
|
4965
|
+
const operation = argv.shift();
|
|
4966
|
+
if (![
|
|
4967
|
+
"start",
|
|
4968
|
+
"status",
|
|
4969
|
+
"diagnose",
|
|
4970
|
+
"stop",
|
|
4971
|
+
"token-link",
|
|
4972
|
+
"token-rotate"
|
|
4973
|
+
].includes(operation ?? ""))
|
|
4974
|
+
throw new ContractError("invalid");
|
|
4975
|
+
let config = serverConfigPath(homedir5()), stateHome = defaultStateHome(), agentDir, policyFile;
|
|
4976
|
+
let foreground = false, reveal = false;
|
|
4977
|
+
const seen = new Set;
|
|
4978
|
+
while (argv.length) {
|
|
4979
|
+
const key = argv.shift();
|
|
4980
|
+
if (seen.has(key))
|
|
4981
|
+
throw new ContractError("invalid");
|
|
4982
|
+
seen.add(key);
|
|
4983
|
+
if (key === "--foreground" && operation === "start")
|
|
4984
|
+
foreground = true;
|
|
4985
|
+
else if (key === "--reveal" && operation === "token-link")
|
|
4986
|
+
reveal = true;
|
|
4987
|
+
else if (argv[0] && ["--server-config", "--state-home", "--agent-dir", "--config"].includes(key)) {
|
|
4988
|
+
const value = resolve6(argv.shift());
|
|
4989
|
+
if (key === "--server-config")
|
|
4990
|
+
config = value;
|
|
4991
|
+
if (key === "--state-home")
|
|
4992
|
+
stateHome = value;
|
|
4993
|
+
if (key === "--agent-dir")
|
|
4994
|
+
agentDir = value;
|
|
4995
|
+
if (key === "--config")
|
|
4996
|
+
policyFile = value;
|
|
4997
|
+
} else
|
|
4998
|
+
throw new ContractError("invalid");
|
|
4999
|
+
}
|
|
5000
|
+
const store = new GatewayStore(config, stateHome);
|
|
5001
|
+
let result;
|
|
5002
|
+
if (operation === "start") {
|
|
5003
|
+
const network = networkSnapshot(process.env), env = hostEnvironment(process.env);
|
|
5004
|
+
if (!seen.has("--server-config"))
|
|
5005
|
+
ensureServerConfig(homedir5());
|
|
5006
|
+
const server = readServerConfig(config).config;
|
|
5007
|
+
const selectedFile = store.files.read(store.path("input.json"));
|
|
5008
|
+
const selected = selectedFile ? decodeGatewayInput(json(selectedFile)) : undefined;
|
|
5009
|
+
let policy = selected?.policy ?? decodeHttpPolicy({
|
|
5010
|
+
schema: "cadence-service-http/1",
|
|
5011
|
+
host: "127.0.0.1",
|
|
5012
|
+
port: server.port,
|
|
5013
|
+
proxy: null
|
|
5014
|
+
});
|
|
5015
|
+
if (policyFile) {
|
|
5016
|
+
const file = new PrivateFiles(dirname8(policyFile)).read(policyFile);
|
|
5017
|
+
if (!file)
|
|
5018
|
+
throw new ContractError("invalid");
|
|
5019
|
+
policy = decodeHttpPolicy(json(file));
|
|
5020
|
+
}
|
|
5021
|
+
const input = decodeGatewayInput({
|
|
5022
|
+
kind: "workbench",
|
|
5023
|
+
serverConfig: config,
|
|
5024
|
+
stateHome: store.stateHome,
|
|
5025
|
+
agentDir: agentDir ?? selected?.agentDir ?? resolve6(process.env.PI_CODING_AGENT_DIR ?? join9(homedir5(), ".pi", "agent")),
|
|
5026
|
+
policy: { ...policy, port: server.port }
|
|
5027
|
+
});
|
|
5028
|
+
const status = await serviceStatus(store);
|
|
5029
|
+
if (status && typeof status === "object" && "status" in status && status.status === "alive") {
|
|
5030
|
+
if (!selected || encoded(selected) !== encoded(input))
|
|
5031
|
+
throw new ContractError("conflict");
|
|
5032
|
+
result = status;
|
|
5033
|
+
} else
|
|
5034
|
+
result = await launchHost(input, store, foreground, hostUrl, dirname8(dirname8(fileURLToPath(hostUrl))), env, network);
|
|
5035
|
+
if (!foreground && result && typeof result === "object" && "status" in result && result.status === "alive") {
|
|
5036
|
+
const reply = closed(await nativeCall(store, "token-link", true), [
|
|
5037
|
+
"link"
|
|
5038
|
+
]);
|
|
5039
|
+
process.stderr.write(`Cadence workbench: ${text(reply.link, 4096, true)}
|
|
5040
|
+
No project or default workspace was created.
|
|
5041
|
+
`);
|
|
5042
|
+
}
|
|
5043
|
+
} else if (operation === "stop")
|
|
5044
|
+
result = await stopService(store);
|
|
5045
|
+
else if (operation === "status" || operation === "diagnose")
|
|
5046
|
+
result = await serviceStatus(store, operation === "diagnose");
|
|
5047
|
+
else {
|
|
5048
|
+
if (operation === "token-link" && !reveal)
|
|
5049
|
+
throw new ContractError("forbidden");
|
|
5050
|
+
result = await nativeCall(store, operation, reveal);
|
|
5051
|
+
}
|
|
5052
|
+
if (result !== null)
|
|
5053
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
5054
|
+
`);
|
|
5055
|
+
if (result && typeof result === "object" && "status" in result && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)))
|
|
5056
|
+
process.exitCode = 1;
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
// src/service/launchd.ts
|
|
5060
|
+
import { rmSync as rmSync2 } from "node:fs";
|
|
5061
|
+
import { dirname as dirname9, join as join10 } from "node:path";
|
|
5062
|
+
var LABEL = "com.abelxiaoxing.cadence";
|
|
5063
|
+
var MARKER2 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5064
|
+
<!-- Generated by @abelxiaoxing/cadence`;
|
|
5065
|
+
function plistPath(s) {
|
|
5066
|
+
return join10(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
5067
|
+
}
|
|
5068
|
+
var xml = (value) => checked(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
5069
|
+
function buildPlist(u) {
|
|
5070
|
+
const string = (value) => ` <string>${xml(value)}</string>`;
|
|
5071
|
+
return [
|
|
5072
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
5073
|
+
"<!-- Generated by @abelxiaoxing/cadence; `cadence server install` and `restart` rewrite this file. -->",
|
|
5074
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
5075
|
+
'<plist version="1.0">',
|
|
5076
|
+
"<dict>",
|
|
5077
|
+
"\t<key>Label</key>",
|
|
5078
|
+
` <string>${LABEL}</string>`,
|
|
5079
|
+
"\t<key>ProgramArguments</key>",
|
|
5080
|
+
"\t<array>",
|
|
5081
|
+
...[
|
|
5082
|
+
u.node,
|
|
5083
|
+
u.cli,
|
|
5084
|
+
...u.workspace === null ? ["workbench", "start", "--foreground"] : ["start", "--foreground", "--workspace", u.workspace],
|
|
5085
|
+
"--server-config",
|
|
5086
|
+
u.serverConfig
|
|
5087
|
+
].map(string),
|
|
5088
|
+
"\t</array>",
|
|
5089
|
+
...u.workspace === null ? ["<!-- Cadence workbench gateway; no project workspace -->"] : [],
|
|
5090
|
+
"\t<key>WorkingDirectory</key>",
|
|
5091
|
+
` <string>${xml(u.workspace ?? dirname9(dirname9(u.cli)))}</string>`,
|
|
5092
|
+
"\t<key>EnvironmentVariables</key>",
|
|
5093
|
+
"\t<dict>",
|
|
5094
|
+
...u.environment.flatMap(([k, v]) => [
|
|
5095
|
+
` <key>${xml(k)}</key>`,
|
|
5096
|
+
string(v)
|
|
5097
|
+
]),
|
|
5098
|
+
"\t</dict>",
|
|
5099
|
+
"\t<key>RunAtLoad</key>",
|
|
5100
|
+
"\t<true/>",
|
|
5101
|
+
"\t<key>ProcessType</key>",
|
|
5102
|
+
"\t<string>Background</string>",
|
|
5103
|
+
"\t<key>StandardOutPath</key>",
|
|
5104
|
+
` <string>${xml(u.log)}</string>`,
|
|
5105
|
+
"\t<key>StandardErrorPath</key>",
|
|
5106
|
+
` <string>${xml(u.log)}</string>`,
|
|
5107
|
+
"</dict>",
|
|
5108
|
+
"</plist>",
|
|
5109
|
+
""
|
|
5110
|
+
].join(`
|
|
5111
|
+
`);
|
|
5112
|
+
}
|
|
5113
|
+
function plistWorkspace(text2) {
|
|
5114
|
+
if (text2?.includes("<!-- Cadence workbench gateway; no project workspace -->"))
|
|
5115
|
+
return null;
|
|
5116
|
+
const m = text2?.match(/^\t<key>WorkingDirectory<\/key>\n\t<string>([^<]*)<\/string>$/m);
|
|
5117
|
+
return m?.[1].replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
5118
|
+
}
|
|
5119
|
+
var domain = (s) => `gui/${s.uid}`;
|
|
5120
|
+
var target = (s) => `${domain(s)}/${LABEL}`;
|
|
5121
|
+
var loaded = (s) => s.launchctl(["print", target(s)]).status === 0;
|
|
5122
|
+
function disabled(s) {
|
|
5123
|
+
return new RegExp(`"${LABEL.replaceAll(".", "\\.")}" => (true|disabled)`).test(s.launchctl(["print-disabled", domain(s)]).stdout);
|
|
5124
|
+
}
|
|
5125
|
+
async function configureLaunchAgent(s, mode, requested) {
|
|
5126
|
+
if (s.uid === undefined)
|
|
5127
|
+
return { status: "unavailable" };
|
|
5128
|
+
const path2 = plistPath(s);
|
|
5129
|
+
const previous = readGenerated(path2, MARKER2);
|
|
5130
|
+
if (previous === null)
|
|
5131
|
+
return { status: "foreign", unit: path2 };
|
|
5132
|
+
if (mode === "restart" && previous === undefined)
|
|
5133
|
+
return { status: "absent" };
|
|
5134
|
+
const sdk = sdkPackages(s.root);
|
|
5135
|
+
const kept = plistWorkspace(previous);
|
|
5136
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : kept);
|
|
5137
|
+
if (workspace !== null && !sdk)
|
|
5138
|
+
return { status: "missing-pi-sdk" };
|
|
5139
|
+
const refused2 = await settle(s, kept);
|
|
5140
|
+
if (refused2)
|
|
5141
|
+
return { ...refused2, unit: path2 };
|
|
5142
|
+
if (loaded(s))
|
|
5143
|
+
s.launchctl(["bootout", target(s)]);
|
|
5144
|
+
write(path2, buildPlist({
|
|
5145
|
+
node: s.node,
|
|
5146
|
+
cli: join10(s.root, "src", "service-cli.mjs"),
|
|
5147
|
+
workspace,
|
|
5148
|
+
serverConfig: serverConfigPath(s.home),
|
|
5149
|
+
environment: serviceEnvironment(s),
|
|
5150
|
+
log: join10(s.home, "Library", "Logs", "cadence.log")
|
|
5151
|
+
}));
|
|
5152
|
+
const result = {
|
|
5153
|
+
unit: path2,
|
|
5154
|
+
workspace,
|
|
5155
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
5156
|
+
};
|
|
5157
|
+
if (mode === "install")
|
|
5158
|
+
s.launchctl(["enable", target(s)]);
|
|
5159
|
+
else if (disabled(s))
|
|
5160
|
+
return { status: "disabled", ...result };
|
|
5161
|
+
if (workspace !== null && !regularFile(join10(agentDirectory(s), "auth.json")))
|
|
5162
|
+
return { status: "waiting-for-pi-login", ...result };
|
|
5163
|
+
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
5164
|
+
return { status: started ? "started" : "failed", ...result };
|
|
5165
|
+
}
|
|
5166
|
+
async function stopLaunchAgent(s) {
|
|
5167
|
+
const path2 = plistPath(s);
|
|
5168
|
+
const previous = readGenerated(path2, MARKER2);
|
|
5169
|
+
if (previous === null)
|
|
5170
|
+
return { status: "foreign", unit: path2 };
|
|
5171
|
+
if (previous === undefined)
|
|
5172
|
+
return { status: "absent" };
|
|
5173
|
+
const workspace = plistWorkspace(previous);
|
|
5174
|
+
const refused2 = await settle(s, workspace);
|
|
5175
|
+
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
5176
|
+
}
|
|
5177
|
+
async function removeLaunchAgent(s) {
|
|
5178
|
+
const path2 = plistPath(s);
|
|
5179
|
+
const previous = readGenerated(path2, MARKER2);
|
|
5180
|
+
if (previous === null)
|
|
5181
|
+
return { status: "foreign", unit: path2 };
|
|
5182
|
+
if (previous === undefined)
|
|
5183
|
+
return { status: "absent" };
|
|
5184
|
+
const refused2 = await settle(s, plistWorkspace(previous));
|
|
5185
|
+
if (refused2)
|
|
5186
|
+
return { ...refused2, unit: path2 };
|
|
5187
|
+
if (s.uid !== undefined && loaded(s))
|
|
5188
|
+
s.launchctl(["bootout", target(s)]);
|
|
5189
|
+
rmSync2(path2);
|
|
5190
|
+
return { status: "removed", unit: path2 };
|
|
5191
|
+
}
|
|
5192
|
+
function launchAgentStatus(s) {
|
|
5193
|
+
const path2 = plistPath(s);
|
|
5194
|
+
const previous = readGenerated(path2, MARKER2);
|
|
5195
|
+
if (previous === null)
|
|
5196
|
+
return { status: "foreign", unit: path2 };
|
|
5197
|
+
if (previous === undefined)
|
|
5198
|
+
return { status: "absent" };
|
|
5199
|
+
const printed = s.uid === undefined ? undefined : s.launchctl(["print", target(s)]);
|
|
5200
|
+
return {
|
|
5201
|
+
status: printed?.status !== 0 ? "unloaded" : /^\s*state = running$/m.test(printed.stdout) ? "active" : "inactive",
|
|
5202
|
+
enabled: s.uid !== undefined && disabled(s) ? "disabled" : "enabled",
|
|
5203
|
+
unit: path2,
|
|
5204
|
+
workspace: plistWorkspace(previous)
|
|
5205
|
+
};
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
// src/service/login-run.ts
|
|
4190
5209
|
import { rmSync as rmSync3 } from "node:fs";
|
|
4191
|
-
import { isAbsolute as
|
|
5210
|
+
import { dirname as dirname10, isAbsolute as isAbsolute7, join as join11 } from "node:path";
|
|
4192
5211
|
var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
4193
5212
|
var APPROVED_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run";
|
|
4194
5213
|
var RUN_VALUE = "Cadence";
|
|
4195
5214
|
var MARKER3 = "' Generated by @abelxiaoxing/cadence";
|
|
4196
5215
|
function launcherPath(s) {
|
|
4197
5216
|
const local = s.env.LOCALAPPDATA;
|
|
4198
|
-
return
|
|
5217
|
+
return join11(local && isAbsolute7(local) ? local : join11(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
|
|
4199
5218
|
}
|
|
4200
5219
|
var literal2 = (value) => `"${checked(value).replaceAll('"', '""')}"`;
|
|
4201
5220
|
function argument2(value) {
|
|
@@ -4204,14 +5223,15 @@ function argument2(value) {
|
|
|
4204
5223
|
return `"${value}"`;
|
|
4205
5224
|
}
|
|
4206
5225
|
function buildLauncher(u) {
|
|
4207
|
-
const command = `${argument2(u.node)} ${argument2(u.cli)} start --workspace ${argument2(u.workspace)} --server-config ${argument2(u.serverConfig)}`;
|
|
5226
|
+
const command = `${argument2(u.node)} ${argument2(u.cli)} ${u.workspace === null ? "workbench start" : `start --workspace ${argument2(u.workspace)}`} --server-config ${argument2(u.serverConfig)}`;
|
|
4208
5227
|
return [
|
|
4209
5228
|
`${MARKER3}; \`cadence server install\` and \`restart\` rewrite this file.`,
|
|
4210
5229
|
`' Started at login by ${RUN_KEY}\\${RUN_VALUE}; window style 0 hides the console.`,
|
|
4211
5230
|
'Set shell = CreateObject("WScript.Shell")',
|
|
4212
5231
|
...u.environment.length ? ['Set env = shell.Environment("Process")'] : [],
|
|
4213
5232
|
...u.environment.map(([k, v]) => `env(${literal2(k)}) = ${literal2(v)}`),
|
|
4214
|
-
|
|
5233
|
+
...u.workspace === null ? ["' Cadence workbench gateway; no project workspace"] : [],
|
|
5234
|
+
`shell.CurrentDirectory = ${literal2(u.workspace ?? dirname10(dirname10(u.cli)))}`,
|
|
4215
5235
|
`shell.Run ${literal2(command)}, 0, False`,
|
|
4216
5236
|
""
|
|
4217
5237
|
].join(`\r
|
|
@@ -4219,6 +5239,8 @@ function buildLauncher(u) {
|
|
|
4219
5239
|
}
|
|
4220
5240
|
var utf16 = (text2) => Buffer.concat([Buffer.from([255, 254]), Buffer.from(text2, "utf16le")]);
|
|
4221
5241
|
function launcherWorkspace(text2) {
|
|
5242
|
+
if (text2?.includes("' Cadence workbench gateway; no project workspace"))
|
|
5243
|
+
return null;
|
|
4222
5244
|
return text2?.match(/^shell\.CurrentDirectory = "((?:[^"]|"")*)"\r?$/m)?.[1].replaceAll('""', '"');
|
|
4223
5245
|
}
|
|
4224
5246
|
var runEntry = (path2, s) => `"${wscript(s.env)}" "${path2}"`;
|
|
@@ -4238,17 +5260,17 @@ async function configureLoginRun(s, mode, requested) {
|
|
|
4238
5260
|
if (mode === "restart" && previous === undefined)
|
|
4239
5261
|
return { status: "absent" };
|
|
4240
5262
|
const sdk = sdkPackages(s.root);
|
|
4241
|
-
if (!sdk)
|
|
4242
|
-
return { status: "missing-pi-sdk" };
|
|
4243
5263
|
const kept = launcherWorkspace(previous);
|
|
4244
|
-
const { workspace, redirectedFrom } = selectWorkspace(s, requested, kept);
|
|
5264
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : kept);
|
|
5265
|
+
if (workspace !== null && !sdk)
|
|
5266
|
+
return { status: "missing-pi-sdk" };
|
|
4245
5267
|
const refused2 = await settle(s, kept);
|
|
4246
5268
|
if (refused2)
|
|
4247
5269
|
return { ...refused2, unit: path2 };
|
|
4248
5270
|
const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
|
|
4249
5271
|
write(path2, utf16(buildLauncher({
|
|
4250
5272
|
node: s.node,
|
|
4251
|
-
cli:
|
|
5273
|
+
cli: join11(s.root, "src", "service-cli.mjs"),
|
|
4252
5274
|
workspace,
|
|
4253
5275
|
serverConfig: serverConfigPath(s.home),
|
|
4254
5276
|
environment
|
|
@@ -4264,7 +5286,7 @@ async function configureLoginRun(s, mode, requested) {
|
|
|
4264
5286
|
return { status: "failed", ...result };
|
|
4265
5287
|
s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
|
|
4266
5288
|
}
|
|
4267
|
-
if (!regularFile(
|
|
5289
|
+
if (workspace !== null && !regularFile(join11(agentDirectory(s), "auth.json")))
|
|
4268
5290
|
return { status: "waiting-for-pi-login", ...result };
|
|
4269
5291
|
return { status: s.launch(path2) ? "started" : "failed", ...result };
|
|
4270
5292
|
}
|
|
@@ -4303,7 +5325,7 @@ async function loginRunStatus(s) {
|
|
|
4303
5325
|
if (previous === undefined)
|
|
4304
5326
|
return { status: "absent" };
|
|
4305
5327
|
const workspace = launcherWorkspace(previous);
|
|
4306
|
-
const owner = workspace ? await s.ownerStatus(workspace) : "unavailable";
|
|
5328
|
+
const owner = workspace !== undefined ? await s.ownerStatus(workspace, serverConfigPath(s.home)) : "unavailable";
|
|
4307
5329
|
return {
|
|
4308
5330
|
status: owner === "alive" ? "active" : "inactive",
|
|
4309
5331
|
enabled: present(s, RUN_KEY) && !approvedOff(s) ? "enabled" : "disabled",
|
|
@@ -4316,9 +5338,9 @@ async function loginRunStatus(s) {
|
|
|
4316
5338
|
import { isatty } from "node:tty";
|
|
4317
5339
|
|
|
4318
5340
|
// src/history/recovery.ts
|
|
4319
|
-
import { createHash as
|
|
4320
|
-
import { lstatSync as lstatSync7, opendirSync as
|
|
4321
|
-
import { join as
|
|
5341
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
|
|
5342
|
+
import { lstatSync as lstatSync7, opendirSync as opendirSync4 } from "node:fs";
|
|
5343
|
+
import { join as join13, relative as relative4 } from "node:path";
|
|
4322
5344
|
|
|
4323
5345
|
// src/service/recovery.ts
|
|
4324
5346
|
async function inspectRecovery(coordination, history) {
|
|
@@ -4366,19 +5388,19 @@ async function resolveRecovery(coordination, confirmation, history) {
|
|
|
4366
5388
|
}
|
|
4367
5389
|
|
|
4368
5390
|
// src/history/files.ts
|
|
4369
|
-
import { createHash as
|
|
5391
|
+
import { createHash as createHash5, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "node:crypto";
|
|
4370
5392
|
import {
|
|
4371
5393
|
closeSync as closeSync5,
|
|
4372
5394
|
constants as constants5,
|
|
4373
5395
|
fstatSync as fstatSync4,
|
|
4374
5396
|
fsyncSync as fsyncSync3,
|
|
4375
5397
|
lstatSync as lstatSync6,
|
|
4376
|
-
opendirSync as
|
|
5398
|
+
opendirSync as opendirSync3,
|
|
4377
5399
|
openSync as openSync5,
|
|
4378
5400
|
statfsSync
|
|
4379
5401
|
} from "node:fs";
|
|
4380
5402
|
import { open } from "node:fs/promises";
|
|
4381
|
-
import { dirname as
|
|
5403
|
+
import { dirname as dirname12, join as join12 } from "node:path";
|
|
4382
5404
|
|
|
4383
5405
|
// src/history/admission.ts
|
|
4384
5406
|
function decodeRunAdmission(value) {
|
|
@@ -4406,13 +5428,13 @@ import {
|
|
|
4406
5428
|
openSync as openSync4,
|
|
4407
5429
|
readSync as readSync3
|
|
4408
5430
|
} from "node:fs";
|
|
4409
|
-
import { dirname as
|
|
5431
|
+
import { dirname as dirname11 } from "node:path";
|
|
4410
5432
|
function bad() {
|
|
4411
5433
|
throw new ContractError("corrupt-selected");
|
|
4412
5434
|
}
|
|
4413
5435
|
var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
|
|
4414
5436
|
function selected(files, path2, read, walks) {
|
|
4415
|
-
if (!files.files.directory(
|
|
5437
|
+
if (!files.files.directory(dirname11(path2), false, walks))
|
|
4416
5438
|
bad();
|
|
4417
5439
|
const before = lstatSync5(path2);
|
|
4418
5440
|
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
@@ -4444,7 +5466,7 @@ function statSize(files, path2, maximum) {
|
|
|
4444
5466
|
}
|
|
4445
5467
|
|
|
4446
5468
|
// src/history/types.ts
|
|
4447
|
-
import { createHash as
|
|
5469
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4448
5470
|
var RECORD_BYTES = LIMITS.controlBytes + 64 * 1024;
|
|
4449
5471
|
var LEGACY_FORMAT = "cadence-history-store/2";
|
|
4450
5472
|
var FORMAT = "cadence-history-store/3";
|
|
@@ -4810,7 +5832,7 @@ function batchArtifacts(rows) {
|
|
|
4810
5832
|
}
|
|
4811
5833
|
var decodedLines = new Map;
|
|
4812
5834
|
var decodedBytes = 0;
|
|
4813
|
-
var lineKey = (text2) =>
|
|
5835
|
+
var lineKey = (text2) => createHash4("sha256").update(text2).digest("hex");
|
|
4814
5836
|
function remember(text2, value) {
|
|
4815
5837
|
const key = lineKey(text2);
|
|
4816
5838
|
if (decodedLines.has(key))
|
|
@@ -4909,14 +5931,14 @@ class HistoryFiles {
|
|
|
4909
5931
|
this.coordination = coordination;
|
|
4910
5932
|
this.format = format;
|
|
4911
5933
|
this.formatSnapshot = formatSnapshot;
|
|
4912
|
-
this.directory =
|
|
5934
|
+
this.directory = join12(coordination.directory, "history-v2");
|
|
4913
5935
|
this.files = coordination.files;
|
|
4914
5936
|
}
|
|
4915
5937
|
static open(coordination) {
|
|
4916
|
-
const dir =
|
|
5938
|
+
const dir = join12(coordination.directory, "history-v2");
|
|
4917
5939
|
if (!coordination.files.directory(dir))
|
|
4918
5940
|
return;
|
|
4919
|
-
const file = coordination.files.read(
|
|
5941
|
+
const file = coordination.files.read(join12(dir, "format.json"));
|
|
4920
5942
|
if (!file)
|
|
4921
5943
|
corrupt();
|
|
4922
5944
|
const format = decodeFormat(json(file));
|
|
@@ -4932,29 +5954,29 @@ class HistoryFiles {
|
|
|
4932
5954
|
existing.assertWritable();
|
|
4933
5955
|
return existing;
|
|
4934
5956
|
}
|
|
4935
|
-
const dir =
|
|
5957
|
+
const dir = join12(c.directory, "history-v2");
|
|
4936
5958
|
const format = {
|
|
4937
5959
|
schema: FORMAT,
|
|
4938
|
-
storeId:
|
|
4939
|
-
workspaceId:
|
|
5960
|
+
storeId: randomUUID6(),
|
|
5961
|
+
workspaceId: randomUUID6(),
|
|
4940
5962
|
rootIdentity: hash(c.store.root)
|
|
4941
5963
|
};
|
|
4942
5964
|
c.files.directory(dir, true);
|
|
4943
5965
|
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
4944
5966
|
lease.assert();
|
|
4945
|
-
c.files.directory(
|
|
5967
|
+
c.files.directory(join12(dir, name), true);
|
|
4946
5968
|
}
|
|
4947
5969
|
lease.assert();
|
|
4948
|
-
const keyPath =
|
|
5970
|
+
const keyPath = join12(dir, "read-proof-key.json");
|
|
4949
5971
|
c.files.create(keyPath, encoded({
|
|
4950
5972
|
schema: "cadence-history-read-key/1",
|
|
4951
5973
|
storeId: format.storeId,
|
|
4952
|
-
key:
|
|
5974
|
+
key: randomBytes5(32).toString("base64url")
|
|
4953
5975
|
}));
|
|
4954
5976
|
lease.assert();
|
|
4955
|
-
const snapshot = c.files.create(
|
|
5977
|
+
const snapshot = c.files.create(join12(dir, "format.json"), encoded(format));
|
|
4956
5978
|
const result = new HistoryFiles(c, format, snapshot);
|
|
4957
|
-
result.syncParents(
|
|
5979
|
+
result.syncParents(join12(dir, "format.json"));
|
|
4958
5980
|
result.assertWritable();
|
|
4959
5981
|
return result;
|
|
4960
5982
|
}
|
|
@@ -4989,7 +6011,7 @@ class HistoryFiles {
|
|
|
4989
6011
|
throw new ContractError("quarantine");
|
|
4990
6012
|
}
|
|
4991
6013
|
assertFormat() {
|
|
4992
|
-
this.files.assert(
|
|
6014
|
+
this.files.assert(join12(this.directory, "format.json"), this.formatSnapshot);
|
|
4993
6015
|
}
|
|
4994
6016
|
readProofMaterial(budget) {
|
|
4995
6017
|
if (this.format.schema !== FORMAT)
|
|
@@ -4998,7 +6020,7 @@ class HistoryFiles {
|
|
|
4998
6020
|
this.assertReadFormat(budget);
|
|
4999
6021
|
else
|
|
5000
6022
|
this.assertFormat();
|
|
5001
|
-
const selected2 = this.readBudgeted(
|
|
6023
|
+
const selected2 = this.readBudgeted(join12(this.directory, "read-proof-key.json"), 512, budget);
|
|
5002
6024
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
5003
6025
|
corrupt();
|
|
5004
6026
|
try {
|
|
@@ -5026,17 +6048,17 @@ class HistoryFiles {
|
|
|
5026
6048
|
return;
|
|
5027
6049
|
}
|
|
5028
6050
|
this.assertFormat();
|
|
5029
|
-
assertIncarnation(this,
|
|
6051
|
+
assertIncarnation(this, join12(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
5030
6052
|
}
|
|
5031
6053
|
segmentPath(segment) {
|
|
5032
|
-
return
|
|
6054
|
+
return join12(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
5033
6055
|
}
|
|
5034
6056
|
artifactPath(ref) {
|
|
5035
|
-
return
|
|
6057
|
+
return join12(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
5036
6058
|
}
|
|
5037
6059
|
admissionPath(runId) {
|
|
5038
6060
|
decodeId(runId);
|
|
5039
|
-
return
|
|
6061
|
+
return join12(this.directory, "runs", runId, "admission.json");
|
|
5040
6062
|
}
|
|
5041
6063
|
writeAdmission(lease, input) {
|
|
5042
6064
|
lease.assert();
|
|
@@ -5062,7 +6084,7 @@ class HistoryFiles {
|
|
|
5062
6084
|
readCommand(commandId, budget) {
|
|
5063
6085
|
decodeId(commandId);
|
|
5064
6086
|
this.assertReadFormat(budget);
|
|
5065
|
-
return this.readBudgeted(
|
|
6087
|
+
return this.readBudgeted(join12(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
5066
6088
|
}
|
|
5067
6089
|
readAdmission(runId, identity, budget) {
|
|
5068
6090
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -5083,7 +6105,7 @@ class HistoryFiles {
|
|
|
5083
6105
|
syncParents(file) {
|
|
5084
6106
|
if (process.platform === "win32")
|
|
5085
6107
|
return;
|
|
5086
|
-
for (let path2 =
|
|
6108
|
+
for (let path2 = dirname12(file);; path2 = dirname12(path2)) {
|
|
5087
6109
|
if (!this.files.directory(path2))
|
|
5088
6110
|
corrupt();
|
|
5089
6111
|
const fd = openSync5(path2, constants5.O_RDONLY | constants5.O_DIRECTORY);
|
|
@@ -5094,13 +6116,13 @@ class HistoryFiles {
|
|
|
5094
6116
|
}
|
|
5095
6117
|
if (path2 === this.coordination.directory)
|
|
5096
6118
|
break;
|
|
5097
|
-
if (path2 ===
|
|
6119
|
+
if (path2 === dirname12(path2))
|
|
5098
6120
|
corrupt();
|
|
5099
6121
|
}
|
|
5100
6122
|
}
|
|
5101
6123
|
assertReadFormat(budget) {
|
|
5102
6124
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
5103
|
-
this.files.assert(
|
|
6125
|
+
this.files.assert(join12(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
5104
6126
|
}
|
|
5105
6127
|
assertPublished(segment, end, seq) {
|
|
5106
6128
|
const fence = publicationFences.get(this.directory);
|
|
@@ -5135,10 +6157,10 @@ class HistoryFiles {
|
|
|
5135
6157
|
if (depth > 8 || !this.files.directory(path2))
|
|
5136
6158
|
corrupt();
|
|
5137
6159
|
let bytes = 0;
|
|
5138
|
-
const dir =
|
|
6160
|
+
const dir = opendirSync3(path2);
|
|
5139
6161
|
try {
|
|
5140
6162
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
5141
|
-
const p =
|
|
6163
|
+
const p = join12(path2, e.name), st = lstatSync6(p);
|
|
5142
6164
|
if (st.isDirectory())
|
|
5143
6165
|
bytes += walk(p, depth + 1);
|
|
5144
6166
|
else {
|
|
@@ -5173,10 +6195,10 @@ class HistoryFiles {
|
|
|
5173
6195
|
state.bytes = 0;
|
|
5174
6196
|
state.identity = null;
|
|
5175
6197
|
let count = 0;
|
|
5176
|
-
const ledger =
|
|
6198
|
+
const ledger = join12(this.directory, "ledger");
|
|
5177
6199
|
if (!this.files.directory(ledger))
|
|
5178
6200
|
corrupt();
|
|
5179
|
-
const dir =
|
|
6201
|
+
const dir = opendirSync3(ledger);
|
|
5180
6202
|
try {
|
|
5181
6203
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
5182
6204
|
budget?.reserve(0);
|
|
@@ -5210,7 +6232,7 @@ class HistoryFiles {
|
|
|
5210
6232
|
state.bytes = file.bytes.length;
|
|
5211
6233
|
state.identity = file.identity;
|
|
5212
6234
|
state.records = 0;
|
|
5213
|
-
let at = 0, pending = [], pendingBytes = 0, checksum =
|
|
6235
|
+
let at = 0, pending = [], pendingBytes = 0, checksum = createHash5("sha256");
|
|
5214
6236
|
while (at < file.bytes.length) {
|
|
5215
6237
|
budget?.reserve(0);
|
|
5216
6238
|
const end = file.bytes.indexOf(10, at);
|
|
@@ -5278,7 +6300,7 @@ class HistoryFiles {
|
|
|
5278
6300
|
};
|
|
5279
6301
|
pending = [];
|
|
5280
6302
|
pendingBytes = 0;
|
|
5281
|
-
checksum =
|
|
6303
|
+
checksum = createHash5("sha256");
|
|
5282
6304
|
}
|
|
5283
6305
|
if (pending.length)
|
|
5284
6306
|
state.tail = true;
|
|
@@ -5470,7 +6492,7 @@ class HistoryFiles {
|
|
|
5470
6492
|
lease.assert();
|
|
5471
6493
|
this.assertWritable();
|
|
5472
6494
|
const after = this.files.read(path2, LIMITS.segmentBytes);
|
|
5473
|
-
if (!after || after.identity !==
|
|
6495
|
+
if (!after || after.identity !== createHash5("sha256").update(before.bytes).update(body).update(trailer).digest("hex"))
|
|
5474
6496
|
corrupt();
|
|
5475
6497
|
return {
|
|
5476
6498
|
committedSeq: marker.last,
|
|
@@ -5490,7 +6512,7 @@ class HistoryFiles {
|
|
|
5490
6512
|
checkpoint(lease, state) {
|
|
5491
6513
|
lease.assert();
|
|
5492
6514
|
this.assertWritable();
|
|
5493
|
-
const path2 =
|
|
6515
|
+
const path2 = join12(this.directory, "checkpoint.json"), old = this.files.read(path2);
|
|
5494
6516
|
const body = encoded({
|
|
5495
6517
|
schema: "cadence-history-checkpoint/2",
|
|
5496
6518
|
storeId: this.format.storeId,
|
|
@@ -5898,7 +6920,7 @@ function projectRunObservation(runId, epoch, o, child) {
|
|
|
5898
6920
|
// src/history/recovery.ts
|
|
5899
6921
|
function digestHistory(files, exclude) {
|
|
5900
6922
|
files.assertFormat();
|
|
5901
|
-
const digest =
|
|
6923
|
+
const digest = createHash6("sha256");
|
|
5902
6924
|
let count = 0;
|
|
5903
6925
|
const walk = (path2, depth) => {
|
|
5904
6926
|
if (depth > 8 || !files.files.directory(path2))
|
|
@@ -5906,7 +6928,7 @@ function digestHistory(files, exclude) {
|
|
|
5906
6928
|
const directory2 = lstatSync7(path2);
|
|
5907
6929
|
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
5908
6930
|
`);
|
|
5909
|
-
const dir =
|
|
6931
|
+
const dir = opendirSync4(path2), names2 = [];
|
|
5910
6932
|
try {
|
|
5911
6933
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
5912
6934
|
if (++count > LIMITS.dataFrames)
|
|
@@ -5917,7 +6939,7 @@ function digestHistory(files, exclude) {
|
|
|
5917
6939
|
dir.closeSync();
|
|
5918
6940
|
}
|
|
5919
6941
|
for (const name of names2.sort()) {
|
|
5920
|
-
const selected2 =
|
|
6942
|
+
const selected2 = join13(path2, name);
|
|
5921
6943
|
if (selected2 === exclude)
|
|
5922
6944
|
continue;
|
|
5923
6945
|
const rel = relative4(files.directory, selected2);
|
|
@@ -6040,7 +7062,7 @@ class HistoryRecovery {
|
|
|
6040
7062
|
const current = await inspectRecovery(this.coordination, this);
|
|
6041
7063
|
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
6042
7064
|
throw new ContractError("conflict");
|
|
6043
|
-
const state = files.inspect(), recoveryId =
|
|
7065
|
+
const state = files.inspect(), recoveryId = randomUUID7();
|
|
6044
7066
|
const bridge = {
|
|
6045
7067
|
kind: "recovery",
|
|
6046
7068
|
recoveryId,
|
|
@@ -6071,7 +7093,7 @@ class HistoryRecovery {
|
|
|
6071
7093
|
throw new ContractError("conflict");
|
|
6072
7094
|
const row = {
|
|
6073
7095
|
kind: "record",
|
|
6074
|
-
id:
|
|
7096
|
+
id: randomUUID7(),
|
|
6075
7097
|
seq: String(BigInt(bridge.previous.highSeq) + 1n),
|
|
6076
7098
|
value: bridge
|
|
6077
7099
|
};
|
|
@@ -6131,11 +7153,11 @@ async function confirm(expected) {
|
|
|
6131
7153
|
"storage"
|
|
6132
7154
|
],
|
|
6133
7155
|
[
|
|
6134
|
-
|
|
7156
|
+
`${expected.scope === "global-registry" ? "Release ONLY the global registry lock; all project fences remain." : "Release ONLY the displayed unchanged service-owned fences."} Type resolve ${expected.epoch}`,
|
|
6135
7157
|
`resolve ${expected.epoch}`
|
|
6136
7158
|
]
|
|
6137
7159
|
];
|
|
6138
|
-
await new Promise((
|
|
7160
|
+
await new Promise((resolve7, reject) => {
|
|
6139
7161
|
let index = 0, line = "", done = false;
|
|
6140
7162
|
const raw = process.stdin.isRaw;
|
|
6141
7163
|
const finish = (ok) => {
|
|
@@ -6151,7 +7173,7 @@ async function confirm(expected) {
|
|
|
6151
7173
|
process.stdout.write(`\x1B[?2004l
|
|
6152
7174
|
`);
|
|
6153
7175
|
if (ok)
|
|
6154
|
-
|
|
7176
|
+
resolve7();
|
|
6155
7177
|
else
|
|
6156
7178
|
reject(new ContractError("forbidden"));
|
|
6157
7179
|
};
|
|
@@ -6194,14 +7216,50 @@ async function confirm(expected) {
|
|
|
6194
7216
|
async function localRecovery(c, operation) {
|
|
6195
7217
|
if (operation === "resolve")
|
|
6196
7218
|
native();
|
|
7219
|
+
const registryLock = c.registryLockDiagnostic();
|
|
7220
|
+
if (registryLock) {
|
|
7221
|
+
let expected2;
|
|
7222
|
+
try {
|
|
7223
|
+
expected2 = c.inspectRegistryRecovery();
|
|
7224
|
+
} catch {
|
|
7225
|
+
const blocked = {
|
|
7226
|
+
scope: "global-registry",
|
|
7227
|
+
...registryLock,
|
|
7228
|
+
coordination: "unavailable",
|
|
7229
|
+
resolution: "blocked-preserve-state",
|
|
7230
|
+
notice: "Registry state is incomplete, changed or unsafe. Preserve it; no lock or project resource was released."
|
|
7231
|
+
};
|
|
7232
|
+
if (operation === "inspect")
|
|
7233
|
+
return blocked;
|
|
7234
|
+
process.stdout.write(`${JSON.stringify(blocked)}
|
|
7235
|
+
`);
|
|
7236
|
+
throw new ContractError("quarantine");
|
|
7237
|
+
}
|
|
7238
|
+
const view2 = {
|
|
7239
|
+
...expected2,
|
|
7240
|
+
process: {
|
|
7241
|
+
...expected2.process,
|
|
7242
|
+
host: diagnosticText(expected2.process.host, 255)
|
|
7243
|
+
},
|
|
7244
|
+
notice: `Global registry lock blocks every project in this state home. Recovery preserves its original bytes and all project reservations, owner/checkout locks and history. Inspect project fences separately afterward. ${warning}`
|
|
7245
|
+
};
|
|
7246
|
+
if (operation === "inspect")
|
|
7247
|
+
return view2;
|
|
7248
|
+
process.stdout.write(`${JSON.stringify(view2)}
|
|
7249
|
+
`);
|
|
7250
|
+
if (expected2.owner !== "absent" || expected2.previousResolver !== null && expected2.previousResolver !== "absent")
|
|
7251
|
+
throw new ContractError("quarantine");
|
|
7252
|
+
const assertFresh2 = await confirm(expected2);
|
|
7253
|
+
return c.resolveRegistryRecovery(expected2, assertFresh2);
|
|
7254
|
+
}
|
|
6197
7255
|
const history = new HistoryRecovery(c);
|
|
6198
7256
|
const expected = await inspectRecovery(c, history);
|
|
6199
|
-
const
|
|
7257
|
+
const registration2 = c.recoverySnapshot().registration;
|
|
6200
7258
|
const view = {
|
|
6201
7259
|
...expected,
|
|
6202
7260
|
process: {
|
|
6203
|
-
pid:
|
|
6204
|
-
host: diagnosticText(
|
|
7261
|
+
pid: registration2.pid,
|
|
7262
|
+
host: diagnosticText(registration2.host, 255)
|
|
6205
7263
|
},
|
|
6206
7264
|
notice: warning
|
|
6207
7265
|
};
|
|
@@ -6226,227 +7284,9 @@ async function localRecovery(c, operation) {
|
|
|
6226
7284
|
}, history);
|
|
6227
7285
|
}
|
|
6228
7286
|
|
|
6229
|
-
// src/service/runtime.ts
|
|
6230
|
-
import { homedir as homedir4 } from "node:os";
|
|
6231
|
-
import { dirname as dirname8, isAbsolute as isAbsolute6, join as join10, resolve as resolve4 } from "node:path";
|
|
6232
|
-
|
|
6233
|
-
// src/service/config.ts
|
|
6234
|
-
import { isIP } from "node:net";
|
|
6235
|
-
function decodeHttpPolicy(value) {
|
|
6236
|
-
const o = closed(value, ["schema", "host", "port", "proxy"]);
|
|
6237
|
-
if (o.host !== "127.0.0.1" && o.host !== "::1")
|
|
6238
|
-
throw new ContractError("invalid");
|
|
6239
|
-
let proxy = null;
|
|
6240
|
-
if (o.proxy !== null) {
|
|
6241
|
-
const p = closed(o.proxy, ["origin", "trustedAddresses"]);
|
|
6242
|
-
const origin = text(p.origin, 256, true);
|
|
6243
|
-
let url;
|
|
6244
|
-
try {
|
|
6245
|
-
url = new URL(origin);
|
|
6246
|
-
} catch {
|
|
6247
|
-
throw new ContractError("invalid");
|
|
6248
|
-
}
|
|
6249
|
-
if (url.protocol !== "https:" || url.origin !== origin || url.username || url.password)
|
|
6250
|
-
throw new ContractError("invalid");
|
|
6251
|
-
const trustedAddresses = list(p.trustedAddresses, 8, (v) => {
|
|
6252
|
-
const address = text(v, 64, true);
|
|
6253
|
-
if (!isIP(address) || !loopbackAddress(address))
|
|
6254
|
-
throw new ContractError("invalid");
|
|
6255
|
-
return address;
|
|
6256
|
-
});
|
|
6257
|
-
if (!trustedAddresses.length || new Set(trustedAddresses).size !== trustedAddresses.length)
|
|
6258
|
-
throw new ContractError("invalid");
|
|
6259
|
-
proxy = { origin, trustedAddresses };
|
|
6260
|
-
}
|
|
6261
|
-
return {
|
|
6262
|
-
schema: literal(o.schema, "cadence-service-http/1"),
|
|
6263
|
-
host: o.host,
|
|
6264
|
-
port: integer(o.port, 0, 65535),
|
|
6265
|
-
proxy
|
|
6266
|
-
};
|
|
6267
|
-
}
|
|
6268
|
-
function loopbackAddress(host) {
|
|
6269
|
-
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
|
|
6270
|
-
const h = mapped ? mapped[1] : host;
|
|
6271
|
-
if (h.includes(":"))
|
|
6272
|
-
return h === "::1";
|
|
6273
|
-
const parts = h.split(".");
|
|
6274
|
-
return parts.length === 4 && parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255) && Number(parts[0]) === 127;
|
|
6275
|
-
}
|
|
6276
|
-
function localOrigin(policy, actualPort) {
|
|
6277
|
-
integer(actualPort, 1, 65535);
|
|
6278
|
-
return `http://${policy.host === "::1" ? "[::1]" : policy.host}:${actualPort}`;
|
|
6279
|
-
}
|
|
6280
|
-
|
|
6281
|
-
// src/service/runtime.ts
|
|
6282
|
-
var STARTUP_INPUT_BYTES = 16 * 1024;
|
|
6283
|
-
function absolute(value) {
|
|
6284
|
-
const result = text(value, 4096, true);
|
|
6285
|
-
if (!isAbsolute6(result))
|
|
6286
|
-
throw new ContractError("invalid");
|
|
6287
|
-
return result;
|
|
6288
|
-
}
|
|
6289
|
-
function decodeHostConfig(value) {
|
|
6290
|
-
const o = closed(value, [
|
|
6291
|
-
"schema",
|
|
6292
|
-
"http",
|
|
6293
|
-
"cwd",
|
|
6294
|
-
"agentDir",
|
|
6295
|
-
"tokenSource",
|
|
6296
|
-
"tokenFile"
|
|
6297
|
-
]);
|
|
6298
|
-
const tokenSource = oneOf(o.tokenSource, [
|
|
6299
|
-
"generated",
|
|
6300
|
-
"environment",
|
|
6301
|
-
"file",
|
|
6302
|
-
"config"
|
|
6303
|
-
]);
|
|
6304
|
-
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
6305
|
-
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
6306
|
-
throw new ContractError("invalid");
|
|
6307
|
-
return {
|
|
6308
|
-
schema: literal(o.schema, "cadence-service-host/1"),
|
|
6309
|
-
http: decodeHttpPolicy(o.http),
|
|
6310
|
-
cwd: absolute(o.cwd),
|
|
6311
|
-
agentDir: absolute(o.agentDir),
|
|
6312
|
-
tokenSource,
|
|
6313
|
-
tokenFile
|
|
6314
|
-
};
|
|
6315
|
-
}
|
|
6316
|
-
function readHostConfig(c) {
|
|
6317
|
-
const owner = c.configuration();
|
|
6318
|
-
if (!owner)
|
|
6319
|
-
return;
|
|
6320
|
-
const snapshot = c.files.read(c.path("host-config.json"));
|
|
6321
|
-
if (!snapshot)
|
|
6322
|
-
throw new ContractError("corrupt-selected");
|
|
6323
|
-
const config = decodeHostConfig(json(snapshot));
|
|
6324
|
-
if (!config.http.port || owner.endpoint !== localOrigin(config.http, config.http.port) || owner.policyIdentity !== hash(encoded(config)))
|
|
6325
|
-
throw new ContractError("conflict");
|
|
6326
|
-
return config;
|
|
6327
|
-
}
|
|
6328
|
-
function hostEnvironment(ambient) {
|
|
6329
|
-
let route;
|
|
6330
|
-
const facts = networkSnapshot(ambient);
|
|
6331
|
-
try {
|
|
6332
|
-
if (facts.NODE_OPTIONS.present)
|
|
6333
|
-
throw new Error;
|
|
6334
|
-
route = sdkNetworkEnvironment(facts, networkSnapshot(undefined));
|
|
6335
|
-
} catch {
|
|
6336
|
-
throw new ContractError("unsupported-context");
|
|
6337
|
-
}
|
|
6338
|
-
const result = Object.create(null);
|
|
6339
|
-
for (const key of Object.keys(ambient)) {
|
|
6340
|
-
const d = Object.getOwnPropertyDescriptor(ambient, key);
|
|
6341
|
-
if (!d || !("value" in d) || typeof d.value !== "string")
|
|
6342
|
-
throw new ContractError("invalid");
|
|
6343
|
-
if (/^(?:CADENCE_|PI_|ABEL_|TRELLIS_|GIT_)/i.test(key) || networkKeys.some((name) => name === key))
|
|
6344
|
-
continue;
|
|
6345
|
-
result[key] = d.value;
|
|
6346
|
-
}
|
|
6347
|
-
if (route.redactionLiterals.length) {
|
|
6348
|
-
result.HTTP_PROXY = route.env.HTTP_PROXY;
|
|
6349
|
-
result.HTTPS_PROXY = route.env.HTTPS_PROXY;
|
|
6350
|
-
result.NODE_USE_ENV_PROXY = "1";
|
|
6351
|
-
}
|
|
6352
|
-
result.PI_OFFLINE = "1";
|
|
6353
|
-
result.NO_COLOR = "1";
|
|
6354
|
-
if (!sameNetwork(facts, networkSnapshot(ambient)))
|
|
6355
|
-
throw new ContractError("stale");
|
|
6356
|
-
return result;
|
|
6357
|
-
}
|
|
6358
|
-
function selectStart(options, ambient) {
|
|
6359
|
-
const workspace = resolveWorkspace(options.workspace), c = new ServiceCoordination(workspace.root, options.stateHome);
|
|
6360
|
-
const old = readHostConfig(c);
|
|
6361
|
-
const env = Object.getOwnPropertyDescriptor(ambient, "CADENCE_WEB_TOKEN");
|
|
6362
|
-
if (env && (!("value" in env) || typeof env.value !== "string"))
|
|
6363
|
-
throw new ContractError("invalid");
|
|
6364
|
-
let fromFile;
|
|
6365
|
-
if (options.config) {
|
|
6366
|
-
const path2 = resolve4(options.config), f = new PrivateFiles(dirname8(path2));
|
|
6367
|
-
const bytes = f.read(path2);
|
|
6368
|
-
if (!bytes)
|
|
6369
|
-
throw new ContractError("corrupt-selected");
|
|
6370
|
-
fromFile = decodeHostConfig(json(bytes));
|
|
6371
|
-
}
|
|
6372
|
-
const base = old ?? fromFile ?? decodeHostConfig({
|
|
6373
|
-
schema: "cadence-service-host/1",
|
|
6374
|
-
cwd: workspace.cwd,
|
|
6375
|
-
http: {
|
|
6376
|
-
schema: "cadence-service-http/1",
|
|
6377
|
-
host: options.host ?? "127.0.0.1",
|
|
6378
|
-
port: options.port ?? 17423,
|
|
6379
|
-
proxy: null
|
|
6380
|
-
},
|
|
6381
|
-
agentDir: resolve4(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join10(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
6382
|
-
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
6383
|
-
tokenFile: options.tokenFile ? resolve4(options.tokenFile) : null
|
|
6384
|
-
});
|
|
6385
|
-
const server = options.serverConfig !== undefined ? resolve4(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
|
|
6386
|
-
let selected2 = base;
|
|
6387
|
-
if (server) {
|
|
6388
|
-
if (options.tokenFile !== undefined)
|
|
6389
|
-
throw new ContractError("conflict");
|
|
6390
|
-
const { port } = readServerConfig(server).config;
|
|
6391
|
-
selected2 = decodeHostConfig({
|
|
6392
|
-
...base,
|
|
6393
|
-
http: { ...base.http, port },
|
|
6394
|
-
tokenSource: "config",
|
|
6395
|
-
tokenFile: server
|
|
6396
|
-
});
|
|
6397
|
-
}
|
|
6398
|
-
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)
|
|
6399
|
-
throw new ContractError("conflict");
|
|
6400
|
-
return {
|
|
6401
|
-
workspace: workspace.root,
|
|
6402
|
-
stateHome: c.stateHome,
|
|
6403
|
-
config: selected2,
|
|
6404
|
-
environmentToken: env?.value ?? null
|
|
6405
|
-
};
|
|
6406
|
-
}
|
|
6407
|
-
function decodeHostInput(value) {
|
|
6408
|
-
const o = closed(value, [
|
|
6409
|
-
"workspace",
|
|
6410
|
-
"stateHome",
|
|
6411
|
-
"config",
|
|
6412
|
-
"environmentToken"
|
|
6413
|
-
]);
|
|
6414
|
-
const config = decodeHostConfig(o.config);
|
|
6415
|
-
const environmentToken = o.environmentToken === null ? null : text(o.environmentToken, 1024, true);
|
|
6416
|
-
if (config.tokenSource === "environment" !== (environmentToken !== null))
|
|
6417
|
-
throw new ContractError("invalid");
|
|
6418
|
-
return {
|
|
6419
|
-
workspace: absolute(o.workspace),
|
|
6420
|
-
stateHome: absolute(o.stateHome),
|
|
6421
|
-
config,
|
|
6422
|
-
environmentToken
|
|
6423
|
-
};
|
|
6424
|
-
}
|
|
6425
|
-
function portValue(value) {
|
|
6426
|
-
if (!/^(0|[1-9][0-9]{0,4})$/.test(value))
|
|
6427
|
-
throw new ContractError("invalid");
|
|
6428
|
-
return integer(Number(value), 0, 65535);
|
|
6429
|
-
}
|
|
6430
|
-
var STARTUP_STAGES = [
|
|
6431
|
-
"package",
|
|
6432
|
-
"pi-agent-directory",
|
|
6433
|
-
"pi-auth",
|
|
6434
|
-
"pi-models",
|
|
6435
|
-
"pi-runtime",
|
|
6436
|
-
"service"
|
|
6437
|
-
];
|
|
6438
|
-
function decodeStartupReport(value) {
|
|
6439
|
-
const o = closed(value, ["status", "error", "stage"]);
|
|
6440
|
-
return {
|
|
6441
|
-
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
6442
|
-
error: oneOf(o.error, ERROR_CODES),
|
|
6443
|
-
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
6444
|
-
};
|
|
6445
|
-
}
|
|
6446
|
-
|
|
6447
7287
|
// src/service/system-service.ts
|
|
6448
|
-
import { existsSync as existsSync2,
|
|
6449
|
-
import { isAbsolute as
|
|
7288
|
+
import { existsSync as existsSync2, realpathSync as realpathSync5, rmSync as rmSync4 } from "node:fs";
|
|
7289
|
+
import { isAbsolute as isAbsolute8, join as join14 } from "node:path";
|
|
6450
7290
|
var MARKER4 = "# Generated by @abelxiaoxing/cadence";
|
|
6451
7291
|
function boundary(s) {
|
|
6452
7292
|
if (s.platform !== "linux" || s.scope !== "system")
|
|
@@ -6460,7 +7300,7 @@ function legacyPath(s) {
|
|
|
6460
7300
|
function observed(s) {
|
|
6461
7301
|
const path2 = unitPath(s);
|
|
6462
7302
|
let previous = readGenerated(path2, MARKER4);
|
|
6463
|
-
if (typeof previous === "string" && (!
|
|
7303
|
+
if (typeof previous === "string" && (unitWorkspace(previous) !== null && !isAbsolute8(unitWorkspace(previous) ?? "") || !previous.includes(`
|
|
6464
7304
|
User=root
|
|
6465
7305
|
`)))
|
|
6466
7306
|
previous = null;
|
|
@@ -6489,13 +7329,13 @@ function sameStateEnvironment(s, previous) {
|
|
|
6489
7329
|
values.set(key, value.slice(at + 1).replaceAll("%%", "%"));
|
|
6490
7330
|
}
|
|
6491
7331
|
const state = s.env.XDG_STATE_HOME;
|
|
6492
|
-
return values.get("HOME") === s.home && values.get("XDG_STATE_HOME") === (state &&
|
|
7332
|
+
return values.get("HOME") === s.home && values.get("XDG_STATE_HOME") === (state && isAbsolute8(state) ? state : undefined);
|
|
6493
7333
|
} catch {
|
|
6494
7334
|
return false;
|
|
6495
7335
|
}
|
|
6496
7336
|
}
|
|
6497
7337
|
async function ownerBoundary(s, workspace, managed) {
|
|
6498
|
-
const status = await s.ownerStatus(workspace);
|
|
7338
|
+
const status = await s.ownerStatus(workspace, serviceConfigPath(s));
|
|
6499
7339
|
if (managed) {
|
|
6500
7340
|
if (status === "absent")
|
|
6501
7341
|
return { status: "unavailable", workspace };
|
|
@@ -6522,21 +7362,21 @@ async function configureSystemService(s, mode, requested) {
|
|
|
6522
7362
|
if (mode === "restart" && previous === undefined)
|
|
6523
7363
|
return { status: "absent", scope: "system" };
|
|
6524
7364
|
const sdk = sdkPackages(s.root);
|
|
6525
|
-
if (!
|
|
6526
|
-
return { status: "missing-pi-sdk", scope: "system" };
|
|
6527
|
-
if (previous && !existsSync2(unitWorkspace(previous)))
|
|
7365
|
+
if (previous && unitWorkspace(previous) && !existsSync2(unitWorkspace(previous)))
|
|
6528
7366
|
return { status: "workspace-changed", ...result };
|
|
6529
|
-
const selected2 = selectWorkspace(s, requested, unitWorkspace(previous), false);
|
|
6530
|
-
|
|
7367
|
+
const selected2 = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : unitWorkspace(previous), false);
|
|
7368
|
+
if (selected2.workspace !== null && !sdk)
|
|
7369
|
+
return { status: "missing-pi-sdk", scope: "system" };
|
|
7370
|
+
const selectedReal = selected2.workspace && existsSync2(selected2.workspace) ? realpathSync5(selected2.workspace) : undefined;
|
|
6531
7371
|
const agentDir = agentDirectory(s);
|
|
6532
7372
|
const content = buildUnit({
|
|
6533
7373
|
system: true,
|
|
6534
7374
|
node: s.node,
|
|
6535
|
-
cli:
|
|
7375
|
+
cli: join14(s.root, "src", "service-cli.mjs"),
|
|
6536
7376
|
workspace: selected2.workspace,
|
|
6537
7377
|
serverConfig: serviceConfigPath(s),
|
|
6538
7378
|
agentDir,
|
|
6539
|
-
sdk,
|
|
7379
|
+
sdk: sdk ?? [],
|
|
6540
7380
|
environment: serviceEnvironment(s)
|
|
6541
7381
|
});
|
|
6542
7382
|
if (previous && !sameStateEnvironment(s, previous))
|
|
@@ -6546,13 +7386,13 @@ async function configureSystemService(s, mode, requested) {
|
|
|
6546
7386
|
return { status: "unavailable", ...result };
|
|
6547
7387
|
if (active.status === 0 && previous === undefined)
|
|
6548
7388
|
return { status: "foreign", ...result };
|
|
6549
|
-
if (selectedReal && (previous === undefined || selected2.workspace !== unitWorkspace(previous))) {
|
|
7389
|
+
if ((selected2.workspace === null || selectedReal) && (previous === undefined || selected2.workspace !== unitWorkspace(previous))) {
|
|
6550
7390
|
const target2 = await ownerBoundary(s, selected2.workspace, false);
|
|
6551
7391
|
if (target2)
|
|
6552
7392
|
return { ...selected2, ...target2, ...result };
|
|
6553
7393
|
}
|
|
6554
7394
|
const workspace = unitWorkspace(previous);
|
|
6555
|
-
if (workspace) {
|
|
7395
|
+
if (workspace !== undefined) {
|
|
6556
7396
|
const refused3 = await ownerBoundary(s, workspace, active.status === 0);
|
|
6557
7397
|
if (refused3)
|
|
6558
7398
|
return { ...refused3, ...result };
|
|
@@ -6562,7 +7402,7 @@ async function configureSystemService(s, mode, requested) {
|
|
|
6562
7402
|
return { ...unsettled, ...result };
|
|
6563
7403
|
if (readGenerated(path2, MARKER4) !== previous)
|
|
6564
7404
|
return { status: "foreign", ...result };
|
|
6565
|
-
if (selectedReal) {
|
|
7405
|
+
if (selectedReal && selected2.workspace) {
|
|
6566
7406
|
try {
|
|
6567
7407
|
if (realpathSync5(selected2.workspace) !== selectedReal)
|
|
6568
7408
|
return { status: "workspace-changed", ...result };
|
|
@@ -6576,13 +7416,12 @@ async function configureSystemService(s, mode, requested) {
|
|
|
6576
7416
|
workspace: unitWorkspace(previous),
|
|
6577
7417
|
...result
|
|
6578
7418
|
};
|
|
6579
|
-
mkdirSync6(selected2.workspace, { recursive: true });
|
|
6580
7419
|
write(path2, content);
|
|
6581
7420
|
if (s.systemctl(["daemon-reload"]).status !== 0)
|
|
6582
7421
|
return { status: "failed", ...result, ...selected2 };
|
|
6583
7422
|
if (mode === "install" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
6584
7423
|
return { status: "failed", ...result, ...selected2 };
|
|
6585
|
-
if (!regularFile(
|
|
7424
|
+
if (selected2.workspace !== null && !regularFile(join14(agentDir, "auth.json")))
|
|
6586
7425
|
return { status: "waiting-for-pi-login", ...result, ...selected2 };
|
|
6587
7426
|
s.systemctl(["reset-failed", s.unit]);
|
|
6588
7427
|
return {
|
|
@@ -6651,12 +7490,12 @@ function systemServiceStatus(s) {
|
|
|
6651
7490
|
}
|
|
6652
7491
|
|
|
6653
7492
|
// src/service/cli.ts
|
|
6654
|
-
function nativeCall(c, operation, reveal = false) {
|
|
7493
|
+
function nativeCall(c, operation, reveal = false, controlBody) {
|
|
6655
7494
|
const access = c.localAccess();
|
|
6656
7495
|
const url = new URL(`${access.endpoint}/local/v1/admin/${operation}`);
|
|
6657
7496
|
const write2 = !["status", "diagnose"].includes(operation);
|
|
6658
|
-
const body = write2 ? JSON.stringify(reveal ? { reveal: true } : {}) : undefined;
|
|
6659
|
-
return new Promise((
|
|
7497
|
+
const body = write2 ? JSON.stringify(controlBody ?? (reveal ? { reveal: true } : {})) : undefined;
|
|
7498
|
+
return new Promise((resolve8, reject) => {
|
|
6660
7499
|
let timer;
|
|
6661
7500
|
const fail = () => {
|
|
6662
7501
|
clearTimeout(timer);
|
|
@@ -6697,7 +7536,7 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
6697
7536
|
throw new ContractError("stale");
|
|
6698
7537
|
}
|
|
6699
7538
|
clearTimeout(timer);
|
|
6700
|
-
|
|
7539
|
+
resolve8(reply.value);
|
|
6701
7540
|
} catch {
|
|
6702
7541
|
fail();
|
|
6703
7542
|
}
|
|
@@ -6712,8 +7551,15 @@ function nativeCall(c, operation, reveal = false) {
|
|
|
6712
7551
|
req.end(body);
|
|
6713
7552
|
});
|
|
6714
7553
|
}
|
|
6715
|
-
var pause = (ms) => new Promise((
|
|
7554
|
+
var pause = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
6716
7555
|
async function serviceStatus(c, diagnose = false) {
|
|
7556
|
+
const registryLock = c.registryLockDiagnostic?.();
|
|
7557
|
+
if (registryLock)
|
|
7558
|
+
return {
|
|
7559
|
+
status: "unavailable",
|
|
7560
|
+
...registryLock,
|
|
7561
|
+
recovery: "cadence recover inspect --workspace <project> [--state-home <state-home>]"
|
|
7562
|
+
};
|
|
6717
7563
|
const observed2 = c.inspect();
|
|
6718
7564
|
if (observed2.status === "absent" || observed2.status === "stopped")
|
|
6719
7565
|
return observed2;
|
|
@@ -6756,6 +7602,10 @@ function serverConfigHint(error) {
|
|
|
6756
7602
|
Fix it and run the command again; the file holds the WebUI port and token as ${PORT_KEY}=<1-65535> and ${TOKEN_KEY}=<at least 32 characters>, one line each.
|
|
6757
7603
|
`;
|
|
6758
7604
|
}
|
|
7605
|
+
function registryGuidance(workspace, stateHome) {
|
|
7606
|
+
return `The shared registry lock blocks project access in this state home. No automatic lock deletion. Inspect locally: cadence recover inspect --workspace ${JSON.stringify(workspace)} --state-home ${JSON.stringify(stateHome)}; eligible dead locks require cadence recover resolve in a local terminal.
|
|
7607
|
+
`;
|
|
7608
|
+
}
|
|
6759
7609
|
function startupGuidance(result, input) {
|
|
6760
7610
|
let report;
|
|
6761
7611
|
try {
|
|
@@ -6763,6 +7613,8 @@ function startupGuidance(result, input) {
|
|
|
6763
7613
|
} catch {
|
|
6764
7614
|
return "";
|
|
6765
7615
|
}
|
|
7616
|
+
if (report.error === "registry-locked")
|
|
7617
|
+
return registryGuidance(input.workspace, input.stateHome);
|
|
6766
7618
|
const agent = input.config.agentDir;
|
|
6767
7619
|
const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
|
|
6768
7620
|
`;
|
|
@@ -6774,10 +7626,10 @@ function startupGuidance(result, input) {
|
|
|
6774
7626
|
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
6775
7627
|
`;
|
|
6776
7628
|
case "pi-auth":
|
|
6777
|
-
return `${
|
|
7629
|
+
return `${join15(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
6778
7630
|
`;
|
|
6779
7631
|
case "pi-models":
|
|
6780
|
-
return `${
|
|
7632
|
+
return `${join15(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.
|
|
6781
7633
|
`;
|
|
6782
7634
|
case "pi-runtime":
|
|
6783
7635
|
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
@@ -6790,6 +7642,15 @@ ${inspect}` : "";
|
|
|
6790
7642
|
async function startService(options, foreground, hostUrl) {
|
|
6791
7643
|
const network = networkSnapshot(process.env);
|
|
6792
7644
|
const env = hostEnvironment(process.env);
|
|
7645
|
+
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
7646
|
+
if (c.registryLockDiagnostic()) {
|
|
7647
|
+
process.stderr.write(registryGuidance(c.store.root, c.stateHome));
|
|
7648
|
+
return {
|
|
7649
|
+
status: "refused",
|
|
7650
|
+
error: "registry-locked",
|
|
7651
|
+
stage: "service"
|
|
7652
|
+
};
|
|
7653
|
+
}
|
|
6793
7654
|
let input;
|
|
6794
7655
|
try {
|
|
6795
7656
|
input = decodeHostInput(selectStart(options, process.env));
|
|
@@ -6802,7 +7663,6 @@ async function startService(options, foreground, hostUrl) {
|
|
|
6802
7663
|
`;
|
|
6803
7664
|
if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
|
|
6804
7665
|
throw new ContractError("oversize");
|
|
6805
|
-
const c = new ServiceCoordination(input.workspace, input.stateHome);
|
|
6806
7666
|
const existing = await serviceStatus(c);
|
|
6807
7667
|
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
|
|
6808
7668
|
const running = readHostConfig(c);
|
|
@@ -6810,31 +7670,38 @@ async function startService(options, foreground, hostUrl) {
|
|
|
6810
7670
|
throw new ContractError("conflict");
|
|
6811
7671
|
return existing;
|
|
6812
7672
|
}
|
|
7673
|
+
return launchHost(input, c, foreground, hostUrl, input.config.cwd, env, network);
|
|
7674
|
+
}
|
|
7675
|
+
async function launchHost(input, c, foreground, hostUrl, cwd, env, network) {
|
|
7676
|
+
const serialized = `${JSON.stringify(input)}
|
|
7677
|
+
`;
|
|
7678
|
+
if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
|
|
7679
|
+
throw new ContractError("oversize");
|
|
6813
7680
|
if (!sameNetwork(network, networkSnapshot(process.env)))
|
|
6814
7681
|
throw new ContractError("stale");
|
|
6815
7682
|
const child = spawn(process.execPath, [
|
|
6816
7683
|
...env.NODE_USE_ENV_PROXY === "1" ? ["--use-env-proxy"] : [],
|
|
6817
|
-
|
|
7684
|
+
fileURLToPath2(hostUrl)
|
|
6818
7685
|
], {
|
|
6819
|
-
cwd
|
|
7686
|
+
cwd,
|
|
6820
7687
|
env,
|
|
6821
7688
|
detached: !foreground,
|
|
6822
7689
|
windowsHide: true,
|
|
6823
7690
|
stdio: ["pipe", "pipe", "ignore"]
|
|
6824
7691
|
});
|
|
6825
7692
|
let completed = false;
|
|
6826
|
-
const closure = new Promise((
|
|
7693
|
+
const closure = new Promise((resolve8) => child.once("close", (code2, signal) => {
|
|
6827
7694
|
completed = true;
|
|
6828
7695
|
if (foreground && (code2 !== 0 || signal !== null))
|
|
6829
7696
|
process.exitCode = 1;
|
|
6830
|
-
|
|
7697
|
+
resolve8();
|
|
6831
7698
|
}));
|
|
6832
|
-
const startup = new Promise((
|
|
7699
|
+
const startup = new Promise((resolve8) => {
|
|
6833
7700
|
let bytes = "", resolved = false;
|
|
6834
7701
|
const finish = (value) => {
|
|
6835
7702
|
if (!resolved) {
|
|
6836
7703
|
resolved = true;
|
|
6837
|
-
|
|
7704
|
+
resolve8(value);
|
|
6838
7705
|
}
|
|
6839
7706
|
};
|
|
6840
7707
|
child.on("error", () => finish({ status: "unconfirmed" }));
|
|
@@ -6874,12 +7741,12 @@ async function startService(options, foreground, hostUrl) {
|
|
|
6874
7741
|
let timer;
|
|
6875
7742
|
const result = await Promise.race([
|
|
6876
7743
|
startup,
|
|
6877
|
-
new Promise((
|
|
6878
|
-
timer = setTimeout(() =>
|
|
7744
|
+
new Promise((resolve8) => {
|
|
7745
|
+
timer = setTimeout(() => resolve8({ status: "unconfirmed" }), LIMITS.startWaitMs);
|
|
6879
7746
|
})
|
|
6880
7747
|
]);
|
|
6881
7748
|
clearTimeout(timer);
|
|
6882
|
-
const guidance = startupGuidance(result, input);
|
|
7749
|
+
const guidance = "kind" in input ? "" : startupGuidance(result, input);
|
|
6883
7750
|
if (guidance)
|
|
6884
7751
|
process.stderr.write(guidance);
|
|
6885
7752
|
child.stdout.destroy();
|
|
@@ -6896,12 +7763,12 @@ async function startService(options, foreground, hostUrl) {
|
|
|
6896
7763
|
}
|
|
6897
7764
|
return result;
|
|
6898
7765
|
}
|
|
6899
|
-
async function observeStarted(workspace) {
|
|
7766
|
+
async function observeStarted(workspace, config) {
|
|
6900
7767
|
const deadline = performance.now() + LIMITS.startWaitMs + 2000;
|
|
6901
7768
|
let service = "unconfirmed";
|
|
6902
7769
|
while (performance.now() < deadline) {
|
|
6903
7770
|
try {
|
|
6904
|
-
const c = new ServiceCoordination(workspace);
|
|
7771
|
+
const c = workspace === null ? new GatewayStore(config ?? serverConfigPath(homedir6())) : new ServiceCoordination(workspace);
|
|
6905
7772
|
const observed2 = await serviceStatus(c);
|
|
6906
7773
|
if (observed2 && typeof observed2 === "object" && "status" in observed2)
|
|
6907
7774
|
service = String(observed2.status);
|
|
@@ -6914,26 +7781,26 @@ async function observeStarted(workspace) {
|
|
|
6914
7781
|
}
|
|
6915
7782
|
return { service };
|
|
6916
7783
|
}
|
|
6917
|
-
async function privateLink(workspace, stateHome) {
|
|
7784
|
+
async function privateLink(workspace, stateHome, config) {
|
|
6918
7785
|
try {
|
|
6919
|
-
const value = closed(await nativeCall(new ServiceCoordination(workspace, stateHome), "token-link", true), ["link"]);
|
|
7786
|
+
const value = closed(await nativeCall(workspace === null ? new GatewayStore(config ?? serverConfigPath(homedir6()), stateHome) : new ServiceCoordination(workspace, stateHome), "token-link", true), ["link"]);
|
|
6920
7787
|
return text(value.link, 4096, true);
|
|
6921
7788
|
} catch {
|
|
6922
7789
|
return;
|
|
6923
7790
|
}
|
|
6924
7791
|
}
|
|
6925
7792
|
var ownerControl = {
|
|
6926
|
-
async ownerStatus(workspace) {
|
|
7793
|
+
async ownerStatus(workspace, config) {
|
|
6927
7794
|
try {
|
|
6928
|
-
const observed2 = await serviceStatus(new ServiceCoordination(workspace));
|
|
7795
|
+
const observed2 = await serviceStatus(workspace === null ? new GatewayStore(config ?? serverConfigPath(homedir6())) : new ServiceCoordination(workspace));
|
|
6929
7796
|
return observed2 && typeof observed2 === "object" && "status" in observed2 ? String(observed2.status) : "unavailable";
|
|
6930
7797
|
} catch {
|
|
6931
7798
|
return "unavailable";
|
|
6932
7799
|
}
|
|
6933
7800
|
},
|
|
6934
|
-
async stopOwner(workspace) {
|
|
7801
|
+
async stopOwner(workspace, config) {
|
|
6935
7802
|
try {
|
|
6936
|
-
const c = new ServiceCoordination(workspace);
|
|
7803
|
+
const c = workspace === null ? new GatewayStore(config ?? serverConfigPath(homedir6())) : new ServiceCoordination(workspace);
|
|
6937
7804
|
const observed2 = await serviceStatus(c);
|
|
6938
7805
|
const status = observed2 && typeof observed2 === "object" && "status" in observed2 ? String(observed2.status) : "unavailable";
|
|
6939
7806
|
if (status === "absent" || status === "stopped")
|
|
@@ -6978,7 +7845,7 @@ async function serverAction(s, action, requested, linger) {
|
|
|
6978
7845
|
}
|
|
6979
7846
|
}
|
|
6980
7847
|
function serverGuidance(s, action, result, link) {
|
|
6981
|
-
const cli = piUserInstall(s) ? `node ${JSON.stringify(
|
|
7848
|
+
const cli = piUserInstall(s) ? `node ${JSON.stringify(join15(s.root, "src", "service-cli.mjs"))}` : "cadence";
|
|
6982
7849
|
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
6983
7850
|
` : "";
|
|
6984
7851
|
return note + serverStatusGuidance(s, action, result, cli, link);
|
|
@@ -7010,6 +7877,11 @@ function managerDetail(s) {
|
|
|
7010
7877
|
}
|
|
7011
7878
|
function serverStatusGuidance(s, action, result, cli, link) {
|
|
7012
7879
|
const workspace = JSON.stringify(result.workspace ?? "");
|
|
7880
|
+
if (result.workspace === null && result.status === "started")
|
|
7881
|
+
return `Cadence unified workbench: ${link ?? result.endpoint ?? result.service ?? "unconfirmed"}
|
|
7882
|
+
No default workspace was created. Projects are opened explicitly.
|
|
7883
|
+
Manage: ${cli} server status|restart|stop|uninstall
|
|
7884
|
+
`;
|
|
7013
7885
|
const flags = `${s.scope === "system" ? " --system" : s.platform === "linux" ? " --user" : ""}${serviceName(s) ? ` --name ${serviceName(s)}` : ""}`;
|
|
7014
7886
|
const manage = `Manage: ${cli} server status|restart|stop|uninstall${flags}`;
|
|
7015
7887
|
const when = s.scope === "system" ? "at boot, as root (no linger)" : s.platform === "linux" ? "at boot or login" : "at login";
|
|
@@ -7035,7 +7907,7 @@ ${manage}
|
|
|
7035
7907
|
${linger}`;
|
|
7036
7908
|
}
|
|
7037
7909
|
case "waiting-for-pi-login":
|
|
7038
|
-
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${
|
|
7910
|
+
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join15(agentDirectory(s), "auth.json")}.
|
|
7039
7911
|
Log in to Pi, then: ${cli} server restart${flags}
|
|
7040
7912
|
${config}
|
|
7041
7913
|
${lingerGuidance(result.linger)}`;
|
|
@@ -7131,7 +8003,7 @@ async function serverMain(argv, hostUrl) {
|
|
|
7131
8003
|
else if (flag === "--name" && argv[0])
|
|
7132
8004
|
name = argv.shift();
|
|
7133
8005
|
else if (flag === "--workspace" && action === "install" && argv[0])
|
|
7134
|
-
requested =
|
|
8006
|
+
requested = resolve7(argv.shift());
|
|
7135
8007
|
else if (flag === "--port" && action === "install" && argv[0]) {
|
|
7136
8008
|
port = portValue(argv.shift());
|
|
7137
8009
|
if (!port)
|
|
@@ -7139,7 +8011,7 @@ async function serverMain(argv, hostUrl) {
|
|
|
7139
8011
|
} else
|
|
7140
8012
|
throw new ContractError("invalid");
|
|
7141
8013
|
}
|
|
7142
|
-
const s = hostSystem(
|
|
8014
|
+
const s = hostSystem(dirname13(dirname13(fileURLToPath2(hostUrl))), ownerControl, {
|
|
7143
8015
|
scope,
|
|
7144
8016
|
name
|
|
7145
8017
|
});
|
|
@@ -7184,10 +8056,10 @@ async function serverMain(argv, hostUrl) {
|
|
|
7184
8056
|
throw error;
|
|
7185
8057
|
}
|
|
7186
8058
|
let link;
|
|
7187
|
-
if (result.status === "started" && result.workspace) {
|
|
7188
|
-
Object.assign(result, await observeStarted(result.workspace));
|
|
8059
|
+
if (result.status === "started" && result.workspace !== undefined) {
|
|
8060
|
+
Object.assign(result, await observeStarted(result.workspace, serviceConfigPath(s)));
|
|
7189
8061
|
if (result.service === "alive")
|
|
7190
|
-
link = await privateLink(result.workspace);
|
|
8062
|
+
link = await privateLink(result.workspace, undefined, serviceConfigPath(s));
|
|
7191
8063
|
}
|
|
7192
8064
|
process.stderr.write(serverGuidance(s, action, result, link));
|
|
7193
8065
|
process.stdout.write(`${JSON.stringify(result)}
|
|
@@ -7217,14 +8089,14 @@ async function projectMain(argv, hostUrl) {
|
|
|
7217
8089
|
while (argv.length) {
|
|
7218
8090
|
const flag = argv.shift();
|
|
7219
8091
|
if (!flag.startsWith("--") && !selected2) {
|
|
7220
|
-
options.workspace =
|
|
8092
|
+
options.workspace = resolve7(flag);
|
|
7221
8093
|
selected2 = true;
|
|
7222
8094
|
} else if (!seen.has(flag) && flag === "--create") {
|
|
7223
8095
|
seen.add(flag);
|
|
7224
8096
|
create = true;
|
|
7225
8097
|
} else if (!seen.has(flag) && (flag === "--state-home" || flag === "--agent-dir") && argv[0]) {
|
|
7226
8098
|
seen.add(flag);
|
|
7227
|
-
const value =
|
|
8099
|
+
const value = resolve7(argv.shift());
|
|
7228
8100
|
if (flag === "--state-home")
|
|
7229
8101
|
options.stateHome = value;
|
|
7230
8102
|
else
|
|
@@ -7235,16 +8107,10 @@ async function projectMain(argv, hostUrl) {
|
|
|
7235
8107
|
if (privateWorkspace(options.workspace, options.stateHome))
|
|
7236
8108
|
throw new ContractError("forbidden");
|
|
7237
8109
|
if (create)
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
if (landing.redirected) {
|
|
7241
|
-
process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
|
|
7242
|
-
`);
|
|
7243
|
-
mkdirSync7(landing.workspace, { recursive: true });
|
|
7244
|
-
}
|
|
7245
|
-
options.workspace = landing.workspace;
|
|
8110
|
+
mkdirSync6(options.workspace, { recursive: true });
|
|
8111
|
+
effectiveWorkspace(options.workspace, options.stateHome);
|
|
7246
8112
|
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
7247
|
-
const kept = readHostConfig(c);
|
|
8113
|
+
const kept = c.registryLockDiagnostic() ? undefined : readHostConfig(c);
|
|
7248
8114
|
if (!kept)
|
|
7249
8115
|
options.port = 0;
|
|
7250
8116
|
const result = await startService(options, false, hostUrl);
|
|
@@ -7270,8 +8136,25 @@ async function managementMain(argv, hostUrl) {
|
|
|
7270
8136
|
if (!process.versions.node || process.versions.bun)
|
|
7271
8137
|
throw new ContractError("unsupported-context");
|
|
7272
8138
|
const command = argv.shift();
|
|
8139
|
+
if (command === "workbench") {
|
|
8140
|
+
await gatewayMain(argv, hostUrl);
|
|
8141
|
+
return;
|
|
8142
|
+
}
|
|
8143
|
+
if ([
|
|
8144
|
+
"start",
|
|
8145
|
+
"status",
|
|
8146
|
+
"diagnose",
|
|
8147
|
+
"stop",
|
|
8148
|
+
"token-link",
|
|
8149
|
+
"token-rotate"
|
|
8150
|
+
].includes(command ?? "") && !argv.includes("--workspace")) {
|
|
8151
|
+
await gatewayMain([command, ...argv], hostUrl);
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
7273
8154
|
if (command === "--help" && !argv.length) {
|
|
7274
8155
|
process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
8156
|
+
Without --workspace these commands address the unified workbench (except project recovery); no default project directory is created.
|
|
8157
|
+
cadence workbench start|status|diagnose|stop|token-link --reveal|token-rotate [--server-config PATH] [--state-home PATH]
|
|
7275
8158
|
cadence server install [--workspace PATH] [--no-linger]|status|restart|stop|uninstall
|
|
7276
8159
|
Linux server options: --system (root, no linger) or --user; --name NAME selects an independent service; install also accepts --port PORT (required for a new named service). Root defaults to --system. No automatic sudo or profile migration.
|
|
7277
8160
|
cadence project open [PATH] [--create] [--agent-dir PATH] [--state-home PATH]
|
|
@@ -7349,23 +8232,11 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
7349
8232
|
else
|
|
7350
8233
|
throw new ContractError("invalid");
|
|
7351
8234
|
}
|
|
7352
|
-
|
|
7353
|
-
if (landing.redirected) {
|
|
7354
|
-
if (seen.has("--workspace"))
|
|
7355
|
-
process.stderr.write(`Cadence workspace: ${landing.workspace} (${options.workspace} contains Cadence's private state)
|
|
7356
|
-
`);
|
|
7357
|
-
options.workspace = landing.workspace;
|
|
7358
|
-
if (command === "start")
|
|
7359
|
-
mkdirSync7(landing.workspace, { recursive: true });
|
|
7360
|
-
}
|
|
8235
|
+
effectiveWorkspace(options.workspace, options.stateHome);
|
|
7361
8236
|
let result;
|
|
7362
8237
|
if (command === "start")
|
|
7363
8238
|
result = await startService(options, foreground, hostUrl);
|
|
7364
|
-
else
|
|
7365
|
-
if (command !== "status" && command !== "diagnose")
|
|
7366
|
-
throw new ContractError("unavailable");
|
|
7367
|
-
result = { status: "absent" };
|
|
7368
|
-
} else {
|
|
8239
|
+
else {
|
|
7369
8240
|
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
7370
8241
|
if (command === "recover" && recovery)
|
|
7371
8242
|
result = await localRecovery(c, recovery);
|
|
@@ -7383,8 +8254,6 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
7383
8254
|
else
|
|
7384
8255
|
throw new ContractError("invalid");
|
|
7385
8256
|
}
|
|
7386
|
-
if (landing.redirected && result && typeof result === "object")
|
|
7387
|
-
result = { ...result, workspace: landing.workspace };
|
|
7388
8257
|
if (result !== null)
|
|
7389
8258
|
process.stdout.write(`${JSON.stringify(result)}
|
|
7390
8259
|
`);
|