@abelxiaoxing/cadence 2.0.14 → 2.0.16
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 +76 -7
- package/docs/service-management.md +96 -5
- package/package.json +1 -1
- package/src/native-build.ts +1 -1
- package/src/service/coordination.ts +301 -53
- package/src/service-cli.mjs +2169 -1546
- package/src/service-host.mjs +539 -293
- package/src/worker-public.mjs +1 -0
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +10 -10
- package/web/dist/manifest.json +7 -7
package/src/service-cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { mkdirSync as mkdirSync6 } from "node:fs";
|
|
7
7
|
import { request } from "node:http";
|
|
8
8
|
import { homedir as homedir6 } from "node:os";
|
|
9
|
-
import { dirname as dirname13, join as
|
|
9
|
+
import { dirname as dirname13, join as join16, resolve as resolve7 } from "node:path";
|
|
10
10
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11
11
|
|
|
12
12
|
// src/install-config.mjs
|
|
@@ -327,6 +327,7 @@ var ERROR_CODES = [
|
|
|
327
327
|
"quarantine",
|
|
328
328
|
"capacity",
|
|
329
329
|
"unavailable",
|
|
330
|
+
"registry-locked",
|
|
330
331
|
"auth-unavailable",
|
|
331
332
|
"recording-exhausted",
|
|
332
333
|
"receipt-inconclusive"
|
|
@@ -3232,6 +3233,7 @@ function buildUnit(u) {
|
|
|
3232
3233
|
"KillMode=mixed",
|
|
3233
3234
|
"TimeoutStopSec=90",
|
|
3234
3235
|
"Restart=on-failure",
|
|
3236
|
+
"RestartPreventExitStatus=78",
|
|
3235
3237
|
"RestartSec=10",
|
|
3236
3238
|
"",
|
|
3237
3239
|
"[Install]",
|
|
@@ -3425,7 +3427,7 @@ function autostartStatus(s) {
|
|
|
3425
3427
|
|
|
3426
3428
|
// src/service/coordination.ts
|
|
3427
3429
|
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
3428
|
-
import { opendirSync
|
|
3430
|
+
import { opendirSync } from "node:fs";
|
|
3429
3431
|
import { hostname as hostname2 } from "node:os";
|
|
3430
3432
|
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join5, relative as relative3, sep as sep4 } from "node:path";
|
|
3431
3433
|
var registrationKeys = [
|
|
@@ -3517,6 +3519,21 @@ function names(path2, allowed) {
|
|
|
3517
3519
|
dir.closeSync();
|
|
3518
3520
|
}
|
|
3519
3521
|
}
|
|
3522
|
+
function decodeRegistryLock(snapshot) {
|
|
3523
|
+
const o = closed(json(snapshot), ["schema", "token", "pid", "host"]);
|
|
3524
|
+
return {
|
|
3525
|
+
schema: literal(o.schema, "cadence-service-registry-lock/1"),
|
|
3526
|
+
token: decodeId(o.token),
|
|
3527
|
+
pid: integer(o.pid, 1, 2147483647),
|
|
3528
|
+
host: text(o.host, 255, true)
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
var registryNames = [
|
|
3532
|
+
"format.json",
|
|
3533
|
+
"roots.json",
|
|
3534
|
+
"registry.lock",
|
|
3535
|
+
"recovery-v1"
|
|
3536
|
+
];
|
|
3520
3537
|
function recoveryOwnerStatus(pid, host) {
|
|
3521
3538
|
if (host !== hostname2())
|
|
3522
3539
|
return "ambiguous";
|
|
@@ -3546,19 +3563,25 @@ function recoveryDigest(registry, selected) {
|
|
|
3546
3563
|
}
|
|
3547
3564
|
|
|
3548
3565
|
class ServiceCoordination {
|
|
3549
|
-
|
|
3566
|
+
project;
|
|
3550
3567
|
files;
|
|
3551
|
-
directory;
|
|
3552
3568
|
registryDirectory;
|
|
3553
3569
|
stateHome;
|
|
3554
3570
|
constructor(workspace, stateHome) {
|
|
3555
|
-
this.
|
|
3556
|
-
|
|
3571
|
+
this.project = workspace === null ? undefined : new TaskStore(resolveWorkspace(workspace).root, stateHome);
|
|
3572
|
+
this.stateHome = this.project ? dirname5(dirname5(this.project.privateRoot)) : canonicalAmbientRoot(stateHome ?? defaultStateHome());
|
|
3573
|
+
const namespace = join5(this.stateHome, "cadence-assistant-v1");
|
|
3557
3574
|
this.files = new PrivateFiles(namespace);
|
|
3558
|
-
this.stateHome = dirname5(namespace);
|
|
3559
|
-
this.directory = join5(this.store.privateRoot, "service-v1");
|
|
3560
3575
|
this.registryDirectory = join5(namespace, "service-registry-v1");
|
|
3561
3576
|
}
|
|
3577
|
+
get store() {
|
|
3578
|
+
if (!this.project)
|
|
3579
|
+
throw new ContractError("forbidden");
|
|
3580
|
+
return this.project;
|
|
3581
|
+
}
|
|
3582
|
+
get directory() {
|
|
3583
|
+
return join5(this.store.privateRoot, "service-v1");
|
|
3584
|
+
}
|
|
3562
3585
|
path(name) {
|
|
3563
3586
|
return join5(this.directory, name);
|
|
3564
3587
|
}
|
|
@@ -3568,11 +3591,7 @@ class ServiceCoordination {
|
|
|
3568
3591
|
registry() {
|
|
3569
3592
|
if (!this.files.directory(this.registryDirectory))
|
|
3570
3593
|
return;
|
|
3571
|
-
names(this.registryDirectory,
|
|
3572
|
-
"format.json",
|
|
3573
|
-
"roots.json",
|
|
3574
|
-
"registry.lock"
|
|
3575
|
-
]);
|
|
3594
|
+
names(this.registryDirectory, registryNames);
|
|
3576
3595
|
const format = this.files.read(this.registryPath("format.json"));
|
|
3577
3596
|
const snapshot = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
|
|
3578
3597
|
if (!format || !snapshot)
|
|
@@ -3583,7 +3602,7 @@ class ServiceCoordination {
|
|
|
3583
3602
|
const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
|
|
3584
3603
|
const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
|
|
3585
3604
|
for (const [i, row] of entries.entries()) {
|
|
3586
|
-
if (row.stateHome !== this.stateHome ||
|
|
3605
|
+
if (row.stateHome !== this.stateHome || canonicalAmbientRoot(row.root) !== row.root)
|
|
3587
3606
|
refused();
|
|
3588
3607
|
if (entries.slice(0, i).some((other) => rootsOverlap(row.root, other.root) || row.endpoint === other.endpoint))
|
|
3589
3608
|
refused();
|
|
@@ -3597,10 +3616,10 @@ class ServiceCoordination {
|
|
|
3597
3616
|
}
|
|
3598
3617
|
};
|
|
3599
3618
|
}
|
|
3600
|
-
assertNamespace(walks) {
|
|
3601
|
-
if (!this.files.directory(
|
|
3619
|
+
assertNamespace(walks, directory2 = this.directory) {
|
|
3620
|
+
if (!this.files.directory(directory2, false, walks))
|
|
3602
3621
|
refused();
|
|
3603
|
-
names(
|
|
3622
|
+
names(directory2, [
|
|
3604
3623
|
"format.json",
|
|
3605
3624
|
"config.json",
|
|
3606
3625
|
"registration.json",
|
|
@@ -3612,15 +3631,16 @@ class ServiceCoordination {
|
|
|
3612
3631
|
"history-v2",
|
|
3613
3632
|
"conversations-v1"
|
|
3614
3633
|
]);
|
|
3615
|
-
this.files.directory(join5(
|
|
3634
|
+
this.files.directory(join5(directory2, "history-v2"), false, walks);
|
|
3616
3635
|
}
|
|
3617
|
-
selected() {
|
|
3636
|
+
selected(directory2 = this.directory, root = this.store.root, stoppedOnly = false) {
|
|
3618
3637
|
const walks = new DirectoryWalks;
|
|
3619
|
-
if (!this.files.directory(
|
|
3638
|
+
if (!this.files.directory(directory2, false, walks))
|
|
3620
3639
|
return;
|
|
3621
|
-
this.assertNamespace(walks);
|
|
3622
|
-
const
|
|
3623
|
-
const
|
|
3640
|
+
this.assertNamespace(walks, directory2);
|
|
3641
|
+
const path2 = (name) => join5(directory2, name);
|
|
3642
|
+
const manifest = this.files.read(path2("format.json"), undefined, walks);
|
|
3643
|
+
const config = this.files.read(path2("config.json"), undefined, walks);
|
|
3624
3644
|
if (!manifest || !config)
|
|
3625
3645
|
refused();
|
|
3626
3646
|
const f = closed(json(manifest), [
|
|
@@ -3630,20 +3650,22 @@ class ServiceCoordination {
|
|
|
3630
3650
|
"configIdentity"
|
|
3631
3651
|
]);
|
|
3632
3652
|
literal(f.schema, "cadence-service-format/1");
|
|
3633
|
-
literal(f.root,
|
|
3653
|
+
literal(f.root, root);
|
|
3634
3654
|
literal(f.stateHome, this.stateHome);
|
|
3635
3655
|
literal(f.configIdentity, config.identity);
|
|
3636
3656
|
const c = decodeOwnerConfig(json(config));
|
|
3637
|
-
const hostConfig = this.files.read(
|
|
3657
|
+
const hostConfig = this.files.read(path2("host-config.json"), undefined, walks);
|
|
3638
3658
|
if (hostConfig && hostConfig.identity !== c.policyIdentity)
|
|
3639
3659
|
refused();
|
|
3640
|
-
const registration = this.files.read(
|
|
3641
|
-
const owner = this.files.read(
|
|
3642
|
-
const capability = this.files.read(
|
|
3643
|
-
const checkout = this.files.read(
|
|
3660
|
+
const registration = this.files.read(path2("registration.json"), undefined, walks);
|
|
3661
|
+
const owner = this.files.read(path2("owner.lock"), undefined, walks);
|
|
3662
|
+
const capability = this.files.read(path2("local-capability.json"), undefined, walks);
|
|
3663
|
+
const checkout = this.files.read(join5(dirname5(directory2), "checkout.lock"), 8192, walks);
|
|
3644
3664
|
const value = registration ? decodeRegistration(json(registration)) : undefined;
|
|
3645
|
-
if (value && (value.root !==
|
|
3665
|
+
if (value && (value.root !== root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
|
|
3646
3666
|
refused();
|
|
3667
|
+
if (stoppedOnly && (checkout || value && value.phase !== "stopped"))
|
|
3668
|
+
throw new ContractError("quarantine");
|
|
3647
3669
|
if (value && value.phase !== "stopped") {
|
|
3648
3670
|
if (!owner || !checkout || !capability)
|
|
3649
3671
|
refused();
|
|
@@ -3723,18 +3745,21 @@ class ServiceCoordination {
|
|
|
3723
3745
|
}
|
|
3724
3746
|
withRegistry(operation) {
|
|
3725
3747
|
this.files.directory(this.registryDirectory, true);
|
|
3726
|
-
names(this.registryDirectory,
|
|
3727
|
-
"format.json",
|
|
3728
|
-
"roots.json",
|
|
3729
|
-
"registry.lock"
|
|
3730
|
-
]);
|
|
3748
|
+
names(this.registryDirectory, registryNames);
|
|
3731
3749
|
const lockPath = this.registryPath("registry.lock");
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3750
|
+
let lock;
|
|
3751
|
+
try {
|
|
3752
|
+
lock = this.files.create(lockPath, encoded({
|
|
3753
|
+
schema: "cadence-service-registry-lock/1",
|
|
3754
|
+
token: randomUUID3(),
|
|
3755
|
+
pid: process.pid,
|
|
3756
|
+
host: hostname2()
|
|
3757
|
+
}));
|
|
3758
|
+
} catch (error) {
|
|
3759
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST")
|
|
3760
|
+
throw new ContractError("registry-locked");
|
|
3761
|
+
throw error;
|
|
3762
|
+
}
|
|
3738
3763
|
let dirty = false;
|
|
3739
3764
|
try {
|
|
3740
3765
|
let format = this.files.read(this.registryPath("format.json"));
|
|
@@ -3758,11 +3783,7 @@ class ServiceCoordination {
|
|
|
3758
3783
|
}, () => {
|
|
3759
3784
|
this.files.assert(lockPath, lock);
|
|
3760
3785
|
this.files.assert(this.registryPath("format.json"), format);
|
|
3761
|
-
names(this.registryDirectory,
|
|
3762
|
-
"format.json",
|
|
3763
|
-
"roots.json",
|
|
3764
|
-
"registry.lock"
|
|
3765
|
-
]);
|
|
3786
|
+
names(this.registryDirectory, registryNames);
|
|
3766
3787
|
});
|
|
3767
3788
|
this.files.remove(lockPath, lock);
|
|
3768
3789
|
return result;
|
|
@@ -3773,6 +3794,7 @@ class ServiceCoordination {
|
|
|
3773
3794
|
}
|
|
3774
3795
|
}
|
|
3775
3796
|
acquire(input, resolvedEndpoint = input.endpoint, initializeNew, reselectExisting) {
|
|
3797
|
+
this.store;
|
|
3776
3798
|
const config = decodeOwnerConfig(input);
|
|
3777
3799
|
const bound = endpoint(resolvedEndpoint);
|
|
3778
3800
|
if (!endpointMatches(config.endpoint, bound))
|
|
@@ -3894,6 +3916,154 @@ class ServiceCoordination {
|
|
|
3894
3916
|
});
|
|
3895
3917
|
});
|
|
3896
3918
|
}
|
|
3919
|
+
registryLockDiagnostic() {
|
|
3920
|
+
try {
|
|
3921
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
3922
|
+
if (!lock)
|
|
3923
|
+
return;
|
|
3924
|
+
const value = decodeRegistryLock(lock);
|
|
3925
|
+
return {
|
|
3926
|
+
reason: "registry-locked",
|
|
3927
|
+
owner: recoveryOwnerStatus(value.pid, value.host)
|
|
3928
|
+
};
|
|
3929
|
+
} catch {
|
|
3930
|
+
return { reason: "registry-locked", owner: "unreadable" };
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
registryRecoveryState() {
|
|
3934
|
+
const lock = this.files.read(this.registryPath("registry.lock"));
|
|
3935
|
+
if (!lock)
|
|
3936
|
+
throw new ContractError("conflict");
|
|
3937
|
+
const value = decodeRegistryLock(lock);
|
|
3938
|
+
names(this.registryDirectory, registryNames);
|
|
3939
|
+
const format = this.files.read(this.registryPath("format.json"));
|
|
3940
|
+
const roots = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
|
|
3941
|
+
const registry = !format && !roots ? undefined : this.registry();
|
|
3942
|
+
const entries = registry?.value.entries ?? [];
|
|
3943
|
+
const protectedRoots = new Set(entries.map((row) => hash(row.root)));
|
|
3944
|
+
const proofs = [];
|
|
3945
|
+
const directory2 = opendirSync(this.files.namespace);
|
|
3946
|
+
try {
|
|
3947
|
+
let count = 0;
|
|
3948
|
+
for (let entry = directory2.readSync();entry; entry = directory2.readSync()) {
|
|
3949
|
+
if (++count > 1024)
|
|
3950
|
+
throw new ContractError("capacity");
|
|
3951
|
+
if (!/^[a-f0-9]{64}$/.test(entry.name) || protectedRoots.has(entry.name))
|
|
3952
|
+
continue;
|
|
3953
|
+
const service = join5(this.files.namespace, entry.name, "service-v1");
|
|
3954
|
+
if (!this.files.directory(service))
|
|
3955
|
+
continue;
|
|
3956
|
+
const manifest = this.files.read(join5(service, "format.json"));
|
|
3957
|
+
if (!manifest)
|
|
3958
|
+
refused();
|
|
3959
|
+
const f = closed(json(manifest), [
|
|
3960
|
+
"schema",
|
|
3961
|
+
"root",
|
|
3962
|
+
"stateHome",
|
|
3963
|
+
"configIdentity"
|
|
3964
|
+
]);
|
|
3965
|
+
const root = pathValue(f.root);
|
|
3966
|
+
if (hash(root) !== entry.name)
|
|
3967
|
+
refused();
|
|
3968
|
+
const selected = this.selected(service, root, true);
|
|
3969
|
+
if (!selected)
|
|
3970
|
+
throw new ContractError("quarantine");
|
|
3971
|
+
proofs.push([
|
|
3972
|
+
entry.name,
|
|
3973
|
+
...[
|
|
3974
|
+
selected.manifest,
|
|
3975
|
+
selected.config,
|
|
3976
|
+
selected.hostConfig,
|
|
3977
|
+
selected.registration
|
|
3978
|
+
].map((file) => file ? [file.identity, file.stamp] : null)
|
|
3979
|
+
]);
|
|
3980
|
+
}
|
|
3981
|
+
} finally {
|
|
3982
|
+
directory2.closeSync();
|
|
3983
|
+
}
|
|
3984
|
+
proofs.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en"));
|
|
3985
|
+
const stateDigest = hash(encoded([
|
|
3986
|
+
lock.identity,
|
|
3987
|
+
lock.stamp,
|
|
3988
|
+
format ? [format.identity, format.stamp] : null,
|
|
3989
|
+
roots ? [roots.identity, roots.stamp] : null,
|
|
3990
|
+
proofs
|
|
3991
|
+
]));
|
|
3992
|
+
const epoch = hash(encoded([lock.identity, lock.stamp]));
|
|
3993
|
+
const archive = this.registryPath(join5("recovery-v1", epoch));
|
|
3994
|
+
const attempts = [];
|
|
3995
|
+
let previousResolver = null;
|
|
3996
|
+
if (this.files.directory(archive)) {
|
|
3997
|
+
names(archive, Array.from({ length: 128 }, (_, n) => `${n}.json`));
|
|
3998
|
+
for (let n = 0;n < 128; n++) {
|
|
3999
|
+
const attempt = this.files.read(join5(archive, `${n}.json`), 32768);
|
|
4000
|
+
if (!attempt) {
|
|
4001
|
+
names(archive, attempts.map((_, i) => `${i}.json`));
|
|
4002
|
+
break;
|
|
4003
|
+
}
|
|
4004
|
+
const o = closed(json(attempt, 32768), [
|
|
4005
|
+
"schema",
|
|
4006
|
+
"lock",
|
|
4007
|
+
"pid",
|
|
4008
|
+
"host",
|
|
4009
|
+
"digest"
|
|
4010
|
+
]);
|
|
4011
|
+
literal(o.schema, "cadence-registry-recovery/1");
|
|
4012
|
+
literal(o.lock, lock.bytes.toString("utf8"));
|
|
4013
|
+
decodeHash(o.digest);
|
|
4014
|
+
previousResolver = recoveryOwnerStatus(integer(o.pid, 1, 2147483647), text(o.host, 255, true));
|
|
4015
|
+
attempts.push(attempt);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
const inspection = {
|
|
4019
|
+
scope: "global-registry",
|
|
4020
|
+
epoch,
|
|
4021
|
+
coordinationDigest: hash(encoded([stateDigest, attempts.map((a) => [a.identity, a.stamp])])),
|
|
4022
|
+
owner: recoveryOwnerStatus(value.pid, value.host),
|
|
4023
|
+
process: { pid: value.pid, host: value.host },
|
|
4024
|
+
reservations: entries.length,
|
|
4025
|
+
previousResolver,
|
|
4026
|
+
resolution: "local-interactive-only"
|
|
4027
|
+
};
|
|
4028
|
+
return { lock, archive, attempts, stateDigest, inspection };
|
|
4029
|
+
}
|
|
4030
|
+
inspectRegistryRecovery() {
|
|
4031
|
+
return this.registryRecoveryState().inspection;
|
|
4032
|
+
}
|
|
4033
|
+
resolveRegistryRecovery(expected, assertAuthority) {
|
|
4034
|
+
assertAuthority();
|
|
4035
|
+
const state = this.registryRecoveryState();
|
|
4036
|
+
if (encoded(state.inspection) !== encoded(expected))
|
|
4037
|
+
throw new ContractError("conflict");
|
|
4038
|
+
if (expected.owner !== "absent" || expected.previousResolver !== null && expected.previousResolver !== "absent")
|
|
4039
|
+
throw new ContractError("quarantine");
|
|
4040
|
+
if (state.attempts.length >= 128)
|
|
4041
|
+
throw new ContractError("capacity");
|
|
4042
|
+
assertAuthority();
|
|
4043
|
+
const receiptPath = join5(state.archive, `${state.attempts.length}.json`);
|
|
4044
|
+
const receipt = this.files.create(receiptPath, encoded({
|
|
4045
|
+
schema: "cadence-registry-recovery/1",
|
|
4046
|
+
lock: state.lock.bytes.toString("utf8"),
|
|
4047
|
+
pid: process.pid,
|
|
4048
|
+
host: hostname2(),
|
|
4049
|
+
digest: expected.coordinationDigest
|
|
4050
|
+
}), 32768);
|
|
4051
|
+
assertAuthority();
|
|
4052
|
+
const current = this.registryRecoveryState();
|
|
4053
|
+
if (current.stateDigest !== state.stateDigest || current.inspection.owner !== "absent" || current.attempts.length !== state.attempts.length + 1)
|
|
4054
|
+
throw new ContractError("conflict");
|
|
4055
|
+
for (const [n, attempt] of state.attempts.entries())
|
|
4056
|
+
this.files.assert(join5(state.archive, `${n}.json`), attempt, 32768);
|
|
4057
|
+
this.files.assert(receiptPath, receipt, 32768);
|
|
4058
|
+
assertAuthority();
|
|
4059
|
+
this.files.remove(this.registryPath("registry.lock"), state.lock);
|
|
4060
|
+
return {
|
|
4061
|
+
status: "resolved",
|
|
4062
|
+
scope: "global-registry",
|
|
4063
|
+
recoveryId: expected.epoch,
|
|
4064
|
+
notice: "registry-only-project-fences-preserved"
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
3897
4067
|
prepareRecoveryRelease(expected, assertAuthority) {
|
|
3898
4068
|
assertAuthority();
|
|
3899
4069
|
if (this.recoverySnapshot().digest !== expected)
|
|
@@ -4045,7 +4215,7 @@ class OwnerLease {
|
|
|
4045
4215
|
|
|
4046
4216
|
// src/service/gateway-cli.ts
|
|
4047
4217
|
import { homedir as homedir5 } from "node:os";
|
|
4048
|
-
import { dirname as
|
|
4218
|
+
import { dirname as dirname10, join as join12, resolve as resolve6 } from "node:path";
|
|
4049
4219
|
import { fileURLToPath } from "node:url";
|
|
4050
4220
|
|
|
4051
4221
|
// src/service/config.ts
|
|
@@ -4099,6 +4269,7 @@ function localOrigin(policy, actualPort) {
|
|
|
4099
4269
|
// src/service/gateway-store.ts
|
|
4100
4270
|
import { randomBytes as randomBytes4, randomUUID as randomUUID5 } from "node:crypto";
|
|
4101
4271
|
import { opendirSync as opendirSync2 } from "node:fs";
|
|
4272
|
+
import { hostname as hostname3 } from "node:os";
|
|
4102
4273
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
4103
4274
|
|
|
4104
4275
|
// src/service/auth.ts
|
|
@@ -4473,6 +4644,8 @@ function decodeGatewayProject(value) {
|
|
|
4473
4644
|
return project;
|
|
4474
4645
|
}
|
|
4475
4646
|
function registration(value) {
|
|
4647
|
+
const hasHost = !!value && typeof value === "object" && Object.hasOwn(value, "host");
|
|
4648
|
+
const hasRecovery = !!value && typeof value === "object" && Object.hasOwn(value, "recovery");
|
|
4476
4649
|
const o = closed(value, [
|
|
4477
4650
|
"schema",
|
|
4478
4651
|
"epoch",
|
|
@@ -4481,8 +4654,22 @@ function registration(value) {
|
|
|
4481
4654
|
"capability",
|
|
4482
4655
|
"selection",
|
|
4483
4656
|
"pid",
|
|
4484
|
-
"phase"
|
|
4657
|
+
"phase",
|
|
4658
|
+
...hasHost ? ["host"] : [],
|
|
4659
|
+
...hasRecovery ? ["recovery"] : []
|
|
4485
4660
|
]);
|
|
4661
|
+
let recovery;
|
|
4662
|
+
if (hasRecovery) {
|
|
4663
|
+
const r = closed(o.recovery, ["receipt", "sha256", "stamp"]);
|
|
4664
|
+
const receipt = text(r.receipt, 128, true);
|
|
4665
|
+
if (!/^recovery-v1\/[a-f0-9]{64}\/(?:0|[1-9][0-9]{0,2})\.json$/.test(receipt))
|
|
4666
|
+
throw new ContractError("corrupt-selected");
|
|
4667
|
+
recovery = {
|
|
4668
|
+
receipt,
|
|
4669
|
+
sha256: decodeHash(r.sha256),
|
|
4670
|
+
stamp: text(r.stamp, 512, true)
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4486
4673
|
const endpoint2 = text(o.endpoint, 128, true), capability = text(o.capability, 43, true);
|
|
4487
4674
|
if (!/^http:\/\/(?:127\.0\.0\.1|\[::1\]):[1-9][0-9]{0,4}$/.test(endpoint2) || !/^[A-Za-z0-9_-]{43}$/.test(capability))
|
|
4488
4675
|
throw new ContractError("corrupt-selected");
|
|
@@ -4493,7 +4680,9 @@ function registration(value) {
|
|
|
4493
4680
|
endpoint: endpoint2,
|
|
4494
4681
|
capability,
|
|
4495
4682
|
selection: decodeHash(o.selection),
|
|
4496
|
-
pid: integer(o.pid, 1),
|
|
4683
|
+
pid: integer(o.pid, 1, 2147483647),
|
|
4684
|
+
...hasHost ? { host: text(o.host, 255, true) } : {},
|
|
4685
|
+
...recovery ? { recovery } : {},
|
|
4497
4686
|
phase: oneOf(o.phase, ["alive", "stopping", "stopped", "quarantined"])
|
|
4498
4687
|
};
|
|
4499
4688
|
}
|
|
@@ -4553,6 +4742,8 @@ class GatewayStore {
|
|
|
4553
4742
|
const previous = this.read();
|
|
4554
4743
|
if (previous && previous.phase !== "stopped")
|
|
4555
4744
|
throw new ContractError("quarantine");
|
|
4745
|
+
if (previous?.recovery)
|
|
4746
|
+
this.associatedProjects();
|
|
4556
4747
|
this.lock = this.files.create(this.path("gateway.lock"), encoded({ epoch: randomUUID5() }));
|
|
4557
4748
|
const source = { serverConfig: this.serverConfig, token };
|
|
4558
4749
|
if (!previous)
|
|
@@ -4572,6 +4763,8 @@ class GatewayStore {
|
|
|
4572
4763
|
capability: randomBytes4(32).toString("base64url"),
|
|
4573
4764
|
selection: hash(inputBytes),
|
|
4574
4765
|
pid: process.pid,
|
|
4766
|
+
host: hostname3(),
|
|
4767
|
+
...previous?.recovery ? { recovery: previous.recovery } : {},
|
|
4575
4768
|
phase: "alive"
|
|
4576
4769
|
};
|
|
4577
4770
|
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));
|
|
@@ -4615,6 +4808,21 @@ class GatewayStore {
|
|
|
4615
4808
|
}
|
|
4616
4809
|
return out;
|
|
4617
4810
|
}
|
|
4811
|
+
associatedProjects() {
|
|
4812
|
+
const projects = this.projects(), recovery = this.read()?.recovery;
|
|
4813
|
+
if (!recovery)
|
|
4814
|
+
return projects;
|
|
4815
|
+
const receipt = this.files.read(this.path(recovery.receipt), 65536);
|
|
4816
|
+
if (!receipt || receipt.identity !== recovery.sha256 || receipt.stamp !== recovery.stamp)
|
|
4817
|
+
throw new ContractError("corrupt-selected");
|
|
4818
|
+
const value = json(receipt, 65536);
|
|
4819
|
+
literal(value.schema, "cadence-workbench-recovery/1");
|
|
4820
|
+
const retired = list(value.retiredProjects, 128, (entry) => {
|
|
4821
|
+
const row = closed(entry, ["id", "stamp"]);
|
|
4822
|
+
return { id: decodeHash(row.id), stamp: text(row.stamp, 512, true) };
|
|
4823
|
+
});
|
|
4824
|
+
return projects.filter((project) => !retired.some((row) => row.id === project.id && row.stamp === this.files.read(this.path(`projects/${project.id}.json`))?.stamp));
|
|
4825
|
+
}
|
|
4618
4826
|
saveProject(project) {
|
|
4619
4827
|
this.assert();
|
|
4620
4828
|
const p = decodeGatewayProject(project), path2 = this.path(`projects/${p.id}.json`), before = this.files.read(path2);
|
|
@@ -4630,1202 +4838,660 @@ class GatewayStore {
|
|
|
4630
4838
|
}
|
|
4631
4839
|
}
|
|
4632
4840
|
|
|
4633
|
-
// src/service/
|
|
4634
|
-
import {
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
const
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
"
|
|
4649
|
-
"tokenSource",
|
|
4650
|
-
"tokenFile"
|
|
4651
|
-
]);
|
|
4652
|
-
const tokenSource = oneOf(o.tokenSource, [
|
|
4653
|
-
"generated",
|
|
4654
|
-
"environment",
|
|
4655
|
-
"file",
|
|
4656
|
-
"config"
|
|
4657
|
-
]);
|
|
4658
|
-
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
4659
|
-
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
4660
|
-
throw new ContractError("invalid");
|
|
4841
|
+
// src/service/recovery-cli.ts
|
|
4842
|
+
import { isatty } from "node:tty";
|
|
4843
|
+
|
|
4844
|
+
// src/history/recovery.ts
|
|
4845
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
|
|
4846
|
+
import { lstatSync as lstatSync7, opendirSync as opendirSync4 } from "node:fs";
|
|
4847
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
4848
|
+
|
|
4849
|
+
// src/service/recovery.ts
|
|
4850
|
+
async function inspectRecovery(coordination, history) {
|
|
4851
|
+
const selected = coordination.recoverySnapshot();
|
|
4852
|
+
const facts = history ? await history.inspect(selected.registration.epoch) : null;
|
|
4853
|
+
if (facts)
|
|
4854
|
+
decodeHash(facts.digest);
|
|
4855
|
+
if (coordination.recoverySnapshot().digest !== selected.digest)
|
|
4856
|
+
throw new ContractError("conflict");
|
|
4661
4857
|
return {
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
tokenFile
|
|
4858
|
+
epoch: selected.registration.epoch,
|
|
4859
|
+
coordinationDigest: selected.digest,
|
|
4860
|
+
history: facts,
|
|
4861
|
+
owner: recoveryOwnerStatus(selected.registration.pid, selected.registration.host),
|
|
4862
|
+
resolution: "local-interactive-only"
|
|
4668
4863
|
};
|
|
4669
4864
|
}
|
|
4670
|
-
function
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
const
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
const
|
|
4678
|
-
if (
|
|
4865
|
+
async function resolveRecovery(coordination, confirmation, history) {
|
|
4866
|
+
if (typeof confirmation.assertLocalAuthority !== "function" || confirmation.acknowledgeEscapedProcesses !== true || confirmation.processes !== "externally-resolved" || confirmation.credentials !== "independently-resolved" || confirmation.storage !== "independently-resolved")
|
|
4867
|
+
throw new ContractError("forbidden");
|
|
4868
|
+
const expected = structuredClone(confirmation.expected);
|
|
4869
|
+
const authority = () => confirmation.assertLocalAuthority(structuredClone(expected));
|
|
4870
|
+
authority();
|
|
4871
|
+
const release = coordination.prepareRecoveryRelease(expected.coordinationDigest, authority);
|
|
4872
|
+
const current = await inspectRecovery(coordination, history);
|
|
4873
|
+
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
4679
4874
|
throw new ContractError("conflict");
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
try {
|
|
4686
|
-
if (facts.NODE_OPTIONS.present)
|
|
4687
|
-
throw new Error;
|
|
4688
|
-
route = sdkNetworkEnvironment(facts, networkSnapshot(undefined));
|
|
4689
|
-
} catch {
|
|
4690
|
-
throw new ContractError("unsupported-context");
|
|
4691
|
-
}
|
|
4692
|
-
const result = Object.create(null);
|
|
4693
|
-
for (const key of Object.keys(ambient)) {
|
|
4694
|
-
const d = Object.getOwnPropertyDescriptor(ambient, key);
|
|
4695
|
-
if (!d || !("value" in d) || typeof d.value !== "string")
|
|
4696
|
-
throw new ContractError("invalid");
|
|
4697
|
-
if (/^(?:CADENCE_|PI_|ABEL_|TRELLIS_|GIT_)/i.test(key) || networkKeys.some((name) => name === key))
|
|
4698
|
-
continue;
|
|
4699
|
-
result[key] = d.value;
|
|
4700
|
-
}
|
|
4701
|
-
if (route.redactionLiterals.length) {
|
|
4702
|
-
result.HTTP_PROXY = route.env.HTTP_PROXY;
|
|
4703
|
-
result.HTTPS_PROXY = route.env.HTTPS_PROXY;
|
|
4704
|
-
result.NODE_USE_ENV_PROXY = "1";
|
|
4705
|
-
}
|
|
4706
|
-
result.PI_OFFLINE = "1";
|
|
4707
|
-
result.NO_COLOR = "1";
|
|
4708
|
-
if (!sameNetwork(facts, networkSnapshot(ambient)))
|
|
4709
|
-
throw new ContractError("stale");
|
|
4710
|
-
return result;
|
|
4711
|
-
}
|
|
4712
|
-
function selectStart(options, ambient) {
|
|
4713
|
-
const workspace = resolveWorkspace(options.workspace), c = new ServiceCoordination(workspace.root, options.stateHome);
|
|
4714
|
-
const old = readHostConfig(c);
|
|
4715
|
-
const env = Object.getOwnPropertyDescriptor(ambient, "CADENCE_WEB_TOKEN");
|
|
4716
|
-
if (env && (!("value" in env) || typeof env.value !== "string"))
|
|
4717
|
-
throw new ContractError("invalid");
|
|
4718
|
-
let fromFile;
|
|
4719
|
-
if (options.config) {
|
|
4720
|
-
const path2 = resolve5(options.config), f = new PrivateFiles(dirname7(path2));
|
|
4721
|
-
const bytes = f.read(path2);
|
|
4722
|
-
if (!bytes)
|
|
4723
|
-
throw new ContractError("corrupt-selected");
|
|
4724
|
-
fromFile = decodeHostConfig(json(bytes));
|
|
4725
|
-
}
|
|
4726
|
-
const base = old ?? fromFile ?? decodeHostConfig({
|
|
4727
|
-
schema: "cadence-service-host/1",
|
|
4728
|
-
cwd: workspace.cwd,
|
|
4729
|
-
http: {
|
|
4730
|
-
schema: "cadence-service-http/1",
|
|
4731
|
-
host: options.host ?? "127.0.0.1",
|
|
4732
|
-
port: options.port ?? 17423,
|
|
4733
|
-
proxy: null
|
|
4734
|
-
},
|
|
4735
|
-
agentDir: resolve5(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join8(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
4736
|
-
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
4737
|
-
tokenFile: options.tokenFile ? resolve5(options.tokenFile) : null
|
|
4875
|
+
if (current.owner !== "absent" || !current.history)
|
|
4876
|
+
throw new ContractError("quarantine");
|
|
4877
|
+
const fact = await history.append({
|
|
4878
|
+
...confirmation,
|
|
4879
|
+
expected
|
|
4738
4880
|
});
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
if (server) {
|
|
4742
|
-
if (options.tokenFile !== undefined)
|
|
4743
|
-
throw new ContractError("conflict");
|
|
4744
|
-
const { port } = readServerConfig(server).config;
|
|
4745
|
-
selected = decodeHostConfig({
|
|
4746
|
-
...base,
|
|
4747
|
-
http: { ...base.http, port },
|
|
4748
|
-
tokenSource: "config",
|
|
4749
|
-
tokenFile: server
|
|
4750
|
-
});
|
|
4751
|
-
}
|
|
4752
|
-
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)
|
|
4881
|
+
authority();
|
|
4882
|
+
if (fact.epoch !== expected.epoch || fact.coordinationDigest !== expected.coordinationDigest || fact.historyDigest !== expected.history?.digest)
|
|
4753
4883
|
throw new ContractError("conflict");
|
|
4884
|
+
release(() => history.assertCommitted(fact));
|
|
4754
4885
|
return {
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
}
|
|
4761
|
-
function decodeHostInput(value) {
|
|
4762
|
-
const o = closed(value, [
|
|
4763
|
-
"workspace",
|
|
4764
|
-
"stateHome",
|
|
4765
|
-
"config",
|
|
4766
|
-
"environmentToken"
|
|
4767
|
-
]);
|
|
4768
|
-
const config = decodeHostConfig(o.config);
|
|
4769
|
-
const environmentToken = o.environmentToken === null ? null : text(o.environmentToken, 1024, true);
|
|
4770
|
-
if (config.tokenSource === "environment" !== (environmentToken !== null))
|
|
4771
|
-
throw new ContractError("invalid");
|
|
4772
|
-
return {
|
|
4773
|
-
workspace: absolute(o.workspace),
|
|
4774
|
-
stateHome: absolute(o.stateHome),
|
|
4775
|
-
config,
|
|
4776
|
-
environmentToken
|
|
4777
|
-
};
|
|
4778
|
-
}
|
|
4779
|
-
function portValue(value) {
|
|
4780
|
-
if (!/^(0|[1-9][0-9]{0,4})$/.test(value))
|
|
4781
|
-
throw new ContractError("invalid");
|
|
4782
|
-
return integer(Number(value), 0, 65535);
|
|
4783
|
-
}
|
|
4784
|
-
var STARTUP_STAGES = [
|
|
4785
|
-
"package",
|
|
4786
|
-
"pi-agent-directory",
|
|
4787
|
-
"pi-auth",
|
|
4788
|
-
"pi-models",
|
|
4789
|
-
"pi-runtime",
|
|
4790
|
-
"service"
|
|
4791
|
-
];
|
|
4792
|
-
function decodeStartupReport(value) {
|
|
4793
|
-
const o = closed(value, ["status", "error", "stage"]);
|
|
4794
|
-
return {
|
|
4795
|
-
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
4796
|
-
error: oneOf(o.error, ERROR_CODES),
|
|
4797
|
-
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
4886
|
+
status: "resolved",
|
|
4887
|
+
epoch: fact.epoch,
|
|
4888
|
+
recoveryId: fact.recoveryId,
|
|
4889
|
+
committedSeq: fact.committedSeq,
|
|
4890
|
+
notice: "operator-acknowledgment-not-certification"
|
|
4798
4891
|
};
|
|
4799
4892
|
}
|
|
4800
4893
|
|
|
4801
|
-
// src/
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4894
|
+
// src/history/files.ts
|
|
4895
|
+
import { createHash as createHash5, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "node:crypto";
|
|
4896
|
+
import {
|
|
4897
|
+
closeSync as closeSync5,
|
|
4898
|
+
constants as constants5,
|
|
4899
|
+
fstatSync as fstatSync4,
|
|
4900
|
+
fsyncSync as fsyncSync3,
|
|
4901
|
+
lstatSync as lstatSync6,
|
|
4902
|
+
opendirSync as opendirSync3,
|
|
4903
|
+
openSync as openSync5,
|
|
4904
|
+
statfsSync
|
|
4905
|
+
} from "node:fs";
|
|
4906
|
+
import { open } from "node:fs/promises";
|
|
4907
|
+
import { dirname as dirname8, join as join8 } from "node:path";
|
|
4908
|
+
|
|
4909
|
+
// src/history/admission.ts
|
|
4910
|
+
function decodeRunAdmission(value) {
|
|
4911
|
+
const o = closed(value, ["schema", "runId", "identity", "graph"]);
|
|
4912
|
+
const result = {
|
|
4913
|
+
schema: literal(o.schema, "cadence-run-admission/2"),
|
|
4914
|
+
runId: decodeId(o.runId),
|
|
4915
|
+
identity: decodeRunIdentity(o.identity),
|
|
4916
|
+
graph: nullable(o.graph, decodeGraph)
|
|
4917
|
+
};
|
|
4918
|
+
const { identity, graph } = result;
|
|
4919
|
+
if (identity.kind === "start" !== (graph !== null))
|
|
4812
4920
|
throw new ContractError("invalid");
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
while (argv.length) {
|
|
4817
|
-
const key = argv.shift();
|
|
4818
|
-
if (seen.has(key))
|
|
4819
|
-
throw new ContractError("invalid");
|
|
4820
|
-
seen.add(key);
|
|
4821
|
-
if (key === "--foreground" && operation === "start")
|
|
4822
|
-
foreground = true;
|
|
4823
|
-
else if (key === "--reveal" && operation === "token-link")
|
|
4824
|
-
reveal = true;
|
|
4825
|
-
else if (argv[0] && ["--server-config", "--state-home", "--agent-dir", "--config"].includes(key)) {
|
|
4826
|
-
const value = resolve6(argv.shift());
|
|
4827
|
-
if (key === "--server-config")
|
|
4828
|
-
config = value;
|
|
4829
|
-
if (key === "--state-home")
|
|
4830
|
-
stateHome = value;
|
|
4831
|
-
if (key === "--agent-dir")
|
|
4832
|
-
agentDir = value;
|
|
4833
|
-
if (key === "--config")
|
|
4834
|
-
policyFile = value;
|
|
4835
|
-
} else
|
|
4836
|
-
throw new ContractError("invalid");
|
|
4837
|
-
}
|
|
4838
|
-
const store = new GatewayStore(config, stateHome);
|
|
4839
|
-
let result;
|
|
4840
|
-
if (operation === "start") {
|
|
4841
|
-
const network = networkSnapshot(process.env), env = hostEnvironment(process.env);
|
|
4842
|
-
if (!seen.has("--server-config"))
|
|
4843
|
-
ensureServerConfig(homedir5());
|
|
4844
|
-
const server = readServerConfig(config).config;
|
|
4845
|
-
const selectedFile = store.files.read(store.path("input.json"));
|
|
4846
|
-
const selected = selectedFile ? decodeGatewayInput(json(selectedFile)) : undefined;
|
|
4847
|
-
let policy = selected?.policy ?? decodeHttpPolicy({
|
|
4848
|
-
schema: "cadence-service-http/1",
|
|
4849
|
-
host: "127.0.0.1",
|
|
4850
|
-
port: server.port,
|
|
4851
|
-
proxy: null
|
|
4852
|
-
});
|
|
4853
|
-
if (policyFile) {
|
|
4854
|
-
const file = new PrivateFiles(dirname8(policyFile)).read(policyFile);
|
|
4855
|
-
if (!file)
|
|
4856
|
-
throw new ContractError("invalid");
|
|
4857
|
-
policy = decodeHttpPolicy(json(file));
|
|
4858
|
-
}
|
|
4859
|
-
const input = decodeGatewayInput({
|
|
4860
|
-
kind: "workbench",
|
|
4861
|
-
serverConfig: config,
|
|
4862
|
-
stateHome: store.stateHome,
|
|
4863
|
-
agentDir: agentDir ?? selected?.agentDir ?? resolve6(process.env.PI_CODING_AGENT_DIR ?? join9(homedir5(), ".pi", "agent")),
|
|
4864
|
-
policy: { ...policy, port: server.port }
|
|
4865
|
-
});
|
|
4866
|
-
const status = await serviceStatus(store);
|
|
4867
|
-
if (status && typeof status === "object" && "status" in status && status.status === "alive") {
|
|
4868
|
-
if (!selected || encoded(selected) !== encoded(input))
|
|
4869
|
-
throw new ContractError("conflict");
|
|
4870
|
-
result = status;
|
|
4871
|
-
} else
|
|
4872
|
-
result = await launchHost(input, store, foreground, hostUrl, dirname8(dirname8(fileURLToPath(hostUrl))), env, network);
|
|
4873
|
-
if (!foreground && result && typeof result === "object" && "status" in result && result.status === "alive") {
|
|
4874
|
-
const reply = closed(await nativeCall(store, "token-link", true), [
|
|
4875
|
-
"link"
|
|
4876
|
-
]);
|
|
4877
|
-
process.stderr.write(`Cadence workbench: ${text(reply.link, 4096, true)}
|
|
4878
|
-
No project or default workspace was created.
|
|
4879
|
-
`);
|
|
4880
|
-
}
|
|
4881
|
-
} else if (operation === "stop")
|
|
4882
|
-
result = await stopService(store);
|
|
4883
|
-
else if (operation === "status" || operation === "diagnose")
|
|
4884
|
-
result = await serviceStatus(store, operation === "diagnose");
|
|
4885
|
-
else {
|
|
4886
|
-
if (operation === "token-link" && !reveal)
|
|
4887
|
-
throw new ContractError("forbidden");
|
|
4888
|
-
result = await nativeCall(store, operation, reveal);
|
|
4889
|
-
}
|
|
4890
|
-
if (result !== null)
|
|
4891
|
-
process.stdout.write(`${JSON.stringify(result)}
|
|
4892
|
-
`);
|
|
4893
|
-
if (result && typeof result === "object" && "status" in result && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)))
|
|
4894
|
-
process.exitCode = 1;
|
|
4921
|
+
if (graph && (graph.taskId !== identity.context.task?.id || graph.taskKind !== identity.context.task.kind || graph.taskRevision > identity.context.task.revision || graph.revision !== identity.graph?.revision || hash(canonicalJson(graph)) !== identity.graph.sha256 || canonicalJson(graph.assignments.map((a) => a.id)) !== canonicalJson(identity.assignments.map((a) => a.assignmentId)) || graph.assignments.some((a) => canonicalJson(a.model) !== canonicalJson(identity.context.model))))
|
|
4922
|
+
throw new ContractError("conflict");
|
|
4923
|
+
return result;
|
|
4895
4924
|
}
|
|
4896
4925
|
|
|
4897
|
-
// src/
|
|
4898
|
-
import {
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4926
|
+
// src/history/read-io.ts
|
|
4927
|
+
import {
|
|
4928
|
+
closeSync as closeSync4,
|
|
4929
|
+
constants as constants4,
|
|
4930
|
+
fstatSync as fstatSync3,
|
|
4931
|
+
lstatSync as lstatSync5,
|
|
4932
|
+
openSync as openSync4,
|
|
4933
|
+
readSync as readSync3
|
|
4934
|
+
} from "node:fs";
|
|
4935
|
+
import { dirname as dirname7 } from "node:path";
|
|
4936
|
+
function bad() {
|
|
4937
|
+
throw new ContractError("corrupt-selected");
|
|
4905
4938
|
}
|
|
4906
|
-
var
|
|
4907
|
-
function
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
u.serverConfig
|
|
4925
|
-
].map(string),
|
|
4926
|
-
"\t</array>",
|
|
4927
|
-
...u.workspace === null ? ["<!-- Cadence workbench gateway; no project workspace -->"] : [],
|
|
4928
|
-
"\t<key>WorkingDirectory</key>",
|
|
4929
|
-
` <string>${xml(u.workspace ?? dirname9(dirname9(u.cli)))}</string>`,
|
|
4930
|
-
"\t<key>EnvironmentVariables</key>",
|
|
4931
|
-
"\t<dict>",
|
|
4932
|
-
...u.environment.flatMap(([k, v]) => [
|
|
4933
|
-
` <key>${xml(k)}</key>`,
|
|
4934
|
-
string(v)
|
|
4935
|
-
]),
|
|
4936
|
-
"\t</dict>",
|
|
4937
|
-
"\t<key>RunAtLoad</key>",
|
|
4938
|
-
"\t<true/>",
|
|
4939
|
-
"\t<key>ProcessType</key>",
|
|
4940
|
-
"\t<string>Background</string>",
|
|
4941
|
-
"\t<key>StandardOutPath</key>",
|
|
4942
|
-
` <string>${xml(u.log)}</string>`,
|
|
4943
|
-
"\t<key>StandardErrorPath</key>",
|
|
4944
|
-
` <string>${xml(u.log)}</string>`,
|
|
4945
|
-
"</dict>",
|
|
4946
|
-
"</plist>",
|
|
4947
|
-
""
|
|
4948
|
-
].join(`
|
|
4949
|
-
`);
|
|
4939
|
+
var stamp2 = (s) => [s.dev, s.ino, s.size, s.mode, s.uid, s.nlink, s.ctimeMs, s.mtimeMs].join(":");
|
|
4940
|
+
function selected(files, path2, read, walks) {
|
|
4941
|
+
if (!files.files.directory(dirname7(path2), false, walks))
|
|
4942
|
+
bad();
|
|
4943
|
+
const before = lstatSync5(path2);
|
|
4944
|
+
if (!before.isFile() || before.nlink !== 1 || process.platform !== "win32" && (before.mode & 63 || process.getuid && before.uid !== process.getuid()))
|
|
4945
|
+
bad();
|
|
4946
|
+
const fd = openSync4(path2, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
4947
|
+
try {
|
|
4948
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
4949
|
+
bad();
|
|
4950
|
+
const result = read(fd, before.size, stamp2(before));
|
|
4951
|
+
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync5(path2)) !== stamp2(before))
|
|
4952
|
+
bad();
|
|
4953
|
+
return result;
|
|
4954
|
+
} finally {
|
|
4955
|
+
closeSync4(fd);
|
|
4956
|
+
}
|
|
4950
4957
|
}
|
|
4951
|
-
function
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4958
|
+
function assertIncarnation(files, path2, expected) {
|
|
4959
|
+
selected(files, path2, (_fd, _size, current) => {
|
|
4960
|
+
if (current !== expected)
|
|
4961
|
+
bad();
|
|
4962
|
+
});
|
|
4956
4963
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4964
|
+
function statSize(files, path2, maximum) {
|
|
4965
|
+
return selected(files, path2, (_fd, size) => {
|
|
4966
|
+
if (size > maximum)
|
|
4967
|
+
bad();
|
|
4968
|
+
return size;
|
|
4969
|
+
});
|
|
4962
4970
|
}
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
return { status: "missing-pi-sdk" };
|
|
4977
|
-
const refused2 = await settle(s, kept);
|
|
4978
|
-
if (refused2)
|
|
4979
|
-
return { ...refused2, unit: path2 };
|
|
4980
|
-
if (loaded(s))
|
|
4981
|
-
s.launchctl(["bootout", target(s)]);
|
|
4982
|
-
write(path2, buildPlist({
|
|
4983
|
-
node: s.node,
|
|
4984
|
-
cli: join10(s.root, "src", "service-cli.mjs"),
|
|
4985
|
-
workspace,
|
|
4986
|
-
serverConfig: serverConfigPath(s.home),
|
|
4987
|
-
environment: serviceEnvironment(s),
|
|
4988
|
-
log: join10(s.home, "Library", "Logs", "cadence.log")
|
|
4989
|
-
}));
|
|
4990
|
-
const result = {
|
|
4991
|
-
unit: path2,
|
|
4992
|
-
workspace,
|
|
4993
|
-
...redirectedFrom ? { redirectedFrom } : {}
|
|
4971
|
+
|
|
4972
|
+
// src/history/types.ts
|
|
4973
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4974
|
+
var RECORD_BYTES = LIMITS.controlBytes + 64 * 1024;
|
|
4975
|
+
var LEGACY_FORMAT = "cadence-history-store/2";
|
|
4976
|
+
var FORMAT = "cadence-history-store/3";
|
|
4977
|
+
function decodeFormat(value) {
|
|
4978
|
+
const o = closed(value, ["schema", "storeId", "workspaceId", "rootIdentity"]);
|
|
4979
|
+
return {
|
|
4980
|
+
schema: oneOf(o.schema, [FORMAT, LEGACY_FORMAT]),
|
|
4981
|
+
storeId: decodeId(o.storeId),
|
|
4982
|
+
workspaceId: decodeId(o.workspaceId),
|
|
4983
|
+
rootIdentity: decodeHash(o.rootIdentity)
|
|
4994
4984
|
};
|
|
4995
|
-
if (mode === "install")
|
|
4996
|
-
s.launchctl(["enable", target(s)]);
|
|
4997
|
-
else if (disabled(s))
|
|
4998
|
-
return { status: "disabled", ...result };
|
|
4999
|
-
if (workspace !== null && !regularFile(join10(agentDirectory(s), "auth.json")))
|
|
5000
|
-
return { status: "waiting-for-pi-login", ...result };
|
|
5001
|
-
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
5002
|
-
return { status: started ? "started" : "failed", ...result };
|
|
5003
|
-
}
|
|
5004
|
-
async function stopLaunchAgent(s) {
|
|
5005
|
-
const path2 = plistPath(s);
|
|
5006
|
-
const previous = readGenerated(path2, MARKER2);
|
|
5007
|
-
if (previous === null)
|
|
5008
|
-
return { status: "foreign", unit: path2 };
|
|
5009
|
-
if (previous === undefined)
|
|
5010
|
-
return { status: "absent" };
|
|
5011
|
-
const workspace = plistWorkspace(previous);
|
|
5012
|
-
const refused2 = await settle(s, workspace);
|
|
5013
|
-
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
5014
|
-
}
|
|
5015
|
-
async function removeLaunchAgent(s) {
|
|
5016
|
-
const path2 = plistPath(s);
|
|
5017
|
-
const previous = readGenerated(path2, MARKER2);
|
|
5018
|
-
if (previous === null)
|
|
5019
|
-
return { status: "foreign", unit: path2 };
|
|
5020
|
-
if (previous === undefined)
|
|
5021
|
-
return { status: "absent" };
|
|
5022
|
-
const refused2 = await settle(s, plistWorkspace(previous));
|
|
5023
|
-
if (refused2)
|
|
5024
|
-
return { ...refused2, unit: path2 };
|
|
5025
|
-
if (s.uid !== undefined && loaded(s))
|
|
5026
|
-
s.launchctl(["bootout", target(s)]);
|
|
5027
|
-
rmSync2(path2);
|
|
5028
|
-
return { status: "removed", unit: path2 };
|
|
5029
4985
|
}
|
|
5030
|
-
function
|
|
5031
|
-
const
|
|
5032
|
-
const previous = readGenerated(path2, MARKER2);
|
|
5033
|
-
if (previous === null)
|
|
5034
|
-
return { status: "foreign", unit: path2 };
|
|
5035
|
-
if (previous === undefined)
|
|
5036
|
-
return { status: "absent" };
|
|
5037
|
-
const printed = s.uid === undefined ? undefined : s.launchctl(["print", target(s)]);
|
|
4986
|
+
function decodeArtifact(value) {
|
|
4987
|
+
const o = closed(value, ["runId", "artifactId", "part", "bytes", "sha256"]);
|
|
5038
4988
|
return {
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
4989
|
+
runId: decodeId(o.runId),
|
|
4990
|
+
artifactId: decodeId(o.artifactId),
|
|
4991
|
+
part: integer(o.part),
|
|
4992
|
+
bytes: integer(o.bytes, 1, LIMITS.artifactPartBytes),
|
|
4993
|
+
sha256: decodeHash(o.sha256)
|
|
5043
4994
|
};
|
|
5044
4995
|
}
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
const
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
4996
|
+
function decodeRetainedDetail(value) {
|
|
4997
|
+
const o = closed(value, [
|
|
4998
|
+
"detailId",
|
|
4999
|
+
"origin",
|
|
5000
|
+
"owner",
|
|
5001
|
+
"descriptor",
|
|
5002
|
+
"content",
|
|
5003
|
+
"coverage"
|
|
5004
|
+
]);
|
|
5005
|
+
const origin = closed(o.origin, ["kind", "id"]);
|
|
5006
|
+
const r = {
|
|
5007
|
+
detailId: decodeId(o.detailId),
|
|
5008
|
+
origin: {
|
|
5009
|
+
kind: oneOf(origin.kind, ["worker", "owner"]),
|
|
5010
|
+
id: decodeId(origin.id)
|
|
5011
|
+
},
|
|
5012
|
+
owner: decodeRecordOwner(o.owner),
|
|
5013
|
+
descriptor: nullable(o.descriptor, decodeDetailDescriptor),
|
|
5014
|
+
content: decodeContentRef(o.content),
|
|
5015
|
+
coverage: decodeCoverage(o.coverage)
|
|
5016
|
+
};
|
|
5017
|
+
if (r.origin.kind === "worker" !== (r.descriptor !== null) || r.descriptor && r.content.field !== r.descriptor.field)
|
|
5018
|
+
invalid();
|
|
5019
|
+
return r;
|
|
5062
5020
|
}
|
|
5063
|
-
function
|
|
5064
|
-
const
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5021
|
+
function decodeSavedEvent(value, owner) {
|
|
5022
|
+
const kind = closedTag(value);
|
|
5023
|
+
if (kind === "capture") {
|
|
5024
|
+
const o2 = closed(value, ["kind", "id", "value"]), v = decodeRecordingEvent(o2.value);
|
|
5025
|
+
if (owner || v.event.kind === "stored-chunk" || v.event.kind === "stored-end")
|
|
5026
|
+
invalid();
|
|
5027
|
+
return { kind, id: decodeId(o2.id), value: v };
|
|
5028
|
+
}
|
|
5029
|
+
if (kind === "owner") {
|
|
5030
|
+
const o2 = closed(value, ["kind", "id", "value"]);
|
|
5031
|
+
if (!owner)
|
|
5032
|
+
invalid();
|
|
5033
|
+
const v = decodeOwnerRecordingInput({
|
|
5034
|
+
link: owner,
|
|
5035
|
+
observedMs: 0,
|
|
5036
|
+
events: [o2.value]
|
|
5037
|
+
}).events[0];
|
|
5038
|
+
if (v.kind === "content-chunk" || v.kind === "content-end")
|
|
5039
|
+
invalid();
|
|
5040
|
+
return { kind, id: decodeId(o2.id), value: v };
|
|
5041
|
+
}
|
|
5042
|
+
if (kind === "chunk") {
|
|
5043
|
+
const o2 = closed(value, [
|
|
5044
|
+
"kind",
|
|
5045
|
+
"detailId",
|
|
5046
|
+
"source",
|
|
5047
|
+
"artifact",
|
|
5048
|
+
"offset",
|
|
5049
|
+
"start",
|
|
5050
|
+
"length"
|
|
5051
|
+
]);
|
|
5052
|
+
const source2 = nullable(o2.source, decodeSourceRange);
|
|
5053
|
+
if (source2 === null !== (owner !== null))
|
|
5054
|
+
invalid();
|
|
5055
|
+
const artifact = decodeArtifact(o2.artifact), offset = integer(o2.offset, 0, LIMITS.detailBytes), start = integer(o2.start, 0, LIMITS.artifactPartBytes), length = integer(o2.length, 1, LIMITS.chunkBytes);
|
|
5056
|
+
if (start + length > artifact.bytes || offset + length > LIMITS.detailBytes)
|
|
5057
|
+
invalid();
|
|
5058
|
+
return {
|
|
5059
|
+
kind,
|
|
5060
|
+
detailId: decodeId(o2.detailId),
|
|
5061
|
+
source: source2,
|
|
5062
|
+
artifact,
|
|
5063
|
+
offset,
|
|
5064
|
+
start,
|
|
5065
|
+
length
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
const o = closed(value, ["kind", "detail", "source"]);
|
|
5069
|
+
literal(kind, "end");
|
|
5070
|
+
const detail = decodeRetainedDetail(o.detail), source = nullable(o.source, decodeSourceRange);
|
|
5071
|
+
if (detail.origin.kind === "owner" !== (owner !== null) || source === null !== (owner !== null))
|
|
5072
|
+
invalid();
|
|
5073
|
+
return { kind: "end", detail, source };
|
|
5077
5074
|
}
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
return text2?.match(/^shell\.CurrentDirectory = "((?:[^"]|"")*)"\r?$/m)?.[1].replaceAll('""', '"');
|
|
5075
|
+
function closedTag(value) {
|
|
5076
|
+
if (!value || typeof value !== "object" || !("kind" in value) || typeof value.kind !== "string")
|
|
5077
|
+
invalid();
|
|
5078
|
+
return value.kind;
|
|
5083
5079
|
}
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5080
|
+
function decodeAdmissionIntent(value) {
|
|
5081
|
+
const o = closed(value, [
|
|
5082
|
+
"commandId",
|
|
5083
|
+
"operationId",
|
|
5084
|
+
"runId",
|
|
5085
|
+
"identity",
|
|
5086
|
+
"createdTaskId"
|
|
5087
|
+
]);
|
|
5088
|
+
return {
|
|
5089
|
+
commandId: decodeId(o.commandId),
|
|
5090
|
+
operationId: decodeId(o.operationId),
|
|
5091
|
+
runId: nullable(o.runId, decodeId),
|
|
5092
|
+
identity: decodeCommandIdentity(o.identity),
|
|
5093
|
+
createdTaskId: nullable(o.createdTaskId, decodeId)
|
|
5094
|
+
};
|
|
5090
5095
|
}
|
|
5091
|
-
|
|
5092
|
-
const
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
return { ...refused2, unit: path2 };
|
|
5108
|
-
const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
|
|
5109
|
-
write(path2, utf16(buildLauncher({
|
|
5110
|
-
node: s.node,
|
|
5111
|
-
cli: join11(s.root, "src", "service-cli.mjs"),
|
|
5112
|
-
workspace,
|
|
5113
|
-
serverConfig: serverConfigPath(s.home),
|
|
5114
|
-
environment
|
|
5115
|
-
})));
|
|
5116
|
-
const result = {
|
|
5117
|
-
unit: path2,
|
|
5118
|
-
workspace,
|
|
5119
|
-
...redirectedFrom ? { redirectedFrom } : {}
|
|
5096
|
+
function decodeAuthIntent(value) {
|
|
5097
|
+
const o = closed(value, [
|
|
5098
|
+
"operationId",
|
|
5099
|
+
"epoch",
|
|
5100
|
+
"runId",
|
|
5101
|
+
"childRunId",
|
|
5102
|
+
"sourceSelectionId",
|
|
5103
|
+
"validityMs"
|
|
5104
|
+
]);
|
|
5105
|
+
return {
|
|
5106
|
+
operationId: decodeId(o.operationId),
|
|
5107
|
+
epoch: decodeId(o.epoch),
|
|
5108
|
+
runId: decodeId(o.runId),
|
|
5109
|
+
childRunId: decodeId(o.childRunId),
|
|
5110
|
+
sourceSelectionId: decodeId(o.sourceSelectionId),
|
|
5111
|
+
validityMs: integer(o.validityMs, 1)
|
|
5120
5112
|
};
|
|
5121
|
-
if (mode === "install") {
|
|
5122
|
-
const entry = ["/v", RUN_VALUE, "/t", "REG_SZ", "/d", runEntry(path2, s)];
|
|
5123
|
-
if (s.reg(["add", RUN_KEY, ...entry, "/f"]).status !== 0)
|
|
5124
|
-
return { status: "failed", ...result };
|
|
5125
|
-
s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
|
|
5126
|
-
}
|
|
5127
|
-
if (workspace !== null && !regularFile(join11(agentDirectory(s), "auth.json")))
|
|
5128
|
-
return { status: "waiting-for-pi-login", ...result };
|
|
5129
|
-
return { status: s.launch(path2) ? "started" : "failed", ...result };
|
|
5130
5113
|
}
|
|
5131
|
-
|
|
5132
|
-
const
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
const workspace = launcherWorkspace(previous);
|
|
5139
|
-
const refused2 = await settle(s, workspace);
|
|
5140
|
-
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
5114
|
+
function decodeAuthSettlement(value) {
|
|
5115
|
+
const o = closed(value, ["operationId", "outcome", "admissionClosed"]);
|
|
5116
|
+
return {
|
|
5117
|
+
operationId: decodeId(o.operationId),
|
|
5118
|
+
outcome: oneOf(o.outcome, ["resolved", "refused", "failed"]),
|
|
5119
|
+
admissionClosed: bool(o.admissionClosed)
|
|
5120
|
+
};
|
|
5141
5121
|
}
|
|
5142
|
-
|
|
5143
|
-
const
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
const
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5122
|
+
function decodeRecoveryBridge(v) {
|
|
5123
|
+
const o = closed(v, [
|
|
5124
|
+
"kind",
|
|
5125
|
+
"recoveryId",
|
|
5126
|
+
"epoch",
|
|
5127
|
+
"coordinationDigest",
|
|
5128
|
+
"historyDigest",
|
|
5129
|
+
"previous",
|
|
5130
|
+
"acknowledgments"
|
|
5131
|
+
]);
|
|
5132
|
+
const p = closed(o.previous, [
|
|
5133
|
+
"segment",
|
|
5134
|
+
"bytes",
|
|
5135
|
+
"sha256",
|
|
5136
|
+
"committedBytes",
|
|
5137
|
+
"committedSeq",
|
|
5138
|
+
"highSeq"
|
|
5139
|
+
]);
|
|
5140
|
+
const a = closed(o.acknowledgments, ["processes", "credentials", "storage"]);
|
|
5141
|
+
const previous = {
|
|
5142
|
+
segment: integer(p.segment),
|
|
5143
|
+
bytes: integer(p.bytes, 0, LIMITS.segmentBytes),
|
|
5144
|
+
sha256: decodeHash(p.sha256),
|
|
5145
|
+
committedBytes: integer(p.committedBytes, 0, LIMITS.segmentBytes),
|
|
5146
|
+
committedSeq: decodeSequence(p.committedSeq),
|
|
5147
|
+
highSeq: decodeSequence(p.highSeq)
|
|
5148
|
+
};
|
|
5149
|
+
if (previous.committedBytes > previous.bytes || BigInt(previous.highSeq) < BigInt(previous.committedSeq))
|
|
5150
|
+
invalid();
|
|
5151
|
+
if (a.processes !== true || a.credentials !== true || a.storage !== true)
|
|
5152
|
+
invalid();
|
|
5167
5153
|
return {
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5154
|
+
kind: literal(o.kind, "recovery"),
|
|
5155
|
+
recoveryId: decodeId(o.recoveryId),
|
|
5156
|
+
epoch: decodeId(o.epoch),
|
|
5157
|
+
coordinationDigest: decodeHash(o.coordinationDigest),
|
|
5158
|
+
historyDigest: decodeHash(o.historyDigest),
|
|
5159
|
+
previous,
|
|
5160
|
+
acknowledgments: { processes: true, credentials: true, storage: true }
|
|
5172
5161
|
};
|
|
5173
5162
|
}
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
if (coordination.recoverySnapshot().digest !== selected.digest)
|
|
5190
|
-
throw new ContractError("conflict");
|
|
5163
|
+
function validBatchSequence(rows, previous) {
|
|
5164
|
+
const bridge = rows[0]?.value;
|
|
5165
|
+
const base = bridge?.kind === "recovery" ? bridge.previous.highSeq : previous;
|
|
5166
|
+
if (bridge?.kind === "recovery" && (rows.length !== 1 || bridge.previous.committedSeq !== previous))
|
|
5167
|
+
return false;
|
|
5168
|
+
return rows.every((row, i) => row.seq === String(BigInt(base) + BigInt(i) + 1n) && (i === 0 || row.value.kind !== "recovery"));
|
|
5169
|
+
}
|
|
5170
|
+
function decodeResourcePolicy(v) {
|
|
5171
|
+
const o = closed(v, [
|
|
5172
|
+
"generation",
|
|
5173
|
+
"quotaBytes",
|
|
5174
|
+
"episodeReservationBytes",
|
|
5175
|
+
"freeSpaceFloorBytes",
|
|
5176
|
+
"episodeControlBytes"
|
|
5177
|
+
]);
|
|
5191
5178
|
return {
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5179
|
+
generation: integer(o.generation, 1),
|
|
5180
|
+
quotaBytes: integer(o.quotaBytes, LIMITS.episodeControlBytes),
|
|
5181
|
+
episodeReservationBytes: integer(o.episodeReservationBytes, LIMITS.episodeControlBytes),
|
|
5182
|
+
freeSpaceFloorBytes: literal(o.freeSpaceFloorBytes, LIMITS.freeSpaceFloorBytes),
|
|
5183
|
+
episodeControlBytes: literal(o.episodeControlBytes, LIMITS.episodeControlBytes)
|
|
5197
5184
|
};
|
|
5198
5185
|
}
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5186
|
+
function decodeJournalValue(value) {
|
|
5187
|
+
const kind = closedTag(value);
|
|
5188
|
+
switch (kind) {
|
|
5189
|
+
case "recovery":
|
|
5190
|
+
return decodeRecoveryBridge(value);
|
|
5191
|
+
case "owner-epoch": {
|
|
5192
|
+
const o = closed(value, ["kind", "epoch"]);
|
|
5193
|
+
return { kind, epoch: decodeId(o.epoch) };
|
|
5194
|
+
}
|
|
5195
|
+
case "admission-compact": {
|
|
5196
|
+
const hasPolicy = Object.hasOwn(value, "policy");
|
|
5197
|
+
const o = closed(value, [
|
|
5198
|
+
"kind",
|
|
5199
|
+
"intent",
|
|
5200
|
+
"receipt",
|
|
5201
|
+
...hasPolicy ? ["policy"] : []
|
|
5202
|
+
]);
|
|
5203
|
+
const intent = decodeAdmissionIntent(o.intent);
|
|
5204
|
+
return decodeJournalValue({
|
|
5205
|
+
kind: "admission",
|
|
5206
|
+
intent,
|
|
5207
|
+
record: { identity: intent.identity, receipt: o.receipt },
|
|
5208
|
+
...hasPolicy ? { policy: o.policy } : {}
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
case "admission": {
|
|
5212
|
+
const hasPolicy = typeof value === "object" && value !== null && Object.hasOwn(value, "policy");
|
|
5213
|
+
const o = closed(value, [
|
|
5214
|
+
"kind",
|
|
5215
|
+
"intent",
|
|
5216
|
+
"record",
|
|
5217
|
+
...hasPolicy ? ["policy"] : []
|
|
5218
|
+
]), intent = decodeAdmissionIntent(o.intent), record = decodeStoredCommand(o.record);
|
|
5219
|
+
if (record.receipt.commandId !== intent.commandId || record.receipt.operationId !== intent.operationId || record.receipt.runId !== intent.runId || canonicalJson(record.identity) !== canonicalJson(intent.identity) || (record.receipt.created?.taskId ?? null) !== intent.createdTaskId)
|
|
5220
|
+
invalid();
|
|
5221
|
+
const policy = hasPolicy ? decodeResourcePolicy(o.policy) : undefined;
|
|
5222
|
+
if (policy && intent.identity.kind === "action" && policy.generation !== intent.identity.expected.configRev)
|
|
5223
|
+
invalid();
|
|
5224
|
+
return { kind, intent, record, ...policy ? { policy } : {} };
|
|
5225
|
+
}
|
|
5226
|
+
case "auth-preparation": {
|
|
5227
|
+
const o = closed(value, ["kind", "intent"]);
|
|
5228
|
+
return { kind, intent: decodeAuthIntent(o.intent) };
|
|
5229
|
+
}
|
|
5230
|
+
case "auth-settlement": {
|
|
5231
|
+
const o = closed(value, ["kind", "result"]);
|
|
5232
|
+
return { kind, result: decodeAuthSettlement(o.result) };
|
|
5233
|
+
}
|
|
5234
|
+
case "launch": {
|
|
5235
|
+
const o = closed(value, ["kind", "link", "operationId"]);
|
|
5236
|
+
return {
|
|
5237
|
+
kind,
|
|
5238
|
+
link: decodeRecordingLink(o.link),
|
|
5239
|
+
operationId: decodeId(o.operationId)
|
|
5240
|
+
};
|
|
5241
|
+
}
|
|
5242
|
+
case "settlement": {
|
|
5243
|
+
const o = closed(value, ["kind", "link", "completion"]), link = decodeRecordingLink(o.link), completion = decodeWorkerCompletion(o.completion);
|
|
5244
|
+
if (link.childRunId !== completion.childRunId)
|
|
5245
|
+
invalid();
|
|
5246
|
+
return { kind, link, completion };
|
|
5247
|
+
}
|
|
5248
|
+
case "capture": {
|
|
5249
|
+
const o = closed(value, ["kind", "link", "source", "events"]);
|
|
5250
|
+
return {
|
|
5251
|
+
kind,
|
|
5252
|
+
link: decodeRecordingLink(o.link),
|
|
5253
|
+
source: decodeSourceRange(o.source),
|
|
5254
|
+
events: list(o.events, LIMITS.ownerQueueFrames, (v) => decodeSavedEvent(v, null))
|
|
5255
|
+
};
|
|
5256
|
+
}
|
|
5257
|
+
case "owner": {
|
|
5258
|
+
const o = closed(value, ["kind", "link", "observedMs", "events"]), link = decodeOwnerLink(o.link);
|
|
5259
|
+
return {
|
|
5260
|
+
kind,
|
|
5261
|
+
link,
|
|
5262
|
+
observedMs: integer(o.observedMs),
|
|
5263
|
+
events: list(o.events, LIMITS.ownerQueueFrames, (v) => decodeSavedEvent(v, link))
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
5266
|
+
default:
|
|
5267
|
+
return invalid();
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
function decodeJournalLine(value) {
|
|
5271
|
+
if (closedTag(value) === "record") {
|
|
5272
|
+
const o2 = closed(value, ["kind", "seq", "id", "value"]);
|
|
5273
|
+
return {
|
|
5274
|
+
kind: "record",
|
|
5275
|
+
seq: decodeSequence(o2.seq),
|
|
5276
|
+
id: decodeId(o2.id),
|
|
5277
|
+
value: decodeJournalValue(o2.value)
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
const indexed = typeof value === "object" && value !== null && Object.hasOwn(value, "navigationRoot");
|
|
5281
|
+
const o = closed(value, [
|
|
5282
|
+
...indexed ? ["navigationRoot"] : [],
|
|
5283
|
+
"kind",
|
|
5284
|
+
"previous",
|
|
5285
|
+
"first",
|
|
5286
|
+
"last",
|
|
5287
|
+
"count",
|
|
5288
|
+
"sha256",
|
|
5289
|
+
"artifacts"
|
|
5290
|
+
]);
|
|
5291
|
+
literal(o.kind, "commit");
|
|
5219
5292
|
return {
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5293
|
+
kind: "commit",
|
|
5294
|
+
...indexed ? { navigationRoot: nullable(o.navigationRoot, decodeHash) } : {},
|
|
5295
|
+
previous: decodeSequence(o.previous),
|
|
5296
|
+
first: decodeSequence(o.first),
|
|
5297
|
+
last: decodeSequence(o.last),
|
|
5298
|
+
count: integer(o.count, 1, LIMITS.segmentRecords),
|
|
5299
|
+
sha256: decodeHash(o.sha256),
|
|
5300
|
+
artifacts: list(o.artifacts, LIMITS.ownerQueueFrames, decodeArtifact)
|
|
5225
5301
|
};
|
|
5226
5302
|
}
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
const result = {
|
|
5247
|
-
schema: literal(o.schema, "cadence-run-admission/2"),
|
|
5248
|
-
runId: decodeId(o.runId),
|
|
5249
|
-
identity: decodeRunIdentity(o.identity),
|
|
5250
|
-
graph: nullable(o.graph, decodeGraph)
|
|
5303
|
+
var COMPLETE = {
|
|
5304
|
+
observed: "complete",
|
|
5305
|
+
source: "complete",
|
|
5306
|
+
redacted: false,
|
|
5307
|
+
omissions: []
|
|
5308
|
+
};
|
|
5309
|
+
function captureCoverage(event) {
|
|
5310
|
+
if (event.kind === "capture-status")
|
|
5311
|
+
return event.coverage;
|
|
5312
|
+
if (event.kind === "omitted-block")
|
|
5313
|
+
return { ...COMPLETE, source: "limited", omissions: [event.block.reason] };
|
|
5314
|
+
return null;
|
|
5315
|
+
}
|
|
5316
|
+
function combineCoverage(a, b) {
|
|
5317
|
+
return {
|
|
5318
|
+
observed: a.observed === "complete" && b.observed === "complete" ? "complete" : "incomplete",
|
|
5319
|
+
source: a.source === "unavailable" || b.source === "unavailable" ? "unavailable" : a.source === "limited" || b.source === "limited" ? "limited" : "complete",
|
|
5320
|
+
redacted: a.redacted || b.redacted,
|
|
5321
|
+
omissions: [...new Set([...a.omissions, ...b.omissions])]
|
|
5251
5322
|
};
|
|
5252
|
-
const { identity, graph } = result;
|
|
5253
|
-
if (identity.kind === "start" !== (graph !== null))
|
|
5254
|
-
throw new ContractError("invalid");
|
|
5255
|
-
if (graph && (graph.taskId !== identity.context.task?.id || graph.taskKind !== identity.context.task.kind || graph.taskRevision > identity.context.task.revision || graph.revision !== identity.graph?.revision || hash(canonicalJson(graph)) !== identity.graph.sha256 || canonicalJson(graph.assignments.map((a) => a.id)) !== canonicalJson(identity.assignments.map((a) => a.assignmentId)) || graph.assignments.some((a) => canonicalJson(a.model) !== canonicalJson(identity.context.model))))
|
|
5256
|
-
throw new ContractError("conflict");
|
|
5257
|
-
return result;
|
|
5258
5323
|
}
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
import {
|
|
5262
|
-
closeSync as closeSync4,
|
|
5263
|
-
constants as constants4,
|
|
5264
|
-
fstatSync as fstatSync3,
|
|
5265
|
-
lstatSync as lstatSync5,
|
|
5266
|
-
openSync as openSync4,
|
|
5267
|
-
readSync as readSync3
|
|
5268
|
-
} from "node:fs";
|
|
5269
|
-
import { dirname as dirname11 } from "node:path";
|
|
5270
|
-
function bad() {
|
|
5271
|
-
throw new ContractError("corrupt-selected");
|
|
5324
|
+
function rowArtifacts(row) {
|
|
5325
|
+
return row.value.kind === "capture" || row.value.kind === "owner" ? row.value.events.flatMap((e) => e.kind === "chunk" ? [e.artifact] : []) : [];
|
|
5272
5326
|
}
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
const fd = openSync4(path2, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
5281
|
-
try {
|
|
5282
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before))
|
|
5283
|
-
bad();
|
|
5284
|
-
const result = read(fd, before.size, stamp2(before));
|
|
5285
|
-
if (stamp2(fstatSync3(fd)) !== stamp2(before) || stamp2(lstatSync5(path2)) !== stamp2(before))
|
|
5286
|
-
bad();
|
|
5287
|
-
return result;
|
|
5288
|
-
} finally {
|
|
5289
|
-
closeSync4(fd);
|
|
5327
|
+
function batchArtifacts(rows) {
|
|
5328
|
+
const unique = new Map;
|
|
5329
|
+
for (const ref of rows.flatMap(rowArtifacts)) {
|
|
5330
|
+
const key = `${ref.runId}/${ref.artifactId}/${ref.part}`, previous = unique.get(key);
|
|
5331
|
+
if (previous && canonicalJson(previous) !== canonicalJson(ref))
|
|
5332
|
+
invalid();
|
|
5333
|
+
unique.set(key, ref);
|
|
5290
5334
|
}
|
|
5335
|
+
return [...unique.values()];
|
|
5291
5336
|
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5337
|
+
var decodedLines = new Map;
|
|
5338
|
+
var decodedBytes = 0;
|
|
5339
|
+
var lineKey = (text2) => createHash4("sha256").update(text2).digest("hex");
|
|
5340
|
+
function remember(text2, value) {
|
|
5341
|
+
const key = lineKey(text2);
|
|
5342
|
+
if (decodedLines.has(key))
|
|
5343
|
+
return;
|
|
5344
|
+
const bytes = Buffer.byteLength(text2);
|
|
5345
|
+
while (decodedLines.size && (decodedLines.size >= 2 * LIMITS.segmentRecords || decodedBytes + bytes > 2 * LIMITS.segmentBytes)) {
|
|
5346
|
+
const first = decodedLines.keys().next().value;
|
|
5347
|
+
if (!first)
|
|
5348
|
+
break;
|
|
5349
|
+
decodedBytes -= decodedLines.get(first)?.bytes ?? 0;
|
|
5350
|
+
decodedLines.delete(first);
|
|
5351
|
+
}
|
|
5352
|
+
if (bytes <= 2 * LIMITS.segmentBytes) {
|
|
5353
|
+
decodedLines.set(key, { bytes, value });
|
|
5354
|
+
decodedBytes += bytes;
|
|
5355
|
+
}
|
|
5304
5356
|
}
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
var LEGACY_FORMAT = "cadence-history-store/2";
|
|
5310
|
-
var FORMAT = "cadence-history-store/3";
|
|
5311
|
-
function decodeFormat(value) {
|
|
5312
|
-
const o = closed(value, ["schema", "storeId", "workspaceId", "rootIdentity"]);
|
|
5357
|
+
function compactAdmission(value) {
|
|
5358
|
+
if (value.kind !== "record" || value.value.kind !== "admission")
|
|
5359
|
+
return value;
|
|
5360
|
+
const { intent, record, policy } = value.value;
|
|
5313
5361
|
return {
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5362
|
+
...value,
|
|
5363
|
+
value: {
|
|
5364
|
+
kind: "admission-compact",
|
|
5365
|
+
intent,
|
|
5366
|
+
receipt: record.receipt,
|
|
5367
|
+
...policy ? { policy } : {}
|
|
5368
|
+
}
|
|
5318
5369
|
};
|
|
5319
5370
|
}
|
|
5320
|
-
function
|
|
5321
|
-
const
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
part: integer(o.part),
|
|
5326
|
-
bytes: integer(o.bytes, 1, LIMITS.artifactPartBytes),
|
|
5327
|
-
sha256: decodeHash(o.sha256)
|
|
5328
|
-
};
|
|
5371
|
+
function encodeJournalText(input) {
|
|
5372
|
+
const value = decodeJournalLine(input), text2 = canonicalJson(compactAdmission(value), RECORD_BYTES);
|
|
5373
|
+
remember(text2, value);
|
|
5374
|
+
return `${text2}
|
|
5375
|
+
`;
|
|
5329
5376
|
}
|
|
5330
|
-
function
|
|
5331
|
-
const
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
content: decodeContentRef(o.content),
|
|
5349
|
-
coverage: decodeCoverage(o.coverage)
|
|
5350
|
-
};
|
|
5351
|
-
if (r.origin.kind === "worker" !== (r.descriptor !== null) || r.descriptor && r.content.field !== r.descriptor.field)
|
|
5377
|
+
function decodeJournalText(line) {
|
|
5378
|
+
const text2 = line.endsWith(`
|
|
5379
|
+
`) ? line.slice(0, -1) : line;
|
|
5380
|
+
const epoch = /^\{"id":"([0-9a-f-]{36})","kind":"record","seq":"([0-9]+)","value":\{"epoch":"([0-9a-f-]{36})","kind":"owner-epoch"\}\}$/.exec(text2);
|
|
5381
|
+
if (epoch)
|
|
5382
|
+
return {
|
|
5383
|
+
kind: "record",
|
|
5384
|
+
id: decodeId(epoch[1]),
|
|
5385
|
+
seq: decodeSequence(epoch[2]),
|
|
5386
|
+
value: { kind: "owner-epoch", epoch: decodeId(epoch[3]) }
|
|
5387
|
+
};
|
|
5388
|
+
const key = lineKey(text2), cached = decodedLines.get(key);
|
|
5389
|
+
if (cached)
|
|
5390
|
+
return structuredClone(cached.value);
|
|
5391
|
+
const raw = JSON.parse(text2);
|
|
5392
|
+
const value = decodeJournalLine(raw);
|
|
5393
|
+
const compact = raw?.kind === "record" && raw.value?.kind === "admission-compact";
|
|
5394
|
+
if (canonicalJson(compact ? compactAdmission(value) : value, RECORD_BYTES) !== text2)
|
|
5352
5395
|
invalid();
|
|
5353
|
-
|
|
5396
|
+
remember(text2, value);
|
|
5397
|
+
return structuredClone(value);
|
|
5354
5398
|
}
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5399
|
+
|
|
5400
|
+
// src/history/files.ts
|
|
5401
|
+
class HistoryReadBudget {
|
|
5402
|
+
clock;
|
|
5403
|
+
started = performance.now();
|
|
5404
|
+
bytes = 0;
|
|
5405
|
+
walks = new DirectoryWalks;
|
|
5406
|
+
constructor(clock = true) {
|
|
5407
|
+
this.clock = clock;
|
|
5362
5408
|
}
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
if (!owner)
|
|
5366
|
-
invalid();
|
|
5367
|
-
const v = decodeOwnerRecordingInput({
|
|
5368
|
-
link: owner,
|
|
5369
|
-
observedMs: 0,
|
|
5370
|
-
events: [o2.value]
|
|
5371
|
-
}).events[0];
|
|
5372
|
-
if (v.kind === "content-chunk" || v.kind === "content-end")
|
|
5373
|
-
invalid();
|
|
5374
|
-
return { kind, id: decodeId(o2.id), value: v };
|
|
5409
|
+
static authority() {
|
|
5410
|
+
return new HistoryReadBudget(false);
|
|
5375
5411
|
}
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
"
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
"start",
|
|
5384
|
-
"length"
|
|
5385
|
-
]);
|
|
5386
|
-
const source2 = nullable(o2.source, decodeSourceRange);
|
|
5387
|
-
if (source2 === null !== (owner !== null))
|
|
5388
|
-
invalid();
|
|
5389
|
-
const artifact = decodeArtifact(o2.artifact), offset = integer(o2.offset, 0, LIMITS.detailBytes), start = integer(o2.start, 0, LIMITS.artifactPartBytes), length = integer(o2.length, 1, LIMITS.chunkBytes);
|
|
5390
|
-
if (start + length > artifact.bytes || offset + length > LIMITS.detailBytes)
|
|
5391
|
-
invalid();
|
|
5392
|
-
return {
|
|
5393
|
-
kind,
|
|
5394
|
-
detailId: decodeId(o2.detailId),
|
|
5395
|
-
source: source2,
|
|
5396
|
-
artifact,
|
|
5397
|
-
offset,
|
|
5398
|
-
start,
|
|
5399
|
-
length
|
|
5400
|
-
};
|
|
5412
|
+
reserve(bytes) {
|
|
5413
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
|
|
5414
|
+
throw new ContractError("receipt-inconclusive");
|
|
5415
|
+
this.bytes += bytes;
|
|
5416
|
+
}
|
|
5417
|
+
get elapsed() {
|
|
5418
|
+
return this.clock && performance.now() - this.started >= LIMITS.searchMs;
|
|
5401
5419
|
}
|
|
5402
|
-
const o = closed(value, ["kind", "detail", "source"]);
|
|
5403
|
-
literal(kind, "end");
|
|
5404
|
-
const detail = decodeRetainedDetail(o.detail), source = nullable(o.source, decodeSourceRange);
|
|
5405
|
-
if (detail.origin.kind === "owner" !== (owner !== null) || source === null !== (owner !== null))
|
|
5406
|
-
invalid();
|
|
5407
|
-
return { kind: "end", detail, source };
|
|
5408
5420
|
}
|
|
5409
|
-
function
|
|
5410
|
-
|
|
5411
|
-
invalid();
|
|
5412
|
-
return value.kind;
|
|
5413
|
-
}
|
|
5414
|
-
function decodeAdmissionIntent(value) {
|
|
5415
|
-
const o = closed(value, [
|
|
5416
|
-
"commandId",
|
|
5417
|
-
"operationId",
|
|
5418
|
-
"runId",
|
|
5419
|
-
"identity",
|
|
5420
|
-
"createdTaskId"
|
|
5421
|
-
]);
|
|
5422
|
-
return {
|
|
5423
|
-
commandId: decodeId(o.commandId),
|
|
5424
|
-
operationId: decodeId(o.operationId),
|
|
5425
|
-
runId: nullable(o.runId, decodeId),
|
|
5426
|
-
identity: decodeCommandIdentity(o.identity),
|
|
5427
|
-
createdTaskId: nullable(o.createdTaskId, decodeId)
|
|
5428
|
-
};
|
|
5429
|
-
}
|
|
5430
|
-
function decodeAuthIntent(value) {
|
|
5431
|
-
const o = closed(value, [
|
|
5432
|
-
"operationId",
|
|
5433
|
-
"epoch",
|
|
5434
|
-
"runId",
|
|
5435
|
-
"childRunId",
|
|
5436
|
-
"sourceSelectionId",
|
|
5437
|
-
"validityMs"
|
|
5438
|
-
]);
|
|
5439
|
-
return {
|
|
5440
|
-
operationId: decodeId(o.operationId),
|
|
5441
|
-
epoch: decodeId(o.epoch),
|
|
5442
|
-
runId: decodeId(o.runId),
|
|
5443
|
-
childRunId: decodeId(o.childRunId),
|
|
5444
|
-
sourceSelectionId: decodeId(o.sourceSelectionId),
|
|
5445
|
-
validityMs: integer(o.validityMs, 1)
|
|
5446
|
-
};
|
|
5447
|
-
}
|
|
5448
|
-
function decodeAuthSettlement(value) {
|
|
5449
|
-
const o = closed(value, ["operationId", "outcome", "admissionClosed"]);
|
|
5450
|
-
return {
|
|
5451
|
-
operationId: decodeId(o.operationId),
|
|
5452
|
-
outcome: oneOf(o.outcome, ["resolved", "refused", "failed"]),
|
|
5453
|
-
admissionClosed: bool(o.admissionClosed)
|
|
5454
|
-
};
|
|
5455
|
-
}
|
|
5456
|
-
function decodeRecoveryBridge(v) {
|
|
5457
|
-
const o = closed(v, [
|
|
5458
|
-
"kind",
|
|
5459
|
-
"recoveryId",
|
|
5460
|
-
"epoch",
|
|
5461
|
-
"coordinationDigest",
|
|
5462
|
-
"historyDigest",
|
|
5463
|
-
"previous",
|
|
5464
|
-
"acknowledgments"
|
|
5465
|
-
]);
|
|
5466
|
-
const p = closed(o.previous, [
|
|
5467
|
-
"segment",
|
|
5468
|
-
"bytes",
|
|
5469
|
-
"sha256",
|
|
5470
|
-
"committedBytes",
|
|
5471
|
-
"committedSeq",
|
|
5472
|
-
"highSeq"
|
|
5473
|
-
]);
|
|
5474
|
-
const a = closed(o.acknowledgments, ["processes", "credentials", "storage"]);
|
|
5475
|
-
const previous = {
|
|
5476
|
-
segment: integer(p.segment),
|
|
5477
|
-
bytes: integer(p.bytes, 0, LIMITS.segmentBytes),
|
|
5478
|
-
sha256: decodeHash(p.sha256),
|
|
5479
|
-
committedBytes: integer(p.committedBytes, 0, LIMITS.segmentBytes),
|
|
5480
|
-
committedSeq: decodeSequence(p.committedSeq),
|
|
5481
|
-
highSeq: decodeSequence(p.highSeq)
|
|
5482
|
-
};
|
|
5483
|
-
if (previous.committedBytes > previous.bytes || BigInt(previous.highSeq) < BigInt(previous.committedSeq))
|
|
5484
|
-
invalid();
|
|
5485
|
-
if (a.processes !== true || a.credentials !== true || a.storage !== true)
|
|
5486
|
-
invalid();
|
|
5487
|
-
return {
|
|
5488
|
-
kind: literal(o.kind, "recovery"),
|
|
5489
|
-
recoveryId: decodeId(o.recoveryId),
|
|
5490
|
-
epoch: decodeId(o.epoch),
|
|
5491
|
-
coordinationDigest: decodeHash(o.coordinationDigest),
|
|
5492
|
-
historyDigest: decodeHash(o.historyDigest),
|
|
5493
|
-
previous,
|
|
5494
|
-
acknowledgments: { processes: true, credentials: true, storage: true }
|
|
5495
|
-
};
|
|
5496
|
-
}
|
|
5497
|
-
function validBatchSequence(rows, previous) {
|
|
5498
|
-
const bridge = rows[0]?.value;
|
|
5499
|
-
const base = bridge?.kind === "recovery" ? bridge.previous.highSeq : previous;
|
|
5500
|
-
if (bridge?.kind === "recovery" && (rows.length !== 1 || bridge.previous.committedSeq !== previous))
|
|
5501
|
-
return false;
|
|
5502
|
-
return rows.every((row, i) => row.seq === String(BigInt(base) + BigInt(i) + 1n) && (i === 0 || row.value.kind !== "recovery"));
|
|
5503
|
-
}
|
|
5504
|
-
function decodeResourcePolicy(v) {
|
|
5505
|
-
const o = closed(v, [
|
|
5506
|
-
"generation",
|
|
5507
|
-
"quotaBytes",
|
|
5508
|
-
"episodeReservationBytes",
|
|
5509
|
-
"freeSpaceFloorBytes",
|
|
5510
|
-
"episodeControlBytes"
|
|
5511
|
-
]);
|
|
5512
|
-
return {
|
|
5513
|
-
generation: integer(o.generation, 1),
|
|
5514
|
-
quotaBytes: integer(o.quotaBytes, LIMITS.episodeControlBytes),
|
|
5515
|
-
episodeReservationBytes: integer(o.episodeReservationBytes, LIMITS.episodeControlBytes),
|
|
5516
|
-
freeSpaceFloorBytes: literal(o.freeSpaceFloorBytes, LIMITS.freeSpaceFloorBytes),
|
|
5517
|
-
episodeControlBytes: literal(o.episodeControlBytes, LIMITS.episodeControlBytes)
|
|
5518
|
-
};
|
|
5421
|
+
function corrupt() {
|
|
5422
|
+
throw new ContractError("corrupt-selected");
|
|
5519
5423
|
}
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
if (policy && intent.identity.kind === "action" && policy.generation !== intent.identity.expected.configRev)
|
|
5557
|
-
invalid();
|
|
5558
|
-
return { kind, intent, record, ...policy ? { policy } : {} };
|
|
5559
|
-
}
|
|
5560
|
-
case "auth-preparation": {
|
|
5561
|
-
const o = closed(value, ["kind", "intent"]);
|
|
5562
|
-
return { kind, intent: decodeAuthIntent(o.intent) };
|
|
5563
|
-
}
|
|
5564
|
-
case "auth-settlement": {
|
|
5565
|
-
const o = closed(value, ["kind", "result"]);
|
|
5566
|
-
return { kind, result: decodeAuthSettlement(o.result) };
|
|
5567
|
-
}
|
|
5568
|
-
case "launch": {
|
|
5569
|
-
const o = closed(value, ["kind", "link", "operationId"]);
|
|
5570
|
-
return {
|
|
5571
|
-
kind,
|
|
5572
|
-
link: decodeRecordingLink(o.link),
|
|
5573
|
-
operationId: decodeId(o.operationId)
|
|
5574
|
-
};
|
|
5575
|
-
}
|
|
5576
|
-
case "settlement": {
|
|
5577
|
-
const o = closed(value, ["kind", "link", "completion"]), link = decodeRecordingLink(o.link), completion = decodeWorkerCompletion(o.completion);
|
|
5578
|
-
if (link.childRunId !== completion.childRunId)
|
|
5579
|
-
invalid();
|
|
5580
|
-
return { kind, link, completion };
|
|
5424
|
+
var publicationFences = new Map;
|
|
5425
|
+
var activity = new Map;
|
|
5426
|
+
|
|
5427
|
+
class HistoryFiles {
|
|
5428
|
+
coordination;
|
|
5429
|
+
format;
|
|
5430
|
+
formatSnapshot;
|
|
5431
|
+
directory;
|
|
5432
|
+
files;
|
|
5433
|
+
proofSnapshot;
|
|
5434
|
+
constructor(coordination, format, formatSnapshot) {
|
|
5435
|
+
this.coordination = coordination;
|
|
5436
|
+
this.format = format;
|
|
5437
|
+
this.formatSnapshot = formatSnapshot;
|
|
5438
|
+
this.directory = join8(coordination.directory, "history-v2");
|
|
5439
|
+
this.files = coordination.files;
|
|
5440
|
+
}
|
|
5441
|
+
static open(coordination) {
|
|
5442
|
+
const dir = join8(coordination.directory, "history-v2");
|
|
5443
|
+
if (!coordination.files.directory(dir))
|
|
5444
|
+
return;
|
|
5445
|
+
const file = coordination.files.read(join8(dir, "format.json"));
|
|
5446
|
+
if (!file)
|
|
5447
|
+
corrupt();
|
|
5448
|
+
const format = decodeFormat(json(file));
|
|
5449
|
+
if (format.rootIdentity !== hash(coordination.store.root))
|
|
5450
|
+
corrupt();
|
|
5451
|
+
return new HistoryFiles(coordination, format, file);
|
|
5452
|
+
}
|
|
5453
|
+
static initialize(lease) {
|
|
5454
|
+
lease.assert();
|
|
5455
|
+
const c = lease.coordination;
|
|
5456
|
+
const existing = HistoryFiles.open(c);
|
|
5457
|
+
if (existing) {
|
|
5458
|
+
existing.assertWritable();
|
|
5459
|
+
return existing;
|
|
5581
5460
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5461
|
+
const dir = join8(c.directory, "history-v2");
|
|
5462
|
+
const format = {
|
|
5463
|
+
schema: FORMAT,
|
|
5464
|
+
storeId: randomUUID6(),
|
|
5465
|
+
workspaceId: randomUUID6(),
|
|
5466
|
+
rootIdentity: hash(c.store.root)
|
|
5467
|
+
};
|
|
5468
|
+
c.files.directory(dir, true);
|
|
5469
|
+
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
5470
|
+
lease.assert();
|
|
5471
|
+
c.files.directory(join8(dir, name), true);
|
|
5590
5472
|
}
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5473
|
+
lease.assert();
|
|
5474
|
+
const keyPath = join8(dir, "read-proof-key.json");
|
|
5475
|
+
c.files.create(keyPath, encoded({
|
|
5476
|
+
schema: "cadence-history-read-key/1",
|
|
5477
|
+
storeId: format.storeId,
|
|
5478
|
+
key: randomBytes5(32).toString("base64url")
|
|
5479
|
+
}));
|
|
5480
|
+
lease.assert();
|
|
5481
|
+
const snapshot = c.files.create(join8(dir, "format.json"), encoded(format));
|
|
5482
|
+
const result = new HistoryFiles(c, format, snapshot);
|
|
5483
|
+
result.syncParents(join8(dir, "format.json"));
|
|
5484
|
+
result.assertWritable();
|
|
5485
|
+
return result;
|
|
5486
|
+
}
|
|
5487
|
+
activity() {
|
|
5488
|
+
const key = `${this.directory}/${this.format.storeId}`;
|
|
5489
|
+
let state = activity.get(key);
|
|
5490
|
+
if (!state) {
|
|
5491
|
+
state = { pending: 0, generation: 0 };
|
|
5492
|
+
activity.set(key, state);
|
|
5599
5493
|
}
|
|
5600
|
-
|
|
5601
|
-
return invalid();
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
|
-
function decodeJournalLine(value) {
|
|
5605
|
-
if (closedTag(value) === "record") {
|
|
5606
|
-
const o2 = closed(value, ["kind", "seq", "id", "value"]);
|
|
5607
|
-
return {
|
|
5608
|
-
kind: "record",
|
|
5609
|
-
seq: decodeSequence(o2.seq),
|
|
5610
|
-
id: decodeId(o2.id),
|
|
5611
|
-
value: decodeJournalValue(o2.value)
|
|
5612
|
-
};
|
|
5613
|
-
}
|
|
5614
|
-
const indexed = typeof value === "object" && value !== null && Object.hasOwn(value, "navigationRoot");
|
|
5615
|
-
const o = closed(value, [
|
|
5616
|
-
...indexed ? ["navigationRoot"] : [],
|
|
5617
|
-
"kind",
|
|
5618
|
-
"previous",
|
|
5619
|
-
"first",
|
|
5620
|
-
"last",
|
|
5621
|
-
"count",
|
|
5622
|
-
"sha256",
|
|
5623
|
-
"artifacts"
|
|
5624
|
-
]);
|
|
5625
|
-
literal(o.kind, "commit");
|
|
5626
|
-
return {
|
|
5627
|
-
kind: "commit",
|
|
5628
|
-
...indexed ? { navigationRoot: nullable(o.navigationRoot, decodeHash) } : {},
|
|
5629
|
-
previous: decodeSequence(o.previous),
|
|
5630
|
-
first: decodeSequence(o.first),
|
|
5631
|
-
last: decodeSequence(o.last),
|
|
5632
|
-
count: integer(o.count, 1, LIMITS.segmentRecords),
|
|
5633
|
-
sha256: decodeHash(o.sha256),
|
|
5634
|
-
artifacts: list(o.artifacts, LIMITS.ownerQueueFrames, decodeArtifact)
|
|
5635
|
-
};
|
|
5636
|
-
}
|
|
5637
|
-
var COMPLETE = {
|
|
5638
|
-
observed: "complete",
|
|
5639
|
-
source: "complete",
|
|
5640
|
-
redacted: false,
|
|
5641
|
-
omissions: []
|
|
5642
|
-
};
|
|
5643
|
-
function captureCoverage(event) {
|
|
5644
|
-
if (event.kind === "capture-status")
|
|
5645
|
-
return event.coverage;
|
|
5646
|
-
if (event.kind === "omitted-block")
|
|
5647
|
-
return { ...COMPLETE, source: "limited", omissions: [event.block.reason] };
|
|
5648
|
-
return null;
|
|
5649
|
-
}
|
|
5650
|
-
function combineCoverage(a, b) {
|
|
5651
|
-
return {
|
|
5652
|
-
observed: a.observed === "complete" && b.observed === "complete" ? "complete" : "incomplete",
|
|
5653
|
-
source: a.source === "unavailable" || b.source === "unavailable" ? "unavailable" : a.source === "limited" || b.source === "limited" ? "limited" : "complete",
|
|
5654
|
-
redacted: a.redacted || b.redacted,
|
|
5655
|
-
omissions: [...new Set([...a.omissions, ...b.omissions])]
|
|
5656
|
-
};
|
|
5657
|
-
}
|
|
5658
|
-
function rowArtifacts(row) {
|
|
5659
|
-
return row.value.kind === "capture" || row.value.kind === "owner" ? row.value.events.flatMap((e) => e.kind === "chunk" ? [e.artifact] : []) : [];
|
|
5660
|
-
}
|
|
5661
|
-
function batchArtifacts(rows) {
|
|
5662
|
-
const unique = new Map;
|
|
5663
|
-
for (const ref of rows.flatMap(rowArtifacts)) {
|
|
5664
|
-
const key = `${ref.runId}/${ref.artifactId}/${ref.part}`, previous = unique.get(key);
|
|
5665
|
-
if (previous && canonicalJson(previous) !== canonicalJson(ref))
|
|
5666
|
-
invalid();
|
|
5667
|
-
unique.set(key, ref);
|
|
5668
|
-
}
|
|
5669
|
-
return [...unique.values()];
|
|
5670
|
-
}
|
|
5671
|
-
var decodedLines = new Map;
|
|
5672
|
-
var decodedBytes = 0;
|
|
5673
|
-
var lineKey = (text2) => createHash4("sha256").update(text2).digest("hex");
|
|
5674
|
-
function remember(text2, value) {
|
|
5675
|
-
const key = lineKey(text2);
|
|
5676
|
-
if (decodedLines.has(key))
|
|
5677
|
-
return;
|
|
5678
|
-
const bytes = Buffer.byteLength(text2);
|
|
5679
|
-
while (decodedLines.size && (decodedLines.size >= 2 * LIMITS.segmentRecords || decodedBytes + bytes > 2 * LIMITS.segmentBytes)) {
|
|
5680
|
-
const first = decodedLines.keys().next().value;
|
|
5681
|
-
if (!first)
|
|
5682
|
-
break;
|
|
5683
|
-
decodedBytes -= decodedLines.get(first)?.bytes ?? 0;
|
|
5684
|
-
decodedLines.delete(first);
|
|
5685
|
-
}
|
|
5686
|
-
if (bytes <= 2 * LIMITS.segmentBytes) {
|
|
5687
|
-
decodedLines.set(key, { bytes, value });
|
|
5688
|
-
decodedBytes += bytes;
|
|
5689
|
-
}
|
|
5690
|
-
}
|
|
5691
|
-
function compactAdmission(value) {
|
|
5692
|
-
if (value.kind !== "record" || value.value.kind !== "admission")
|
|
5693
|
-
return value;
|
|
5694
|
-
const { intent, record, policy } = value.value;
|
|
5695
|
-
return {
|
|
5696
|
-
...value,
|
|
5697
|
-
value: {
|
|
5698
|
-
kind: "admission-compact",
|
|
5699
|
-
intent,
|
|
5700
|
-
receipt: record.receipt,
|
|
5701
|
-
...policy ? { policy } : {}
|
|
5702
|
-
}
|
|
5703
|
-
};
|
|
5704
|
-
}
|
|
5705
|
-
function encodeJournalText(input) {
|
|
5706
|
-
const value = decodeJournalLine(input), text2 = canonicalJson(compactAdmission(value), RECORD_BYTES);
|
|
5707
|
-
remember(text2, value);
|
|
5708
|
-
return `${text2}
|
|
5709
|
-
`;
|
|
5710
|
-
}
|
|
5711
|
-
function decodeJournalText(line) {
|
|
5712
|
-
const text2 = line.endsWith(`
|
|
5713
|
-
`) ? line.slice(0, -1) : line;
|
|
5714
|
-
const epoch = /^\{"id":"([0-9a-f-]{36})","kind":"record","seq":"([0-9]+)","value":\{"epoch":"([0-9a-f-]{36})","kind":"owner-epoch"\}\}$/.exec(text2);
|
|
5715
|
-
if (epoch)
|
|
5716
|
-
return {
|
|
5717
|
-
kind: "record",
|
|
5718
|
-
id: decodeId(epoch[1]),
|
|
5719
|
-
seq: decodeSequence(epoch[2]),
|
|
5720
|
-
value: { kind: "owner-epoch", epoch: decodeId(epoch[3]) }
|
|
5721
|
-
};
|
|
5722
|
-
const key = lineKey(text2), cached = decodedLines.get(key);
|
|
5723
|
-
if (cached)
|
|
5724
|
-
return structuredClone(cached.value);
|
|
5725
|
-
const raw = JSON.parse(text2);
|
|
5726
|
-
const value = decodeJournalLine(raw);
|
|
5727
|
-
const compact = raw?.kind === "record" && raw.value?.kind === "admission-compact";
|
|
5728
|
-
if (canonicalJson(compact ? compactAdmission(value) : value, RECORD_BYTES) !== text2)
|
|
5729
|
-
invalid();
|
|
5730
|
-
remember(text2, value);
|
|
5731
|
-
return structuredClone(value);
|
|
5732
|
-
}
|
|
5733
|
-
|
|
5734
|
-
// src/history/files.ts
|
|
5735
|
-
class HistoryReadBudget {
|
|
5736
|
-
clock;
|
|
5737
|
-
started = performance.now();
|
|
5738
|
-
bytes = 0;
|
|
5739
|
-
walks = new DirectoryWalks;
|
|
5740
|
-
constructor(clock = true) {
|
|
5741
|
-
this.clock = clock;
|
|
5742
|
-
}
|
|
5743
|
-
static authority() {
|
|
5744
|
-
return new HistoryReadBudget(false);
|
|
5745
|
-
}
|
|
5746
|
-
reserve(bytes) {
|
|
5747
|
-
if (!Number.isSafeInteger(bytes) || bytes < 0 || this.bytes + bytes > LIMITS.searchBytes || this.elapsed)
|
|
5748
|
-
throw new ContractError("receipt-inconclusive");
|
|
5749
|
-
this.bytes += bytes;
|
|
5750
|
-
}
|
|
5751
|
-
get elapsed() {
|
|
5752
|
-
return this.clock && performance.now() - this.started >= LIMITS.searchMs;
|
|
5753
|
-
}
|
|
5754
|
-
}
|
|
5755
|
-
function corrupt() {
|
|
5756
|
-
throw new ContractError("corrupt-selected");
|
|
5757
|
-
}
|
|
5758
|
-
var publicationFences = new Map;
|
|
5759
|
-
var activity = new Map;
|
|
5760
|
-
|
|
5761
|
-
class HistoryFiles {
|
|
5762
|
-
coordination;
|
|
5763
|
-
format;
|
|
5764
|
-
formatSnapshot;
|
|
5765
|
-
directory;
|
|
5766
|
-
files;
|
|
5767
|
-
proofSnapshot;
|
|
5768
|
-
constructor(coordination, format, formatSnapshot) {
|
|
5769
|
-
this.coordination = coordination;
|
|
5770
|
-
this.format = format;
|
|
5771
|
-
this.formatSnapshot = formatSnapshot;
|
|
5772
|
-
this.directory = join12(coordination.directory, "history-v2");
|
|
5773
|
-
this.files = coordination.files;
|
|
5774
|
-
}
|
|
5775
|
-
static open(coordination) {
|
|
5776
|
-
const dir = join12(coordination.directory, "history-v2");
|
|
5777
|
-
if (!coordination.files.directory(dir))
|
|
5778
|
-
return;
|
|
5779
|
-
const file = coordination.files.read(join12(dir, "format.json"));
|
|
5780
|
-
if (!file)
|
|
5781
|
-
corrupt();
|
|
5782
|
-
const format = decodeFormat(json(file));
|
|
5783
|
-
if (format.rootIdentity !== hash(coordination.store.root))
|
|
5784
|
-
corrupt();
|
|
5785
|
-
return new HistoryFiles(coordination, format, file);
|
|
5786
|
-
}
|
|
5787
|
-
static initialize(lease) {
|
|
5788
|
-
lease.assert();
|
|
5789
|
-
const c = lease.coordination;
|
|
5790
|
-
const existing = HistoryFiles.open(c);
|
|
5791
|
-
if (existing) {
|
|
5792
|
-
existing.assertWritable();
|
|
5793
|
-
return existing;
|
|
5794
|
-
}
|
|
5795
|
-
const dir = join12(c.directory, "history-v2");
|
|
5796
|
-
const format = {
|
|
5797
|
-
schema: FORMAT,
|
|
5798
|
-
storeId: randomUUID6(),
|
|
5799
|
-
workspaceId: randomUUID6(),
|
|
5800
|
-
rootIdentity: hash(c.store.root)
|
|
5801
|
-
};
|
|
5802
|
-
c.files.directory(dir, true);
|
|
5803
|
-
for (const name of ["commands", "ledger", "runs", "indexes"]) {
|
|
5804
|
-
lease.assert();
|
|
5805
|
-
c.files.directory(join12(dir, name), true);
|
|
5806
|
-
}
|
|
5807
|
-
lease.assert();
|
|
5808
|
-
const keyPath = join12(dir, "read-proof-key.json");
|
|
5809
|
-
c.files.create(keyPath, encoded({
|
|
5810
|
-
schema: "cadence-history-read-key/1",
|
|
5811
|
-
storeId: format.storeId,
|
|
5812
|
-
key: randomBytes5(32).toString("base64url")
|
|
5813
|
-
}));
|
|
5814
|
-
lease.assert();
|
|
5815
|
-
const snapshot = c.files.create(join12(dir, "format.json"), encoded(format));
|
|
5816
|
-
const result = new HistoryFiles(c, format, snapshot);
|
|
5817
|
-
result.syncParents(join12(dir, "format.json"));
|
|
5818
|
-
result.assertWritable();
|
|
5819
|
-
return result;
|
|
5820
|
-
}
|
|
5821
|
-
activity() {
|
|
5822
|
-
const key = `${this.directory}/${this.format.storeId}`;
|
|
5823
|
-
let state = activity.get(key);
|
|
5824
|
-
if (!state) {
|
|
5825
|
-
state = { pending: 0, generation: 0 };
|
|
5826
|
-
activity.set(key, state);
|
|
5827
|
-
}
|
|
5828
|
-
return state;
|
|
5494
|
+
return state;
|
|
5829
5495
|
}
|
|
5830
5496
|
get writeGeneration() {
|
|
5831
5497
|
return this.activity().generation;
|
|
@@ -5849,7 +5515,7 @@ class HistoryFiles {
|
|
|
5849
5515
|
throw new ContractError("quarantine");
|
|
5850
5516
|
}
|
|
5851
5517
|
assertFormat() {
|
|
5852
|
-
this.files.assert(
|
|
5518
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot);
|
|
5853
5519
|
}
|
|
5854
5520
|
readProofMaterial(budget) {
|
|
5855
5521
|
if (this.format.schema !== FORMAT)
|
|
@@ -5858,7 +5524,7 @@ class HistoryFiles {
|
|
|
5858
5524
|
this.assertReadFormat(budget);
|
|
5859
5525
|
else
|
|
5860
5526
|
this.assertFormat();
|
|
5861
|
-
const selected2 = this.readBudgeted(
|
|
5527
|
+
const selected2 = this.readBudgeted(join8(this.directory, "read-proof-key.json"), 512, budget);
|
|
5862
5528
|
if (!selected2 || this.proofSnapshot && (selected2.identity !== this.proofSnapshot.identity || selected2.stamp !== this.proofSnapshot.stamp))
|
|
5863
5529
|
corrupt();
|
|
5864
5530
|
try {
|
|
@@ -5886,17 +5552,17 @@ class HistoryFiles {
|
|
|
5886
5552
|
return;
|
|
5887
5553
|
}
|
|
5888
5554
|
this.assertFormat();
|
|
5889
|
-
assertIncarnation(this,
|
|
5555
|
+
assertIncarnation(this, join8(this.directory, "read-proof-key.json"), this.proofSnapshot.stamp);
|
|
5890
5556
|
}
|
|
5891
5557
|
segmentPath(segment) {
|
|
5892
|
-
return
|
|
5558
|
+
return join8(this.directory, "ledger", `${String(segment).padStart(12, "0")}.jsonl`);
|
|
5893
5559
|
}
|
|
5894
5560
|
artifactPath(ref) {
|
|
5895
|
-
return
|
|
5561
|
+
return join8(this.directory, "runs", ref.runId, "artifacts", ref.artifactId, `${ref.part}.part`);
|
|
5896
5562
|
}
|
|
5897
5563
|
admissionPath(runId) {
|
|
5898
5564
|
decodeId(runId);
|
|
5899
|
-
return
|
|
5565
|
+
return join8(this.directory, "runs", runId, "admission.json");
|
|
5900
5566
|
}
|
|
5901
5567
|
writeAdmission(lease, input) {
|
|
5902
5568
|
lease.assert();
|
|
@@ -5922,7 +5588,7 @@ class HistoryFiles {
|
|
|
5922
5588
|
readCommand(commandId, budget) {
|
|
5923
5589
|
decodeId(commandId);
|
|
5924
5590
|
this.assertReadFormat(budget);
|
|
5925
|
-
return this.readBudgeted(
|
|
5591
|
+
return this.readBudgeted(join8(this.directory, "commands", commandId, "intent.json"), RECORD_BYTES, budget);
|
|
5926
5592
|
}
|
|
5927
5593
|
readAdmission(runId, identity, budget) {
|
|
5928
5594
|
return this.readAdmissionIdentity(runId, hash(canonicalJson(identity)), budget);
|
|
@@ -5943,7 +5609,7 @@ class HistoryFiles {
|
|
|
5943
5609
|
syncParents(file) {
|
|
5944
5610
|
if (process.platform === "win32")
|
|
5945
5611
|
return;
|
|
5946
|
-
for (let path2 =
|
|
5612
|
+
for (let path2 = dirname8(file);; path2 = dirname8(path2)) {
|
|
5947
5613
|
if (!this.files.directory(path2))
|
|
5948
5614
|
corrupt();
|
|
5949
5615
|
const fd = openSync5(path2, constants5.O_RDONLY | constants5.O_DIRECTORY);
|
|
@@ -5954,13 +5620,13 @@ class HistoryFiles {
|
|
|
5954
5620
|
}
|
|
5955
5621
|
if (path2 === this.coordination.directory)
|
|
5956
5622
|
break;
|
|
5957
|
-
if (path2 ===
|
|
5623
|
+
if (path2 === dirname8(path2))
|
|
5958
5624
|
corrupt();
|
|
5959
5625
|
}
|
|
5960
5626
|
}
|
|
5961
5627
|
assertReadFormat(budget) {
|
|
5962
5628
|
budget.reserve(this.formatSnapshot.bytes.length);
|
|
5963
|
-
this.files.assert(
|
|
5629
|
+
this.files.assert(join8(this.directory, "format.json"), this.formatSnapshot, this.formatSnapshot.bytes.length);
|
|
5964
5630
|
}
|
|
5965
5631
|
assertPublished(segment, end, seq) {
|
|
5966
5632
|
const fence = publicationFences.get(this.directory);
|
|
@@ -5998,7 +5664,7 @@ class HistoryFiles {
|
|
|
5998
5664
|
const dir = opendirSync3(path2);
|
|
5999
5665
|
try {
|
|
6000
5666
|
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
6001
|
-
const p =
|
|
5667
|
+
const p = join8(path2, e.name), st = lstatSync6(p);
|
|
6002
5668
|
if (st.isDirectory())
|
|
6003
5669
|
bytes += walk(p, depth + 1);
|
|
6004
5670
|
else {
|
|
@@ -6033,7 +5699,7 @@ class HistoryFiles {
|
|
|
6033
5699
|
state.bytes = 0;
|
|
6034
5700
|
state.identity = null;
|
|
6035
5701
|
let count = 0;
|
|
6036
|
-
const ledger =
|
|
5702
|
+
const ledger = join8(this.directory, "ledger");
|
|
6037
5703
|
if (!this.files.directory(ledger))
|
|
6038
5704
|
corrupt();
|
|
6039
5705
|
const dir = opendirSync3(ledger);
|
|
@@ -6350,7 +6016,7 @@ class HistoryFiles {
|
|
|
6350
6016
|
checkpoint(lease, state) {
|
|
6351
6017
|
lease.assert();
|
|
6352
6018
|
this.assertWritable();
|
|
6353
|
-
const path2 =
|
|
6019
|
+
const path2 = join8(this.directory, "checkpoint.json"), old = this.files.read(path2);
|
|
6354
6020
|
const body = encoded({
|
|
6355
6021
|
schema: "cadence-history-checkpoint/2",
|
|
6356
6022
|
storeId: this.format.storeId,
|
|
@@ -6754,341 +6420,1216 @@ function projectRunObservation(runId, epoch, o, child) {
|
|
|
6754
6420
|
result: null
|
|
6755
6421
|
};
|
|
6756
6422
|
}
|
|
6757
|
-
|
|
6758
|
-
// src/history/recovery.ts
|
|
6759
|
-
function digestHistory(files, exclude) {
|
|
6760
|
-
files.assertFormat();
|
|
6761
|
-
const digest = createHash6("sha256");
|
|
6762
|
-
let count = 0;
|
|
6763
|
-
const walk = (path2, depth) => {
|
|
6764
|
-
if (depth > 8 || !files.files.directory(path2))
|
|
6765
|
-
throw new ContractError("corrupt-selected");
|
|
6766
|
-
const directory2 = lstatSync7(path2);
|
|
6767
|
-
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
6768
|
-
`);
|
|
6769
|
-
const dir = opendirSync4(path2), names2 = [];
|
|
6770
|
-
try {
|
|
6771
|
-
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
6772
|
-
if (++count > LIMITS.dataFrames)
|
|
6773
|
-
throw new ContractError("capacity");
|
|
6774
|
-
names2.push(e.name);
|
|
6775
|
-
}
|
|
6776
|
-
} finally {
|
|
6777
|
-
dir.closeSync();
|
|
6778
|
-
}
|
|
6779
|
-
for (const name of names2.sort()) {
|
|
6780
|
-
const selected2 =
|
|
6781
|
-
if (selected2 === exclude)
|
|
6782
|
-
continue;
|
|
6783
|
-
const rel = relative4(files.directory, selected2);
|
|
6784
|
-
if (lstatSync7(selected2).isDirectory()) {
|
|
6785
|
-
digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
|
|
6786
|
-
`);
|
|
6787
|
-
walk(selected2, depth + 1);
|
|
6788
|
-
} else {
|
|
6789
|
-
const value = files.files.read(selected2, LIMITS.segmentBytes);
|
|
6790
|
-
if (!value)
|
|
6791
|
-
throw new ContractError("conflict");
|
|
6792
|
-
digest.update(`file:${Buffer.byteLength(rel)}:${rel}:${value.stamp}:${value.bytes.length}:`).update(value.bytes);
|
|
6793
|
-
}
|
|
6794
|
-
}
|
|
6423
|
+
|
|
6424
|
+
// src/history/recovery.ts
|
|
6425
|
+
function digestHistory(files, exclude) {
|
|
6426
|
+
files.assertFormat();
|
|
6427
|
+
const digest = createHash6("sha256");
|
|
6428
|
+
let count = 0;
|
|
6429
|
+
const walk = (path2, depth) => {
|
|
6430
|
+
if (depth > 8 || !files.files.directory(path2))
|
|
6431
|
+
throw new ContractError("corrupt-selected");
|
|
6432
|
+
const directory2 = lstatSync7(path2);
|
|
6433
|
+
digest.update(`incarnation:${directory2.dev}:${directory2.ino}
|
|
6434
|
+
`);
|
|
6435
|
+
const dir = opendirSync4(path2), names2 = [];
|
|
6436
|
+
try {
|
|
6437
|
+
for (let e = dir.readSync();e; e = dir.readSync()) {
|
|
6438
|
+
if (++count > LIMITS.dataFrames)
|
|
6439
|
+
throw new ContractError("capacity");
|
|
6440
|
+
names2.push(e.name);
|
|
6441
|
+
}
|
|
6442
|
+
} finally {
|
|
6443
|
+
dir.closeSync();
|
|
6444
|
+
}
|
|
6445
|
+
for (const name of names2.sort()) {
|
|
6446
|
+
const selected2 = join9(path2, name);
|
|
6447
|
+
if (selected2 === exclude)
|
|
6448
|
+
continue;
|
|
6449
|
+
const rel = relative4(files.directory, selected2);
|
|
6450
|
+
if (lstatSync7(selected2).isDirectory()) {
|
|
6451
|
+
digest.update(`directory:${Buffer.byteLength(rel)}:${rel}
|
|
6452
|
+
`);
|
|
6453
|
+
walk(selected2, depth + 1);
|
|
6454
|
+
} else {
|
|
6455
|
+
const value = files.files.read(selected2, LIMITS.segmentBytes);
|
|
6456
|
+
if (!value)
|
|
6457
|
+
throw new ContractError("conflict");
|
|
6458
|
+
digest.update(`file:${Buffer.byteLength(rel)}:${rel}:${value.stamp}:${value.bytes.length}:`).update(value.bytes);
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
};
|
|
6462
|
+
walk(files.directory, 0);
|
|
6463
|
+
files.assertFormat();
|
|
6464
|
+
return digest.digest("hex");
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
class HistoryRecovery {
|
|
6468
|
+
coordination;
|
|
6469
|
+
committed = new WeakMap;
|
|
6470
|
+
constructor(coordination) {
|
|
6471
|
+
this.coordination = coordination;
|
|
6472
|
+
}
|
|
6473
|
+
files() {
|
|
6474
|
+
const files = HistoryFiles.open(this.coordination);
|
|
6475
|
+
if (!files)
|
|
6476
|
+
throw new ContractError("unavailable");
|
|
6477
|
+
return files;
|
|
6478
|
+
}
|
|
6479
|
+
assertCommitted(fact) {
|
|
6480
|
+
const assert = this.committed.get(fact);
|
|
6481
|
+
if (!assert)
|
|
6482
|
+
throw new ContractError("conflict");
|
|
6483
|
+
assert();
|
|
6484
|
+
}
|
|
6485
|
+
async inspect(epoch) {
|
|
6486
|
+
decodeId(epoch);
|
|
6487
|
+
const files = this.files(), before = digestHistory(files), state = emptyScan();
|
|
6488
|
+
const auth = new Set, processes = new Set, publication = new Set, unfinished = new Set, metadata = new Set, runs = new Map;
|
|
6489
|
+
for (const batch of files.scan(state))
|
|
6490
|
+
for (const row of batch.rows) {
|
|
6491
|
+
const v = row.value;
|
|
6492
|
+
if (v.kind === "recovery") {
|
|
6493
|
+
auth.clear();
|
|
6494
|
+
processes.clear();
|
|
6495
|
+
publication.clear();
|
|
6496
|
+
unfinished.clear();
|
|
6497
|
+
metadata.clear();
|
|
6498
|
+
runs.clear();
|
|
6499
|
+
}
|
|
6500
|
+
if (v.kind === "admission" && v.intent.identity.kind === "action") {
|
|
6501
|
+
if (v.intent.runId) {
|
|
6502
|
+
if (runs.size >= LIMITS.episodes || runs.has(v.intent.runId))
|
|
6503
|
+
throw new ContractError("corrupt-selected");
|
|
6504
|
+
runs.set(v.intent.runId, new RunState(v.intent.runId));
|
|
6505
|
+
} else
|
|
6506
|
+
metadata.add(v.intent.operationId);
|
|
6507
|
+
}
|
|
6508
|
+
for (const [id, state2] of runs) {
|
|
6509
|
+
state2.apply(row, batch.marker.last);
|
|
6510
|
+
if (state2.drained)
|
|
6511
|
+
runs.delete(id);
|
|
6512
|
+
}
|
|
6513
|
+
if (v.kind === "capture" || v.kind === "owner")
|
|
6514
|
+
for (const e of v.events) {
|
|
6515
|
+
if (e.kind === "capture" && e.value.event.kind === "detail-begin" && v.kind === "capture")
|
|
6516
|
+
unfinished.add(`worker/${v.link.childRunId}/${e.value.event.detailId}`);
|
|
6517
|
+
if (e.kind === "owner" && e.value.kind === "content-begin" && v.kind === "owner")
|
|
6518
|
+
unfinished.add(`owner/${v.link.operationId}/${e.value.detailId}`);
|
|
6519
|
+
if (e.kind === "end")
|
|
6520
|
+
unfinished.delete(`${e.detail.origin.kind}/${e.detail.origin.id}/${e.detail.detailId}`);
|
|
6521
|
+
}
|
|
6522
|
+
if (v.kind === "auth-preparation")
|
|
6523
|
+
auth.add(v.intent.operationId);
|
|
6524
|
+
if (v.kind === "auth-settlement")
|
|
6525
|
+
auth.delete(v.result.operationId);
|
|
6526
|
+
if (v.kind === "launch")
|
|
6527
|
+
processes.add(v.link.childRunId);
|
|
6528
|
+
if (v.kind === "settlement" && v.completion.native.closure !== "unconfirmed")
|
|
6529
|
+
processes.delete(v.link.childRunId);
|
|
6530
|
+
if (v.kind === "owner") {
|
|
6531
|
+
for (const e of v.events)
|
|
6532
|
+
if (e.kind === "owner" && e.value.kind === "fact") {
|
|
6533
|
+
const f = e.value.fact;
|
|
6534
|
+
if (f.kind === "check-start")
|
|
6535
|
+
processes.add(f.checkId);
|
|
6536
|
+
if (f.kind === "check-settled" && f.native.closure !== "unconfirmed")
|
|
6537
|
+
processes.delete(f.checkId);
|
|
6538
|
+
if (f.kind === "publication-intent")
|
|
6539
|
+
publication.add(f.publicationId);
|
|
6540
|
+
if (f.kind === "publication-settled") {
|
|
6541
|
+
if (f.status === "published") {
|
|
6542
|
+
publication.delete(f.publicationId);
|
|
6543
|
+
metadata.delete(v.link.operationId);
|
|
6544
|
+
} else
|
|
6545
|
+
publication.add(f.publicationId);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
if (before !== digestHistory(files))
|
|
6551
|
+
throw new ContractError("conflict");
|
|
6552
|
+
return {
|
|
6553
|
+
digest: before,
|
|
6554
|
+
processes: processes.size ? "unknown" : "externally-resolved",
|
|
6555
|
+
credentials: auth.size ? "unknown" : "independently-resolved",
|
|
6556
|
+
storage: state.tail || publication.size || unfinished.size || metadata.size || runs.size ? "unknown" : "independently-resolved"
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
async append(confirmation, observeWrite) {
|
|
6560
|
+
if (typeof confirmation.assertLocalAuthority !== "function" || confirmation.acknowledgeEscapedProcesses !== true || confirmation.processes !== "externally-resolved" || confirmation.credentials !== "independently-resolved" || confirmation.storage !== "independently-resolved")
|
|
6561
|
+
throw new ContractError("forbidden");
|
|
6562
|
+
const expected = structuredClone(confirmation.expected), files = this.files();
|
|
6563
|
+
files.assertWritable();
|
|
6564
|
+
if (!expected.history || expected.owner !== "absent")
|
|
6565
|
+
throw new ContractError("quarantine");
|
|
6566
|
+
const current = await inspectRecovery(this.coordination, this);
|
|
6567
|
+
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
6568
|
+
throw new ContractError("conflict");
|
|
6569
|
+
const state = files.inspect(), recoveryId = randomUUID7();
|
|
6570
|
+
const bridge = {
|
|
6571
|
+
kind: "recovery",
|
|
6572
|
+
recoveryId,
|
|
6573
|
+
epoch: expected.epoch,
|
|
6574
|
+
coordinationDigest: expected.coordinationDigest,
|
|
6575
|
+
historyDigest: expected.history.digest,
|
|
6576
|
+
previous: {
|
|
6577
|
+
segment: state.segment,
|
|
6578
|
+
bytes: state.bytes,
|
|
6579
|
+
sha256: state.identity ?? hash(""),
|
|
6580
|
+
committedBytes: state.committedBytes ?? state.bytes,
|
|
6581
|
+
committedSeq: state.committedSeq,
|
|
6582
|
+
highSeq: state.highSeq ?? state.committedSeq
|
|
6583
|
+
},
|
|
6584
|
+
acknowledgments: { processes: true, credentials: true, storage: true }
|
|
6585
|
+
};
|
|
6586
|
+
const path2 = files.segmentPath(state.segment + 1);
|
|
6587
|
+
const assertCurrent = async () => {
|
|
6588
|
+
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
6589
|
+
const selected2 = await inspectRecovery(this.coordination);
|
|
6590
|
+
if (selected2.owner !== "absent")
|
|
6591
|
+
throw new ContractError("quarantine");
|
|
6592
|
+
if (selected2.epoch !== expected.epoch || selected2.coordinationDigest !== expected.coordinationDigest || digestHistory(files, path2) !== expected.history?.digest || this.coordination.recoverySnapshot().digest !== expected.coordinationDigest)
|
|
6593
|
+
throw new ContractError("conflict");
|
|
6594
|
+
};
|
|
6595
|
+
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
6596
|
+
if (digestHistory(files) !== expected.history.digest)
|
|
6597
|
+
throw new ContractError("conflict");
|
|
6598
|
+
const row = {
|
|
6599
|
+
kind: "record",
|
|
6600
|
+
id: randomUUID7(),
|
|
6601
|
+
seq: String(BigInt(bridge.previous.highSeq) + 1n),
|
|
6602
|
+
value: bridge
|
|
6603
|
+
};
|
|
6604
|
+
let quota = LIMITS.checkoutQuotaBytes;
|
|
6605
|
+
for (const batch of files.scan())
|
|
6606
|
+
for (const row2 of batch.rows)
|
|
6607
|
+
if (row2.value.kind === "admission" && row2.value.policy)
|
|
6608
|
+
quota = row2.value.policy.quotaBytes;
|
|
6609
|
+
const bytes = Buffer.byteLength(files.encodeBatch(state, [row]).body + files.encodeBatch(state, [row]).trailer);
|
|
6610
|
+
if (files.usage() + bytes > quota || files.freeBytes() < LIMITS.freeSpaceFloorBytes + bytes)
|
|
6611
|
+
throw new ContractError("recording-exhausted");
|
|
6612
|
+
const after = await files.appendRecovery(state, row, assertCurrent, observeWrite);
|
|
6613
|
+
const assert = () => {
|
|
6614
|
+
files.assertWritable();
|
|
6615
|
+
files.assertRecoveryIdle();
|
|
6616
|
+
files.files.assert(path2, after.snapshot, LIMITS.segmentBytes);
|
|
6617
|
+
if (digestHistory(files, path2) !== expected.history?.digest)
|
|
6618
|
+
throw new ContractError("conflict");
|
|
6619
|
+
};
|
|
6620
|
+
assert();
|
|
6621
|
+
const fact = {
|
|
6622
|
+
recoveryId,
|
|
6623
|
+
committedSeq: after.state.committedSeq,
|
|
6624
|
+
epoch: expected.epoch,
|
|
6625
|
+
coordinationDigest: expected.coordinationDigest,
|
|
6626
|
+
historyDigest: expected.history.digest,
|
|
6627
|
+
segmentIdentity: after.state.identity ?? ""
|
|
6628
|
+
};
|
|
6629
|
+
this.committed.set(fact, assert);
|
|
6630
|
+
return fact;
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
// src/service/gateway-recovery.ts
|
|
6635
|
+
import { opendirSync as opendirSync5 } from "node:fs";
|
|
6636
|
+
import { hostname as hostname4 } from "node:os";
|
|
6637
|
+
import { join as join10 } from "node:path";
|
|
6638
|
+
function required(resources, name) {
|
|
6639
|
+
const file = resources.get(name);
|
|
6640
|
+
if (!file)
|
|
6641
|
+
throw new ContractError("corrupt-selected");
|
|
6642
|
+
return file;
|
|
6643
|
+
}
|
|
6644
|
+
function names2(store, path2, maximum) {
|
|
6645
|
+
if (!store.files.directory(path2))
|
|
6646
|
+
return [];
|
|
6647
|
+
const stream = opendirSync5(path2), out = [];
|
|
6648
|
+
try {
|
|
6649
|
+
for (let row = stream.readSync();row; row = stream.readSync()) {
|
|
6650
|
+
if (out.length >= maximum || !row.isFile() && !row.isDirectory())
|
|
6651
|
+
throw new ContractError("corrupt-selected");
|
|
6652
|
+
out.push(row.name);
|
|
6653
|
+
}
|
|
6654
|
+
} finally {
|
|
6655
|
+
stream.closeSync();
|
|
6656
|
+
}
|
|
6657
|
+
return out.sort();
|
|
6658
|
+
}
|
|
6659
|
+
function capture(store) {
|
|
6660
|
+
const out = new Map;
|
|
6661
|
+
const keep = (name) => {
|
|
6662
|
+
const file = store.files.read(store.path(name));
|
|
6663
|
+
if (!file)
|
|
6664
|
+
throw new ContractError("corrupt-selected");
|
|
6665
|
+
out.set(name, file);
|
|
6666
|
+
};
|
|
6667
|
+
for (const name of names2(store, store.directory, 9)) {
|
|
6668
|
+
if ([
|
|
6669
|
+
"gateway.json",
|
|
6670
|
+
"gateway.lock",
|
|
6671
|
+
"input.json",
|
|
6672
|
+
"access-config.json",
|
|
6673
|
+
"access.json"
|
|
6674
|
+
].includes(name))
|
|
6675
|
+
keep(name);
|
|
6676
|
+
else if (name === "projects" || name === "commands") {
|
|
6677
|
+
for (const child of names2(store, store.path(name), name === "projects" ? 128 : 2048)) {
|
|
6678
|
+
if (name === "projects" ? !/^[a-f0-9]{64}\.json$/.test(child) : !/^[a-f0-9-]{36}(?:\.result)?\.json$/.test(child))
|
|
6679
|
+
throw new ContractError("corrupt-selected");
|
|
6680
|
+
keep(`${name}/${child}`);
|
|
6681
|
+
}
|
|
6682
|
+
} else if (name !== "recovery-v1" || !store.files.directory(store.path(name)))
|
|
6683
|
+
throw new ContractError("corrupt-selected");
|
|
6684
|
+
}
|
|
6685
|
+
for (const name of [
|
|
6686
|
+
"gateway.json",
|
|
6687
|
+
"input.json",
|
|
6688
|
+
"access-config.json",
|
|
6689
|
+
"access.json"
|
|
6690
|
+
])
|
|
6691
|
+
if (!out.has(name))
|
|
6692
|
+
throw new ContractError("corrupt-selected");
|
|
6693
|
+
for (const [name, file] of out) {
|
|
6694
|
+
if (!name.startsWith("commands/"))
|
|
6695
|
+
continue;
|
|
6696
|
+
if (name.endsWith(".result.json")) {
|
|
6697
|
+
if (!out.has(name.replace(".result.json", ".json")))
|
|
6698
|
+
throw new ContractError("corrupt-selected");
|
|
6699
|
+
decodeProjectEntry(json(file));
|
|
6700
|
+
} else {
|
|
6701
|
+
decodeId(name.slice("commands/".length, -5));
|
|
6702
|
+
const intent = closed(json(file), ["path", "epoch"]);
|
|
6703
|
+
text(intent.path, 4096, true);
|
|
6704
|
+
decodeId(intent.epoch);
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
return out;
|
|
6708
|
+
}
|
|
6709
|
+
function signature(resources) {
|
|
6710
|
+
return hash(encoded([...resources].sort(([a], [b]) => a.localeCompare(b, "en")).map(([name, file]) => [
|
|
6711
|
+
name,
|
|
6712
|
+
file.stamp,
|
|
6713
|
+
["gateway.json", "access.json", "access-config.json"].includes(name) ? null : file.identity
|
|
6714
|
+
])));
|
|
6715
|
+
}
|
|
6716
|
+
function state(store) {
|
|
6717
|
+
const registration2 = store.read();
|
|
6718
|
+
if (!registration2)
|
|
6719
|
+
throw new ContractError("unavailable");
|
|
6720
|
+
const resources = capture(store), lock = resources.get("gateway.lock");
|
|
6721
|
+
if (!lock)
|
|
6722
|
+
throw new ContractError("unavailable");
|
|
6723
|
+
decodeId(closed(json(lock), ["epoch"]).epoch);
|
|
6724
|
+
const input = decodeGatewayInput(json(required(resources, "input.json")));
|
|
6725
|
+
if (input.serverConfig !== store.serverConfig || input.stateHome !== store.stateHome)
|
|
6726
|
+
throw new ContractError("conflict");
|
|
6727
|
+
const projects = store.projects();
|
|
6728
|
+
store.associatedProjects();
|
|
6729
|
+
const incarnation = hash(encoded([lock.identity, lock.stamp]));
|
|
6730
|
+
if (registration2.phase === "stopped" && !registration2.recovery?.receipt.startsWith(`recovery-v1/${incarnation}/`))
|
|
6731
|
+
throw new ContractError("corrupt-selected");
|
|
6732
|
+
const archive = store.path(`recovery-v1/${incarnation}`);
|
|
6733
|
+
const attempts = [];
|
|
6734
|
+
let previousResolver = null;
|
|
6735
|
+
const entries = names2(store, archive, 128);
|
|
6736
|
+
for (let n = 0;n < entries.length; n++) {
|
|
6737
|
+
if (!entries.includes(`${n}.json`))
|
|
6738
|
+
throw new ContractError("corrupt-selected");
|
|
6739
|
+
const file = store.files.read(join10(archive, `${n}.json`), 65536);
|
|
6740
|
+
if (!file)
|
|
6741
|
+
throw new ContractError("corrupt-selected");
|
|
6742
|
+
const row = closed(json(file, 65536), [
|
|
6743
|
+
"schema",
|
|
6744
|
+
"lock",
|
|
6745
|
+
"epoch",
|
|
6746
|
+
"digest",
|
|
6747
|
+
"pid",
|
|
6748
|
+
"host",
|
|
6749
|
+
"legacyHost",
|
|
6750
|
+
"retiredProjects"
|
|
6751
|
+
]);
|
|
6752
|
+
literal(row.schema, "cadence-workbench-recovery/1");
|
|
6753
|
+
literal(row.lock, lock.bytes.toString("utf8"));
|
|
6754
|
+
literal(row.epoch, registration2.epoch);
|
|
6755
|
+
decodeHash(row.digest);
|
|
6756
|
+
if (!Array.isArray(row.retiredProjects) || row.retiredProjects.length > 128)
|
|
6757
|
+
throw new ContractError("corrupt-selected");
|
|
6758
|
+
for (const entry of row.retiredProjects) {
|
|
6759
|
+
const project = closed(entry, ["id", "stamp"]);
|
|
6760
|
+
decodeHash(project.id);
|
|
6761
|
+
text(project.stamp, 512, true);
|
|
6762
|
+
}
|
|
6763
|
+
if (typeof row.legacyHost !== "boolean")
|
|
6764
|
+
throw new ContractError("corrupt-selected");
|
|
6765
|
+
previousResolver = recoveryOwnerStatus(integer(row.pid, 1, 2147483647), text(row.host, 255, true));
|
|
6766
|
+
attempts.push(file);
|
|
6767
|
+
}
|
|
6768
|
+
if (encoded(store.read()) !== encoded(registration2))
|
|
6769
|
+
throw new ContractError("conflict");
|
|
6770
|
+
for (const [name, file] of resources)
|
|
6771
|
+
store.files.assert(store.path(name), file);
|
|
6772
|
+
const localOwner = recoveryOwnerStatus(registration2.pid, registration2.host ?? hostname4());
|
|
6773
|
+
const inspection = {
|
|
6774
|
+
scope: "workbench",
|
|
6775
|
+
epoch: registration2.epoch,
|
|
6776
|
+
coordinationDigest: hash(encoded([
|
|
6777
|
+
signature(resources),
|
|
6778
|
+
attempts.map((a) => [a.identity, a.stamp])
|
|
6779
|
+
])),
|
|
6780
|
+
owner: registration2.host ? localOwner : localOwner === "live" ? "live" : "ambiguous",
|
|
6781
|
+
process: { pid: registration2.pid, host: registration2.host ?? null },
|
|
6782
|
+
legacyHost: !registration2.host,
|
|
6783
|
+
previousResolver,
|
|
6784
|
+
projects: projects.length,
|
|
6785
|
+
openings: [...resources.keys()].filter((name) => name.startsWith("commands/") && !name.endsWith(".result.json")).length,
|
|
6786
|
+
resolution: registration2.host ? "local-interactive-only" : "local-host-confirmation-required"
|
|
6787
|
+
};
|
|
6788
|
+
return {
|
|
6789
|
+
registration: registration2,
|
|
6790
|
+
resources,
|
|
6791
|
+
lock,
|
|
6792
|
+
archive,
|
|
6793
|
+
incarnation,
|
|
6794
|
+
attempts,
|
|
6795
|
+
localOwner,
|
|
6796
|
+
inspection
|
|
6797
|
+
};
|
|
6798
|
+
}
|
|
6799
|
+
function inspectGatewayRecovery(store) {
|
|
6800
|
+
const before = state(store), after = state(store);
|
|
6801
|
+
if (encoded(before.inspection) !== encoded(after.inspection))
|
|
6802
|
+
throw new ContractError("conflict");
|
|
6803
|
+
return before.inspection;
|
|
6804
|
+
}
|
|
6805
|
+
function resolveGatewayRecovery(store, expected, assertAuthority, legacyHostConfirmed = false) {
|
|
6806
|
+
assertAuthority();
|
|
6807
|
+
const selected2 = state(store);
|
|
6808
|
+
if (encoded(selected2.inspection) !== encoded(expected))
|
|
6809
|
+
throw new ContractError("conflict");
|
|
6810
|
+
const absent2 = () => {
|
|
6811
|
+
assertAuthority();
|
|
6812
|
+
if (expected.legacyHost && !legacyHostConfirmed || recoveryOwnerStatus(selected2.registration.pid, selected2.registration.host ?? hostname4()) !== "absent")
|
|
6813
|
+
throw new ContractError("quarantine");
|
|
6814
|
+
};
|
|
6815
|
+
absent2();
|
|
6816
|
+
if (expected.previousResolver !== null && expected.previousResolver !== "absent")
|
|
6817
|
+
throw new ContractError("quarantine");
|
|
6818
|
+
if (selected2.attempts.length >= 128)
|
|
6819
|
+
throw new ContractError("capacity");
|
|
6820
|
+
const receiptPath = join10(selected2.archive, `${selected2.attempts.length}.json`);
|
|
6821
|
+
const receipt = store.files.create(receiptPath, encoded({
|
|
6822
|
+
schema: "cadence-workbench-recovery/1",
|
|
6823
|
+
lock: selected2.lock.bytes.toString("utf8"),
|
|
6824
|
+
epoch: expected.epoch,
|
|
6825
|
+
digest: expected.coordinationDigest,
|
|
6826
|
+
pid: process.pid,
|
|
6827
|
+
host: hostname4(),
|
|
6828
|
+
legacyHost: expected.legacyHost,
|
|
6829
|
+
retiredProjects: [...selected2.resources].filter(([name]) => name.startsWith("projects/")).map(([name, file]) => ({
|
|
6830
|
+
id: name.slice("projects/".length, -5),
|
|
6831
|
+
stamp: file.stamp
|
|
6832
|
+
}))
|
|
6833
|
+
}), 65536);
|
|
6834
|
+
const assert = () => {
|
|
6835
|
+
absent2();
|
|
6836
|
+
store.associatedProjects();
|
|
6837
|
+
const current = capture(store);
|
|
6838
|
+
if (signature(current) !== signature(selected2.resources))
|
|
6839
|
+
throw new ContractError("conflict");
|
|
6840
|
+
for (const [name, file] of selected2.resources)
|
|
6841
|
+
store.files.assert(store.path(name), file);
|
|
6842
|
+
if (names2(store, selected2.archive, 128).length !== selected2.attempts.length + 1)
|
|
6843
|
+
throw new ContractError("conflict");
|
|
6844
|
+
for (const [n, file] of selected2.attempts.entries())
|
|
6845
|
+
store.files.assert(join10(selected2.archive, `${n}.json`), file, 65536);
|
|
6846
|
+
store.files.assert(receiptPath, receipt, 65536);
|
|
6847
|
+
};
|
|
6848
|
+
assert();
|
|
6849
|
+
const registration2 = store.files.replace(store.path("gateway.json"), required(selected2.resources, "gateway.json"), encoded({
|
|
6850
|
+
...selected2.registration,
|
|
6851
|
+
phase: "stopped",
|
|
6852
|
+
recovery: {
|
|
6853
|
+
receipt: `recovery-v1/${selected2.incarnation}/${selected2.attempts.length}.json`,
|
|
6854
|
+
sha256: receipt.identity,
|
|
6855
|
+
stamp: receipt.stamp
|
|
6856
|
+
}
|
|
6857
|
+
}));
|
|
6858
|
+
selected2.resources.set("gateway.json", registration2);
|
|
6859
|
+
assert();
|
|
6860
|
+
store.files.remove(store.path("gateway.lock"), selected2.lock);
|
|
6861
|
+
return {
|
|
6862
|
+
status: "resolved",
|
|
6863
|
+
scope: "workbench",
|
|
6864
|
+
epoch: expected.epoch,
|
|
6865
|
+
notice: "operator-acknowledgment-only-project-fences-preserved"
|
|
6866
|
+
};
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
// src/service/recovery-cli.ts
|
|
6870
|
+
var warning = "PID absence is not proof that descendants stopped or credential writes settled. Inspect/stop old processes externally, independently inspect the selected Pi credential store and SDK locks, and inspect retained history/storage. Cadence does not kill, repair, reset, migrate or certify those resources. Unknown historical outcomes remain unknown. A new Start is required.";
|
|
6871
|
+
function native() {
|
|
6872
|
+
if (!isatty(0) || !isatty(1) || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
6873
|
+
throw new ContractError("forbidden");
|
|
6874
|
+
}
|
|
6875
|
+
async function confirm(expected) {
|
|
6876
|
+
native();
|
|
6877
|
+
if (process.stdin.readableLength)
|
|
6878
|
+
throw new ContractError("forbidden");
|
|
6879
|
+
const identity = JSON.stringify(expected);
|
|
6880
|
+
const deadline = performance.now() + LIMITS.grantMs;
|
|
6881
|
+
const prompts = [
|
|
6882
|
+
...expected.legacyHost ? [
|
|
6883
|
+
[
|
|
6884
|
+
"Legacy registration has no host identity. Independently verify this state originated on THIS host and is not shared/copied from another host. Type local-host",
|
|
6885
|
+
"local-host"
|
|
6886
|
+
]
|
|
6887
|
+
] : [],
|
|
6888
|
+
[
|
|
6889
|
+
"Confirm old processes stopped externally; acknowledge escaped-descendant uncertainty. Type processes",
|
|
6890
|
+
"processes"
|
|
6891
|
+
],
|
|
6892
|
+
[
|
|
6893
|
+
"Confirm independent credential/refresh/writeback inspection; PID absence does not settle auth. Type credentials",
|
|
6894
|
+
"credentials"
|
|
6895
|
+
],
|
|
6896
|
+
[
|
|
6897
|
+
"Confirm independent storage inspection; preserve damaged bytes and unknown outcomes. Type storage",
|
|
6898
|
+
"storage"
|
|
6899
|
+
],
|
|
6900
|
+
[
|
|
6901
|
+
`${expected.scope === "global-registry" ? "Release ONLY the global registry lock; all project fences remain." : expected.scope === "workbench" ? "Release ONLY the workbench fence and retire its old project associations; all project fences remain." : "Release ONLY the displayed unchanged service-owned fences."} Type resolve ${expected.epoch}`,
|
|
6902
|
+
`resolve ${expected.epoch}`
|
|
6903
|
+
]
|
|
6904
|
+
];
|
|
6905
|
+
await new Promise((resolve5, reject) => {
|
|
6906
|
+
let index = 0, line = "", done = false;
|
|
6907
|
+
const raw = process.stdin.isRaw;
|
|
6908
|
+
const finish = (ok) => {
|
|
6909
|
+
if (done)
|
|
6910
|
+
return;
|
|
6911
|
+
done = true;
|
|
6912
|
+
clearTimeout(timer);
|
|
6913
|
+
process.stdin.removeListener("data", input);
|
|
6914
|
+
process.stdin.removeListener("end", fail);
|
|
6915
|
+
process.stdin.removeListener("error", fail);
|
|
6916
|
+
process.stdin.setRawMode(raw);
|
|
6917
|
+
process.stdin.pause();
|
|
6918
|
+
process.stdout.write(`\x1B[?2004l
|
|
6919
|
+
`);
|
|
6920
|
+
if (ok)
|
|
6921
|
+
resolve5();
|
|
6922
|
+
else
|
|
6923
|
+
reject(new ContractError("forbidden"));
|
|
6924
|
+
};
|
|
6925
|
+
const fail = () => finish(false);
|
|
6926
|
+
const show = () => process.stdout.write(`${prompts[index]?.[0]}: `);
|
|
6927
|
+
const input = (chunk) => {
|
|
6928
|
+
if (performance.now() >= deadline || chunk.length > 128)
|
|
6929
|
+
return fail();
|
|
6930
|
+
for (let n = 0;n < chunk.length; n++) {
|
|
6931
|
+
const byte = chunk[n];
|
|
6932
|
+
if (byte === 13 || byte === 10) {
|
|
6933
|
+
if (n !== chunk.length - 1 || line !== prompts[index]?.[1])
|
|
6934
|
+
return fail();
|
|
6935
|
+
line = "";
|
|
6936
|
+
process.stdout.write(`
|
|
6937
|
+
`);
|
|
6938
|
+
if (++index === prompts.length)
|
|
6939
|
+
return finish(true);
|
|
6940
|
+
show();
|
|
6941
|
+
} else if (byte === 127) {
|
|
6942
|
+
line = line.slice(0, -1);
|
|
6943
|
+
} else if (byte !== undefined && byte >= 32 && byte <= 126 && line.length < 80) {
|
|
6944
|
+
line += String.fromCharCode(byte);
|
|
6945
|
+
} else
|
|
6946
|
+
return fail();
|
|
6947
|
+
}
|
|
6948
|
+
};
|
|
6949
|
+
const timer = setTimeout(fail, LIMITS.grantMs);
|
|
6950
|
+
process.stdin.setRawMode(true);
|
|
6951
|
+
process.stdout.write("\x1B[?2004h");
|
|
6952
|
+
process.stdin.on("data", input).once("end", fail).once("error", fail).resume();
|
|
6953
|
+
show();
|
|
6954
|
+
});
|
|
6955
|
+
return () => {
|
|
6956
|
+
native();
|
|
6957
|
+
if (performance.now() >= deadline || JSON.stringify(expected) !== identity)
|
|
6958
|
+
throw new ContractError("stale");
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
async function localRegistryRecovery(c, operation) {
|
|
6962
|
+
if (operation === "resolve")
|
|
6963
|
+
native();
|
|
6964
|
+
const registryLock = c.registryLockDiagnostic();
|
|
6965
|
+
if (registryLock) {
|
|
6966
|
+
let expected;
|
|
6967
|
+
try {
|
|
6968
|
+
expected = c.inspectRegistryRecovery();
|
|
6969
|
+
} catch {
|
|
6970
|
+
const blocked = {
|
|
6971
|
+
scope: "global-registry",
|
|
6972
|
+
...registryLock,
|
|
6973
|
+
coordination: "unavailable",
|
|
6974
|
+
resolution: "blocked-preserve-state",
|
|
6975
|
+
notice: "Registry state is incomplete, changed or unsafe. Preserve it; no lock or project resource was released."
|
|
6976
|
+
};
|
|
6977
|
+
if (operation === "inspect")
|
|
6978
|
+
return blocked;
|
|
6979
|
+
process.stdout.write(`${JSON.stringify(blocked)}
|
|
6980
|
+
`);
|
|
6981
|
+
throw new ContractError("quarantine");
|
|
6982
|
+
}
|
|
6983
|
+
const view = {
|
|
6984
|
+
...expected,
|
|
6985
|
+
process: {
|
|
6986
|
+
...expected.process,
|
|
6987
|
+
host: diagnosticText(expected.process.host, 255)
|
|
6988
|
+
},
|
|
6989
|
+
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}`
|
|
6990
|
+
};
|
|
6991
|
+
if (operation === "inspect")
|
|
6992
|
+
return view;
|
|
6993
|
+
process.stdout.write(`${JSON.stringify(view)}
|
|
6994
|
+
`);
|
|
6995
|
+
if (expected.owner !== "absent" || expected.previousResolver !== null && expected.previousResolver !== "absent")
|
|
6996
|
+
throw new ContractError("quarantine");
|
|
6997
|
+
const assertFresh = await confirm(expected);
|
|
6998
|
+
return c.resolveRegistryRecovery(expected, assertFresh);
|
|
6999
|
+
}
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
async function localRecovery(c, operation) {
|
|
7003
|
+
const registry = await localRegistryRecovery(c, operation);
|
|
7004
|
+
if (registry !== undefined)
|
|
7005
|
+
return registry;
|
|
7006
|
+
const history = new HistoryRecovery(c);
|
|
7007
|
+
const expected = await inspectRecovery(c, history);
|
|
7008
|
+
const registration2 = c.recoverySnapshot().registration;
|
|
7009
|
+
const view = {
|
|
7010
|
+
...expected,
|
|
7011
|
+
process: {
|
|
7012
|
+
pid: registration2.pid,
|
|
7013
|
+
host: diagnosticText(registration2.host, 255)
|
|
7014
|
+
},
|
|
7015
|
+
notice: warning
|
|
7016
|
+
};
|
|
7017
|
+
if (operation === "inspect")
|
|
7018
|
+
return view;
|
|
7019
|
+
process.stdout.write(`${JSON.stringify(view)}
|
|
7020
|
+
`);
|
|
7021
|
+
if (expected.owner !== "absent")
|
|
7022
|
+
throw new ContractError("quarantine");
|
|
7023
|
+
const assertFresh = await confirm(expected);
|
|
7024
|
+
return resolveRecovery(c, {
|
|
7025
|
+
expected,
|
|
7026
|
+
acknowledgeEscapedProcesses: true,
|
|
7027
|
+
processes: "externally-resolved",
|
|
7028
|
+
credentials: "independently-resolved",
|
|
7029
|
+
storage: "independently-resolved",
|
|
7030
|
+
assertLocalAuthority: (choice) => {
|
|
7031
|
+
assertFresh();
|
|
7032
|
+
if (JSON.stringify(choice) !== JSON.stringify(expected))
|
|
7033
|
+
throw new ContractError("stale");
|
|
7034
|
+
}
|
|
7035
|
+
}, history);
|
|
7036
|
+
}
|
|
7037
|
+
async function localGatewayRecovery(store, operation) {
|
|
7038
|
+
const registry = await localRegistryRecovery(new ServiceCoordination(null, store.stateHome), operation);
|
|
7039
|
+
if (registry !== undefined)
|
|
7040
|
+
return registry;
|
|
7041
|
+
let expected;
|
|
7042
|
+
try {
|
|
7043
|
+
const observed = store.inspect();
|
|
7044
|
+
if (observed.status === "absent" || observed.status === "stopped") {
|
|
7045
|
+
if (operation === "resolve")
|
|
7046
|
+
throw new ContractError("unavailable");
|
|
7047
|
+
return { ...observed, scope: "workbench" };
|
|
7048
|
+
}
|
|
7049
|
+
expected = inspectGatewayRecovery(store);
|
|
7050
|
+
} catch (error) {
|
|
7051
|
+
if (operation === "resolve")
|
|
7052
|
+
throw error;
|
|
7053
|
+
return {
|
|
7054
|
+
status: "unavailable",
|
|
7055
|
+
scope: "workbench",
|
|
7056
|
+
resolution: "blocked-preserve-state",
|
|
7057
|
+
error: error instanceof ContractError ? error.code : "unavailable",
|
|
7058
|
+
notice: "Workbench state is incomplete, changed or unsafe. Preserve it; no resource was released."
|
|
7059
|
+
};
|
|
7060
|
+
}
|
|
7061
|
+
const view = {
|
|
7062
|
+
...expected,
|
|
7063
|
+
process: {
|
|
7064
|
+
...expected.process,
|
|
7065
|
+
host: expected.process.host === null ? null : diagnosticText(expected.process.host, 255)
|
|
7066
|
+
},
|
|
7067
|
+
notice: `Recovery releases ONLY the workbench fence and retires its exact old project associations. Project references, reservations, owner/checkout locks, opening receipts and history remain unchanged. Explicit Open must associate a project again; recover affected project owners separately. ${warning}`
|
|
7068
|
+
};
|
|
7069
|
+
if (operation === "inspect")
|
|
7070
|
+
return view;
|
|
7071
|
+
process.stdout.write(`${JSON.stringify(view)}
|
|
7072
|
+
`);
|
|
7073
|
+
if (!expected.legacyHost && expected.owner !== "absent" || expected.owner === "live" || expected.previousResolver !== null && expected.previousResolver !== "absent")
|
|
7074
|
+
throw new ContractError("quarantine");
|
|
7075
|
+
const authority = await confirm(expected);
|
|
7076
|
+
return resolveGatewayRecovery(store, expected, authority, expected.legacyHost);
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
// src/service/runtime.ts
|
|
7080
|
+
import { homedir as homedir4 } from "node:os";
|
|
7081
|
+
import { dirname as dirname9, isAbsolute as isAbsolute6, join as join11, resolve as resolve5 } from "node:path";
|
|
7082
|
+
var STARTUP_INPUT_BYTES = 16 * 1024;
|
|
7083
|
+
function absolute(value) {
|
|
7084
|
+
const result = text(value, 4096, true);
|
|
7085
|
+
if (!isAbsolute6(result))
|
|
7086
|
+
throw new ContractError("invalid");
|
|
7087
|
+
return result;
|
|
7088
|
+
}
|
|
7089
|
+
function decodeHostConfig(value) {
|
|
7090
|
+
const o = closed(value, [
|
|
7091
|
+
"schema",
|
|
7092
|
+
"http",
|
|
7093
|
+
"cwd",
|
|
7094
|
+
"agentDir",
|
|
7095
|
+
"tokenSource",
|
|
7096
|
+
"tokenFile"
|
|
7097
|
+
]);
|
|
7098
|
+
const tokenSource = oneOf(o.tokenSource, [
|
|
7099
|
+
"generated",
|
|
7100
|
+
"environment",
|
|
7101
|
+
"file",
|
|
7102
|
+
"config"
|
|
7103
|
+
]);
|
|
7104
|
+
const tokenFile = o.tokenFile === null ? null : absolute(o.tokenFile);
|
|
7105
|
+
if ((tokenSource === "file" || tokenSource === "config") !== !!tokenFile)
|
|
7106
|
+
throw new ContractError("invalid");
|
|
7107
|
+
return {
|
|
7108
|
+
schema: literal(o.schema, "cadence-service-host/1"),
|
|
7109
|
+
http: decodeHttpPolicy(o.http),
|
|
7110
|
+
cwd: absolute(o.cwd),
|
|
7111
|
+
agentDir: absolute(o.agentDir),
|
|
7112
|
+
tokenSource,
|
|
7113
|
+
tokenFile
|
|
7114
|
+
};
|
|
7115
|
+
}
|
|
7116
|
+
function readHostConfig(c) {
|
|
7117
|
+
const owner = c.configuration();
|
|
7118
|
+
if (!owner)
|
|
7119
|
+
return;
|
|
7120
|
+
const snapshot = c.files.read(c.path("host-config.json"));
|
|
7121
|
+
if (!snapshot)
|
|
7122
|
+
throw new ContractError("corrupt-selected");
|
|
7123
|
+
const config = decodeHostConfig(json(snapshot));
|
|
7124
|
+
if (!config.http.port || owner.endpoint !== localOrigin(config.http, config.http.port) || owner.policyIdentity !== hash(encoded(config)))
|
|
7125
|
+
throw new ContractError("conflict");
|
|
7126
|
+
return config;
|
|
7127
|
+
}
|
|
7128
|
+
function hostEnvironment(ambient) {
|
|
7129
|
+
let route;
|
|
7130
|
+
const facts = networkSnapshot(ambient);
|
|
7131
|
+
try {
|
|
7132
|
+
if (facts.NODE_OPTIONS.present)
|
|
7133
|
+
throw new Error;
|
|
7134
|
+
route = sdkNetworkEnvironment(facts, networkSnapshot(undefined));
|
|
7135
|
+
} catch {
|
|
7136
|
+
throw new ContractError("unsupported-context");
|
|
7137
|
+
}
|
|
7138
|
+
const result = Object.create(null);
|
|
7139
|
+
for (const key of Object.keys(ambient)) {
|
|
7140
|
+
const d = Object.getOwnPropertyDescriptor(ambient, key);
|
|
7141
|
+
if (!d || !("value" in d) || typeof d.value !== "string")
|
|
7142
|
+
throw new ContractError("invalid");
|
|
7143
|
+
if (/^(?:CADENCE_|PI_|ABEL_|TRELLIS_|GIT_)/i.test(key) || networkKeys.some((name) => name === key))
|
|
7144
|
+
continue;
|
|
7145
|
+
result[key] = d.value;
|
|
7146
|
+
}
|
|
7147
|
+
if (route.redactionLiterals.length) {
|
|
7148
|
+
result.HTTP_PROXY = route.env.HTTP_PROXY;
|
|
7149
|
+
result.HTTPS_PROXY = route.env.HTTPS_PROXY;
|
|
7150
|
+
result.NODE_USE_ENV_PROXY = "1";
|
|
7151
|
+
}
|
|
7152
|
+
result.PI_OFFLINE = "1";
|
|
7153
|
+
result.NO_COLOR = "1";
|
|
7154
|
+
if (!sameNetwork(facts, networkSnapshot(ambient)))
|
|
7155
|
+
throw new ContractError("stale");
|
|
7156
|
+
return result;
|
|
7157
|
+
}
|
|
7158
|
+
function selectStart(options, ambient) {
|
|
7159
|
+
const workspace = resolveWorkspace(options.workspace), c = new ServiceCoordination(workspace.root, options.stateHome);
|
|
7160
|
+
const old = readHostConfig(c);
|
|
7161
|
+
const env = Object.getOwnPropertyDescriptor(ambient, "CADENCE_WEB_TOKEN");
|
|
7162
|
+
if (env && (!("value" in env) || typeof env.value !== "string"))
|
|
7163
|
+
throw new ContractError("invalid");
|
|
7164
|
+
let fromFile;
|
|
7165
|
+
if (options.config) {
|
|
7166
|
+
const path2 = resolve5(options.config), f = new PrivateFiles(dirname9(path2));
|
|
7167
|
+
const bytes = f.read(path2);
|
|
7168
|
+
if (!bytes)
|
|
7169
|
+
throw new ContractError("corrupt-selected");
|
|
7170
|
+
fromFile = decodeHostConfig(json(bytes));
|
|
7171
|
+
}
|
|
7172
|
+
const base = old ?? fromFile ?? decodeHostConfig({
|
|
7173
|
+
schema: "cadence-service-host/1",
|
|
7174
|
+
cwd: workspace.cwd,
|
|
7175
|
+
http: {
|
|
7176
|
+
schema: "cadence-service-http/1",
|
|
7177
|
+
host: options.host ?? "127.0.0.1",
|
|
7178
|
+
port: options.port ?? 17423,
|
|
7179
|
+
proxy: null
|
|
7180
|
+
},
|
|
7181
|
+
agentDir: resolve5(options.agentDir ?? ambient.PI_CODING_AGENT_DIR ?? join11(ambient.HOME ?? ambient.USERPROFILE ?? homedir4(), ".pi", "agent")),
|
|
7182
|
+
tokenSource: options.tokenFile ? "file" : env ? "environment" : "generated",
|
|
7183
|
+
tokenFile: options.tokenFile ? resolve5(options.tokenFile) : null
|
|
7184
|
+
});
|
|
7185
|
+
const server = options.serverConfig !== undefined ? resolve5(options.serverConfig) : old?.tokenSource === "config" ? old.tokenFile : null;
|
|
7186
|
+
let selected2 = base;
|
|
7187
|
+
if (server) {
|
|
7188
|
+
if (options.tokenFile !== undefined)
|
|
7189
|
+
throw new ContractError("conflict");
|
|
7190
|
+
const { port } = readServerConfig(server).config;
|
|
7191
|
+
selected2 = decodeHostConfig({
|
|
7192
|
+
...base,
|
|
7193
|
+
http: { ...base.http, port },
|
|
7194
|
+
tokenSource: "config",
|
|
7195
|
+
tokenFile: server
|
|
7196
|
+
});
|
|
7197
|
+
}
|
|
7198
|
+
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 && resolve5(options.agentDir) !== selected2.agentDir || options.tokenFile !== undefined && resolve5(options.tokenFile) !== selected2.tokenFile || env && selected2.tokenSource !== "environment" || selected2.tokenSource === "environment" && !env || resolveWorkspace(selected2.cwd).root !== workspace.root)
|
|
7199
|
+
throw new ContractError("conflict");
|
|
7200
|
+
return {
|
|
7201
|
+
workspace: workspace.root,
|
|
7202
|
+
stateHome: c.stateHome,
|
|
7203
|
+
config: selected2,
|
|
7204
|
+
environmentToken: env?.value ?? null
|
|
7205
|
+
};
|
|
7206
|
+
}
|
|
7207
|
+
function decodeHostInput(value) {
|
|
7208
|
+
const o = closed(value, [
|
|
7209
|
+
"workspace",
|
|
7210
|
+
"stateHome",
|
|
7211
|
+
"config",
|
|
7212
|
+
"environmentToken"
|
|
7213
|
+
]);
|
|
7214
|
+
const config = decodeHostConfig(o.config);
|
|
7215
|
+
const environmentToken = o.environmentToken === null ? null : text(o.environmentToken, 1024, true);
|
|
7216
|
+
if (config.tokenSource === "environment" !== (environmentToken !== null))
|
|
7217
|
+
throw new ContractError("invalid");
|
|
7218
|
+
return {
|
|
7219
|
+
workspace: absolute(o.workspace),
|
|
7220
|
+
stateHome: absolute(o.stateHome),
|
|
7221
|
+
config,
|
|
7222
|
+
environmentToken
|
|
7223
|
+
};
|
|
7224
|
+
}
|
|
7225
|
+
function portValue(value) {
|
|
7226
|
+
if (!/^(0|[1-9][0-9]{0,4})$/.test(value))
|
|
7227
|
+
throw new ContractError("invalid");
|
|
7228
|
+
return integer(Number(value), 0, 65535);
|
|
7229
|
+
}
|
|
7230
|
+
var STARTUP_STAGES = [
|
|
7231
|
+
"package",
|
|
7232
|
+
"pi-agent-directory",
|
|
7233
|
+
"pi-auth",
|
|
7234
|
+
"pi-models",
|
|
7235
|
+
"pi-runtime",
|
|
7236
|
+
"service"
|
|
7237
|
+
];
|
|
7238
|
+
function decodeStartupReport(value) {
|
|
7239
|
+
const o = closed(value, ["status", "error", "stage"]);
|
|
7240
|
+
return {
|
|
7241
|
+
status: oneOf(o.status, ["refused", "quarantined"]),
|
|
7242
|
+
error: oneOf(o.error, ERROR_CODES),
|
|
7243
|
+
stage: oneOf(o.stage, STARTUP_STAGES)
|
|
7244
|
+
};
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7247
|
+
// src/service/gateway-cli.ts
|
|
7248
|
+
async function gatewayMain(argv, hostUrl) {
|
|
7249
|
+
const operation = argv.shift();
|
|
7250
|
+
if (![
|
|
7251
|
+
"start",
|
|
7252
|
+
"status",
|
|
7253
|
+
"diagnose",
|
|
7254
|
+
"stop",
|
|
7255
|
+
"token-link",
|
|
7256
|
+
"token-rotate",
|
|
7257
|
+
"recover"
|
|
7258
|
+
].includes(operation ?? ""))
|
|
7259
|
+
throw new ContractError("invalid");
|
|
7260
|
+
const recovery = operation === "recover" ? argv.shift() : undefined;
|
|
7261
|
+
if (operation === "recover" && recovery !== "inspect" && recovery !== "resolve")
|
|
7262
|
+
throw new ContractError("invalid");
|
|
7263
|
+
let config = serverConfigPath(homedir5()), stateHome = defaultStateHome(), agentDir, policyFile;
|
|
7264
|
+
let foreground = false, reveal = false;
|
|
7265
|
+
const seen = new Set;
|
|
7266
|
+
while (argv.length) {
|
|
7267
|
+
const key = argv.shift();
|
|
7268
|
+
if (seen.has(key))
|
|
7269
|
+
throw new ContractError("invalid");
|
|
7270
|
+
seen.add(key);
|
|
7271
|
+
if (key === "--foreground" && operation === "start")
|
|
7272
|
+
foreground = true;
|
|
7273
|
+
else if (key === "--reveal" && operation === "token-link")
|
|
7274
|
+
reveal = true;
|
|
7275
|
+
else if (argv[0] && (operation === "recover" ? ["--server-config", "--state-home"] : ["--server-config", "--state-home", "--agent-dir", "--config"]).includes(key)) {
|
|
7276
|
+
const value = resolve6(argv.shift());
|
|
7277
|
+
if (key === "--server-config")
|
|
7278
|
+
config = value;
|
|
7279
|
+
if (key === "--state-home")
|
|
7280
|
+
stateHome = value;
|
|
7281
|
+
if (key === "--agent-dir")
|
|
7282
|
+
agentDir = value;
|
|
7283
|
+
if (key === "--config")
|
|
7284
|
+
policyFile = value;
|
|
7285
|
+
} else
|
|
7286
|
+
throw new ContractError("invalid");
|
|
7287
|
+
}
|
|
7288
|
+
const store = new GatewayStore(config, stateHome);
|
|
7289
|
+
let result;
|
|
7290
|
+
if (operation === "start") {
|
|
7291
|
+
const network = networkSnapshot(process.env), env = hostEnvironment(process.env);
|
|
7292
|
+
if (!seen.has("--server-config"))
|
|
7293
|
+
ensureServerConfig(homedir5());
|
|
7294
|
+
const server = readServerConfig(config).config;
|
|
7295
|
+
const selectedFile = store.files.read(store.path("input.json"));
|
|
7296
|
+
const selected2 = selectedFile ? decodeGatewayInput(json(selectedFile)) : undefined;
|
|
7297
|
+
let policy = selected2?.policy ?? decodeHttpPolicy({
|
|
7298
|
+
schema: "cadence-service-http/1",
|
|
7299
|
+
host: "127.0.0.1",
|
|
7300
|
+
port: server.port,
|
|
7301
|
+
proxy: null
|
|
7302
|
+
});
|
|
7303
|
+
if (policyFile) {
|
|
7304
|
+
const file = new PrivateFiles(dirname10(policyFile)).read(policyFile);
|
|
7305
|
+
if (!file)
|
|
7306
|
+
throw new ContractError("invalid");
|
|
7307
|
+
policy = decodeHttpPolicy(json(file));
|
|
7308
|
+
}
|
|
7309
|
+
const input = decodeGatewayInput({
|
|
7310
|
+
kind: "workbench",
|
|
7311
|
+
serverConfig: config,
|
|
7312
|
+
stateHome: store.stateHome,
|
|
7313
|
+
agentDir: agentDir ?? selected2?.agentDir ?? resolve6(process.env.PI_CODING_AGENT_DIR ?? join12(homedir5(), ".pi", "agent")),
|
|
7314
|
+
policy: { ...policy, port: server.port }
|
|
7315
|
+
});
|
|
7316
|
+
const status = await serviceStatus(store);
|
|
7317
|
+
if (status && typeof status === "object" && "status" in status && status.status === "alive") {
|
|
7318
|
+
if (!selected2 || encoded(selected2) !== encoded(input))
|
|
7319
|
+
throw new ContractError("conflict");
|
|
7320
|
+
result = status;
|
|
7321
|
+
} else
|
|
7322
|
+
result = await launchHost(input, store, foreground, hostUrl, dirname10(dirname10(fileURLToPath(hostUrl))), env, network);
|
|
7323
|
+
if (!foreground && result && typeof result === "object" && "status" in result && result.status === "alive") {
|
|
7324
|
+
const reply = closed(await nativeCall(store, "token-link", true), [
|
|
7325
|
+
"link"
|
|
7326
|
+
]);
|
|
7327
|
+
process.stderr.write(`Cadence workbench: ${text(reply.link, 4096, true)}
|
|
7328
|
+
No project or default workspace was created.
|
|
7329
|
+
`);
|
|
7330
|
+
}
|
|
7331
|
+
} else if (operation === "recover" && (recovery === "inspect" || recovery === "resolve"))
|
|
7332
|
+
result = await localGatewayRecovery(store, recovery);
|
|
7333
|
+
else if (operation === "stop")
|
|
7334
|
+
result = await stopService(store);
|
|
7335
|
+
else if (operation === "status" || operation === "diagnose")
|
|
7336
|
+
result = await serviceStatus(store, operation === "diagnose");
|
|
7337
|
+
else {
|
|
7338
|
+
if (operation === "token-link" && !reveal)
|
|
7339
|
+
throw new ContractError("forbidden");
|
|
7340
|
+
result = await nativeCall(store, operation, reveal);
|
|
7341
|
+
}
|
|
7342
|
+
if (result !== null)
|
|
7343
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
7344
|
+
`);
|
|
7345
|
+
if (result && typeof result === "object" && "status" in result && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)))
|
|
7346
|
+
process.exitCode = managementExitCode(result, foreground);
|
|
7347
|
+
if (result && typeof result === "object" && "status" in result && ["unavailable", "quarantined", "refused", "unconfirmed"].includes(String(result.status)))
|
|
7348
|
+
process.stderr.write(`Preserve state; inspect locally: cadence workbench recover inspect --server-config ${JSON.stringify(config)} --state-home ${JSON.stringify(store.stateHome)}. Eligible recovery requires recover resolve in a local terminal; it never starts work.
|
|
7349
|
+
`);
|
|
7350
|
+
}
|
|
7351
|
+
|
|
7352
|
+
// src/service/launchd.ts
|
|
7353
|
+
import { rmSync as rmSync2 } from "node:fs";
|
|
7354
|
+
import { dirname as dirname11, join as join13 } from "node:path";
|
|
7355
|
+
var LABEL = "com.abelxiaoxing.cadence";
|
|
7356
|
+
var MARKER2 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7357
|
+
<!-- Generated by @abelxiaoxing/cadence`;
|
|
7358
|
+
function plistPath(s) {
|
|
7359
|
+
return join13(s.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
7360
|
+
}
|
|
7361
|
+
var xml = (value) => checked(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
7362
|
+
function buildPlist(u) {
|
|
7363
|
+
const string = (value) => ` <string>${xml(value)}</string>`;
|
|
7364
|
+
return [
|
|
7365
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
7366
|
+
"<!-- Generated by @abelxiaoxing/cadence; `cadence server install` and `restart` rewrite this file. -->",
|
|
7367
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
7368
|
+
'<plist version="1.0">',
|
|
7369
|
+
"<dict>",
|
|
7370
|
+
"\t<key>Label</key>",
|
|
7371
|
+
` <string>${LABEL}</string>`,
|
|
7372
|
+
"\t<key>ProgramArguments</key>",
|
|
7373
|
+
"\t<array>",
|
|
7374
|
+
...[
|
|
7375
|
+
u.node,
|
|
7376
|
+
u.cli,
|
|
7377
|
+
...u.workspace === null ? ["workbench", "start", "--foreground"] : ["start", "--foreground", "--workspace", u.workspace],
|
|
7378
|
+
"--server-config",
|
|
7379
|
+
u.serverConfig
|
|
7380
|
+
].map(string),
|
|
7381
|
+
"\t</array>",
|
|
7382
|
+
...u.workspace === null ? ["<!-- Cadence workbench gateway; no project workspace -->"] : [],
|
|
7383
|
+
"\t<key>WorkingDirectory</key>",
|
|
7384
|
+
` <string>${xml(u.workspace ?? dirname11(dirname11(u.cli)))}</string>`,
|
|
7385
|
+
"\t<key>EnvironmentVariables</key>",
|
|
7386
|
+
"\t<dict>",
|
|
7387
|
+
...u.environment.flatMap(([k, v]) => [
|
|
7388
|
+
` <key>${xml(k)}</key>`,
|
|
7389
|
+
string(v)
|
|
7390
|
+
]),
|
|
7391
|
+
"\t</dict>",
|
|
7392
|
+
"\t<key>RunAtLoad</key>",
|
|
7393
|
+
"\t<true/>",
|
|
7394
|
+
"\t<key>ProcessType</key>",
|
|
7395
|
+
"\t<string>Background</string>",
|
|
7396
|
+
"\t<key>StandardOutPath</key>",
|
|
7397
|
+
` <string>${xml(u.log)}</string>`,
|
|
7398
|
+
"\t<key>StandardErrorPath</key>",
|
|
7399
|
+
` <string>${xml(u.log)}</string>`,
|
|
7400
|
+
"</dict>",
|
|
7401
|
+
"</plist>",
|
|
7402
|
+
""
|
|
7403
|
+
].join(`
|
|
7404
|
+
`);
|
|
7405
|
+
}
|
|
7406
|
+
function plistWorkspace(text2) {
|
|
7407
|
+
if (text2?.includes("<!-- Cadence workbench gateway; no project workspace -->"))
|
|
7408
|
+
return null;
|
|
7409
|
+
const m = text2?.match(/^\t<key>WorkingDirectory<\/key>\n\t<string>([^<]*)<\/string>$/m);
|
|
7410
|
+
return m?.[1].replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
7411
|
+
}
|
|
7412
|
+
var domain = (s) => `gui/${s.uid}`;
|
|
7413
|
+
var target = (s) => `${domain(s)}/${LABEL}`;
|
|
7414
|
+
var loaded = (s) => s.launchctl(["print", target(s)]).status === 0;
|
|
7415
|
+
function disabled(s) {
|
|
7416
|
+
return new RegExp(`"${LABEL.replaceAll(".", "\\.")}" => (true|disabled)`).test(s.launchctl(["print-disabled", domain(s)]).stdout);
|
|
7417
|
+
}
|
|
7418
|
+
async function configureLaunchAgent(s, mode, requested) {
|
|
7419
|
+
if (s.uid === undefined)
|
|
7420
|
+
return { status: "unavailable" };
|
|
7421
|
+
const path2 = plistPath(s);
|
|
7422
|
+
const previous = readGenerated(path2, MARKER2);
|
|
7423
|
+
if (previous === null)
|
|
7424
|
+
return { status: "foreign", unit: path2 };
|
|
7425
|
+
if (mode === "restart" && previous === undefined)
|
|
7426
|
+
return { status: "absent" };
|
|
7427
|
+
const sdk = sdkPackages(s.root);
|
|
7428
|
+
const kept = plistWorkspace(previous);
|
|
7429
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : kept);
|
|
7430
|
+
if (workspace !== null && !sdk)
|
|
7431
|
+
return { status: "missing-pi-sdk" };
|
|
7432
|
+
const refused2 = await settle(s, kept);
|
|
7433
|
+
if (refused2)
|
|
7434
|
+
return { ...refused2, unit: path2 };
|
|
7435
|
+
if (loaded(s))
|
|
7436
|
+
s.launchctl(["bootout", target(s)]);
|
|
7437
|
+
write(path2, buildPlist({
|
|
7438
|
+
node: s.node,
|
|
7439
|
+
cli: join13(s.root, "src", "service-cli.mjs"),
|
|
7440
|
+
workspace,
|
|
7441
|
+
serverConfig: serverConfigPath(s.home),
|
|
7442
|
+
environment: serviceEnvironment(s),
|
|
7443
|
+
log: join13(s.home, "Library", "Logs", "cadence.log")
|
|
7444
|
+
}));
|
|
7445
|
+
const result = {
|
|
7446
|
+
unit: path2,
|
|
7447
|
+
workspace,
|
|
7448
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
7449
|
+
};
|
|
7450
|
+
if (mode === "install")
|
|
7451
|
+
s.launchctl(["enable", target(s)]);
|
|
7452
|
+
else if (disabled(s))
|
|
7453
|
+
return { status: "disabled", ...result };
|
|
7454
|
+
if (workspace !== null && !regularFile(join13(agentDirectory(s), "auth.json")))
|
|
7455
|
+
return { status: "waiting-for-pi-login", ...result };
|
|
7456
|
+
const started = s.launchctl(["bootstrap", domain(s), path2]).status === 0;
|
|
7457
|
+
return { status: started ? "started" : "failed", ...result };
|
|
7458
|
+
}
|
|
7459
|
+
async function stopLaunchAgent(s) {
|
|
7460
|
+
const path2 = plistPath(s);
|
|
7461
|
+
const previous = readGenerated(path2, MARKER2);
|
|
7462
|
+
if (previous === null)
|
|
7463
|
+
return { status: "foreign", unit: path2 };
|
|
7464
|
+
if (previous === undefined)
|
|
7465
|
+
return { status: "absent" };
|
|
7466
|
+
const workspace = plistWorkspace(previous);
|
|
7467
|
+
const refused2 = await settle(s, workspace);
|
|
7468
|
+
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
7469
|
+
}
|
|
7470
|
+
async function removeLaunchAgent(s) {
|
|
7471
|
+
const path2 = plistPath(s);
|
|
7472
|
+
const previous = readGenerated(path2, MARKER2);
|
|
7473
|
+
if (previous === null)
|
|
7474
|
+
return { status: "foreign", unit: path2 };
|
|
7475
|
+
if (previous === undefined)
|
|
7476
|
+
return { status: "absent" };
|
|
7477
|
+
const refused2 = await settle(s, plistWorkspace(previous));
|
|
7478
|
+
if (refused2)
|
|
7479
|
+
return { ...refused2, unit: path2 };
|
|
7480
|
+
if (s.uid !== undefined && loaded(s))
|
|
7481
|
+
s.launchctl(["bootout", target(s)]);
|
|
7482
|
+
rmSync2(path2);
|
|
7483
|
+
return { status: "removed", unit: path2 };
|
|
7484
|
+
}
|
|
7485
|
+
function launchAgentStatus(s) {
|
|
7486
|
+
const path2 = plistPath(s);
|
|
7487
|
+
const previous = readGenerated(path2, MARKER2);
|
|
7488
|
+
if (previous === null)
|
|
7489
|
+
return { status: "foreign", unit: path2 };
|
|
7490
|
+
if (previous === undefined)
|
|
7491
|
+
return { status: "absent" };
|
|
7492
|
+
const printed = s.uid === undefined ? undefined : s.launchctl(["print", target(s)]);
|
|
7493
|
+
return {
|
|
7494
|
+
status: printed?.status !== 0 ? "unloaded" : /^\s*state = running$/m.test(printed.stdout) ? "active" : "inactive",
|
|
7495
|
+
enabled: s.uid !== undefined && disabled(s) ? "disabled" : "enabled",
|
|
7496
|
+
unit: path2,
|
|
7497
|
+
workspace: plistWorkspace(previous)
|
|
6795
7498
|
};
|
|
6796
|
-
walk(files.directory, 0);
|
|
6797
|
-
files.assertFormat();
|
|
6798
|
-
return digest.digest("hex");
|
|
6799
7499
|
}
|
|
6800
7500
|
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
return files;
|
|
6812
|
-
}
|
|
6813
|
-
assertCommitted(fact) {
|
|
6814
|
-
const assert = this.committed.get(fact);
|
|
6815
|
-
if (!assert)
|
|
6816
|
-
throw new ContractError("conflict");
|
|
6817
|
-
assert();
|
|
6818
|
-
}
|
|
6819
|
-
async inspect(epoch) {
|
|
6820
|
-
decodeId(epoch);
|
|
6821
|
-
const files = this.files(), before = digestHistory(files), state = emptyScan();
|
|
6822
|
-
const auth = new Set, processes = new Set, publication = new Set, unfinished = new Set, metadata = new Set, runs = new Map;
|
|
6823
|
-
for (const batch of files.scan(state))
|
|
6824
|
-
for (const row of batch.rows) {
|
|
6825
|
-
const v = row.value;
|
|
6826
|
-
if (v.kind === "recovery") {
|
|
6827
|
-
auth.clear();
|
|
6828
|
-
processes.clear();
|
|
6829
|
-
publication.clear();
|
|
6830
|
-
unfinished.clear();
|
|
6831
|
-
metadata.clear();
|
|
6832
|
-
runs.clear();
|
|
6833
|
-
}
|
|
6834
|
-
if (v.kind === "admission" && v.intent.identity.kind === "action") {
|
|
6835
|
-
if (v.intent.runId) {
|
|
6836
|
-
if (runs.size >= LIMITS.episodes || runs.has(v.intent.runId))
|
|
6837
|
-
throw new ContractError("corrupt-selected");
|
|
6838
|
-
runs.set(v.intent.runId, new RunState(v.intent.runId));
|
|
6839
|
-
} else
|
|
6840
|
-
metadata.add(v.intent.operationId);
|
|
6841
|
-
}
|
|
6842
|
-
for (const [id, state2] of runs) {
|
|
6843
|
-
state2.apply(row, batch.marker.last);
|
|
6844
|
-
if (state2.drained)
|
|
6845
|
-
runs.delete(id);
|
|
6846
|
-
}
|
|
6847
|
-
if (v.kind === "capture" || v.kind === "owner")
|
|
6848
|
-
for (const e of v.events) {
|
|
6849
|
-
if (e.kind === "capture" && e.value.event.kind === "detail-begin" && v.kind === "capture")
|
|
6850
|
-
unfinished.add(`worker/${v.link.childRunId}/${e.value.event.detailId}`);
|
|
6851
|
-
if (e.kind === "owner" && e.value.kind === "content-begin" && v.kind === "owner")
|
|
6852
|
-
unfinished.add(`owner/${v.link.operationId}/${e.value.detailId}`);
|
|
6853
|
-
if (e.kind === "end")
|
|
6854
|
-
unfinished.delete(`${e.detail.origin.kind}/${e.detail.origin.id}/${e.detail.detailId}`);
|
|
6855
|
-
}
|
|
6856
|
-
if (v.kind === "auth-preparation")
|
|
6857
|
-
auth.add(v.intent.operationId);
|
|
6858
|
-
if (v.kind === "auth-settlement")
|
|
6859
|
-
auth.delete(v.result.operationId);
|
|
6860
|
-
if (v.kind === "launch")
|
|
6861
|
-
processes.add(v.link.childRunId);
|
|
6862
|
-
if (v.kind === "settlement" && v.completion.native.closure !== "unconfirmed")
|
|
6863
|
-
processes.delete(v.link.childRunId);
|
|
6864
|
-
if (v.kind === "owner") {
|
|
6865
|
-
for (const e of v.events)
|
|
6866
|
-
if (e.kind === "owner" && e.value.kind === "fact") {
|
|
6867
|
-
const f = e.value.fact;
|
|
6868
|
-
if (f.kind === "check-start")
|
|
6869
|
-
processes.add(f.checkId);
|
|
6870
|
-
if (f.kind === "check-settled" && f.native.closure !== "unconfirmed")
|
|
6871
|
-
processes.delete(f.checkId);
|
|
6872
|
-
if (f.kind === "publication-intent")
|
|
6873
|
-
publication.add(f.publicationId);
|
|
6874
|
-
if (f.kind === "publication-settled") {
|
|
6875
|
-
if (f.status === "published") {
|
|
6876
|
-
publication.delete(f.publicationId);
|
|
6877
|
-
metadata.delete(v.link.operationId);
|
|
6878
|
-
} else
|
|
6879
|
-
publication.add(f.publicationId);
|
|
6880
|
-
}
|
|
6881
|
-
}
|
|
6882
|
-
}
|
|
6883
|
-
}
|
|
6884
|
-
if (before !== digestHistory(files))
|
|
6885
|
-
throw new ContractError("conflict");
|
|
6886
|
-
return {
|
|
6887
|
-
digest: before,
|
|
6888
|
-
processes: processes.size ? "unknown" : "externally-resolved",
|
|
6889
|
-
credentials: auth.size ? "unknown" : "independently-resolved",
|
|
6890
|
-
storage: state.tail || publication.size || unfinished.size || metadata.size || runs.size ? "unknown" : "independently-resolved"
|
|
6891
|
-
};
|
|
6892
|
-
}
|
|
6893
|
-
async append(confirmation, observeWrite) {
|
|
6894
|
-
if (typeof confirmation.assertLocalAuthority !== "function" || confirmation.acknowledgeEscapedProcesses !== true || confirmation.processes !== "externally-resolved" || confirmation.credentials !== "independently-resolved" || confirmation.storage !== "independently-resolved")
|
|
6895
|
-
throw new ContractError("forbidden");
|
|
6896
|
-
const expected = structuredClone(confirmation.expected), files = this.files();
|
|
6897
|
-
files.assertWritable();
|
|
6898
|
-
if (!expected.history || expected.owner !== "absent")
|
|
6899
|
-
throw new ContractError("quarantine");
|
|
6900
|
-
const current = await inspectRecovery(this.coordination, this);
|
|
6901
|
-
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
6902
|
-
throw new ContractError("conflict");
|
|
6903
|
-
const state = files.inspect(), recoveryId = randomUUID7();
|
|
6904
|
-
const bridge = {
|
|
6905
|
-
kind: "recovery",
|
|
6906
|
-
recoveryId,
|
|
6907
|
-
epoch: expected.epoch,
|
|
6908
|
-
coordinationDigest: expected.coordinationDigest,
|
|
6909
|
-
historyDigest: expected.history.digest,
|
|
6910
|
-
previous: {
|
|
6911
|
-
segment: state.segment,
|
|
6912
|
-
bytes: state.bytes,
|
|
6913
|
-
sha256: state.identity ?? hash(""),
|
|
6914
|
-
committedBytes: state.committedBytes ?? state.bytes,
|
|
6915
|
-
committedSeq: state.committedSeq,
|
|
6916
|
-
highSeq: state.highSeq ?? state.committedSeq
|
|
6917
|
-
},
|
|
6918
|
-
acknowledgments: { processes: true, credentials: true, storage: true }
|
|
6919
|
-
};
|
|
6920
|
-
const path2 = files.segmentPath(state.segment + 1);
|
|
6921
|
-
const assertCurrent = async () => {
|
|
6922
|
-
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
6923
|
-
const selected2 = await inspectRecovery(this.coordination);
|
|
6924
|
-
if (selected2.owner !== "absent")
|
|
6925
|
-
throw new ContractError("quarantine");
|
|
6926
|
-
if (selected2.epoch !== expected.epoch || selected2.coordinationDigest !== expected.coordinationDigest || digestHistory(files, path2) !== expected.history?.digest || this.coordination.recoverySnapshot().digest !== expected.coordinationDigest)
|
|
6927
|
-
throw new ContractError("conflict");
|
|
6928
|
-
};
|
|
6929
|
-
await confirmation.assertLocalAuthority(structuredClone(expected));
|
|
6930
|
-
if (digestHistory(files) !== expected.history.digest)
|
|
6931
|
-
throw new ContractError("conflict");
|
|
6932
|
-
const row = {
|
|
6933
|
-
kind: "record",
|
|
6934
|
-
id: randomUUID7(),
|
|
6935
|
-
seq: String(BigInt(bridge.previous.highSeq) + 1n),
|
|
6936
|
-
value: bridge
|
|
6937
|
-
};
|
|
6938
|
-
let quota = LIMITS.checkoutQuotaBytes;
|
|
6939
|
-
for (const batch of files.scan())
|
|
6940
|
-
for (const row2 of batch.rows)
|
|
6941
|
-
if (row2.value.kind === "admission" && row2.value.policy)
|
|
6942
|
-
quota = row2.value.policy.quotaBytes;
|
|
6943
|
-
const bytes = Buffer.byteLength(files.encodeBatch(state, [row]).body + files.encodeBatch(state, [row]).trailer);
|
|
6944
|
-
if (files.usage() + bytes > quota || files.freeBytes() < LIMITS.freeSpaceFloorBytes + bytes)
|
|
6945
|
-
throw new ContractError("recording-exhausted");
|
|
6946
|
-
const after = await files.appendRecovery(state, row, assertCurrent, observeWrite);
|
|
6947
|
-
const assert = () => {
|
|
6948
|
-
files.assertWritable();
|
|
6949
|
-
files.assertRecoveryIdle();
|
|
6950
|
-
files.files.assert(path2, after.snapshot, LIMITS.segmentBytes);
|
|
6951
|
-
if (digestHistory(files, path2) !== expected.history?.digest)
|
|
6952
|
-
throw new ContractError("conflict");
|
|
6953
|
-
};
|
|
6954
|
-
assert();
|
|
6955
|
-
const fact = {
|
|
6956
|
-
recoveryId,
|
|
6957
|
-
committedSeq: after.state.committedSeq,
|
|
6958
|
-
epoch: expected.epoch,
|
|
6959
|
-
coordinationDigest: expected.coordinationDigest,
|
|
6960
|
-
historyDigest: expected.history.digest,
|
|
6961
|
-
segmentIdentity: after.state.identity ?? ""
|
|
6962
|
-
};
|
|
6963
|
-
this.committed.set(fact, assert);
|
|
6964
|
-
return fact;
|
|
6965
|
-
}
|
|
7501
|
+
// src/service/login-run.ts
|
|
7502
|
+
import { rmSync as rmSync3 } from "node:fs";
|
|
7503
|
+
import { dirname as dirname12, isAbsolute as isAbsolute7, join as join14 } from "node:path";
|
|
7504
|
+
var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
7505
|
+
var APPROVED_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run";
|
|
7506
|
+
var RUN_VALUE = "Cadence";
|
|
7507
|
+
var MARKER3 = "' Generated by @abelxiaoxing/cadence";
|
|
7508
|
+
function launcherPath(s) {
|
|
7509
|
+
const local = s.env.LOCALAPPDATA;
|
|
7510
|
+
return join14(local && isAbsolute7(local) ? local : join14(s.home, "AppData", "Local"), "cadence-server", "cadence.vbs");
|
|
6966
7511
|
}
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
throw new ContractError("forbidden");
|
|
7512
|
+
var literal2 = (value) => `"${checked(value).replaceAll('"', '""')}"`;
|
|
7513
|
+
function argument2(value) {
|
|
7514
|
+
if (checked(value).includes('"'))
|
|
7515
|
+
throw new ContractError("invalid");
|
|
7516
|
+
return `"${value}"`;
|
|
6973
7517
|
}
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
[
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
"credentials"
|
|
6988
|
-
],
|
|
6989
|
-
[
|
|
6990
|
-
"Confirm independent storage inspection; preserve damaged bytes and unknown outcomes. Type storage",
|
|
6991
|
-
"storage"
|
|
6992
|
-
],
|
|
6993
|
-
[
|
|
6994
|
-
`Release ONLY the displayed unchanged service-owned fences. Type resolve ${expected.epoch}`,
|
|
6995
|
-
`resolve ${expected.epoch}`
|
|
6996
|
-
]
|
|
6997
|
-
];
|
|
6998
|
-
await new Promise((resolve7, reject) => {
|
|
6999
|
-
let index = 0, line = "", done = false;
|
|
7000
|
-
const raw = process.stdin.isRaw;
|
|
7001
|
-
const finish = (ok) => {
|
|
7002
|
-
if (done)
|
|
7003
|
-
return;
|
|
7004
|
-
done = true;
|
|
7005
|
-
clearTimeout(timer);
|
|
7006
|
-
process.stdin.removeListener("data", input);
|
|
7007
|
-
process.stdin.removeListener("end", fail);
|
|
7008
|
-
process.stdin.removeListener("error", fail);
|
|
7009
|
-
process.stdin.setRawMode(raw);
|
|
7010
|
-
process.stdin.pause();
|
|
7011
|
-
process.stdout.write(`\x1B[?2004l
|
|
7012
|
-
`);
|
|
7013
|
-
if (ok)
|
|
7014
|
-
resolve7();
|
|
7015
|
-
else
|
|
7016
|
-
reject(new ContractError("forbidden"));
|
|
7017
|
-
};
|
|
7018
|
-
const fail = () => finish(false);
|
|
7019
|
-
const show = () => process.stdout.write(`${prompts[index]?.[0]}: `);
|
|
7020
|
-
const input = (chunk) => {
|
|
7021
|
-
if (performance.now() >= deadline || chunk.length > 128)
|
|
7022
|
-
return fail();
|
|
7023
|
-
for (let n = 0;n < chunk.length; n++) {
|
|
7024
|
-
const byte = chunk[n];
|
|
7025
|
-
if (byte === 13 || byte === 10) {
|
|
7026
|
-
if (n !== chunk.length - 1 || line !== prompts[index]?.[1])
|
|
7027
|
-
return fail();
|
|
7028
|
-
line = "";
|
|
7029
|
-
process.stdout.write(`
|
|
7518
|
+
function buildLauncher(u) {
|
|
7519
|
+
const command = `${argument2(u.node)} ${argument2(u.cli)} ${u.workspace === null ? "workbench start" : `start --workspace ${argument2(u.workspace)}`} --server-config ${argument2(u.serverConfig)}`;
|
|
7520
|
+
return [
|
|
7521
|
+
`${MARKER3}; \`cadence server install\` and \`restart\` rewrite this file.`,
|
|
7522
|
+
`' Started at login by ${RUN_KEY}\\${RUN_VALUE}; window style 0 hides the console.`,
|
|
7523
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
7524
|
+
...u.environment.length ? ['Set env = shell.Environment("Process")'] : [],
|
|
7525
|
+
...u.environment.map(([k, v]) => `env(${literal2(k)}) = ${literal2(v)}`),
|
|
7526
|
+
...u.workspace === null ? ["' Cadence workbench gateway; no project workspace"] : [],
|
|
7527
|
+
`shell.CurrentDirectory = ${literal2(u.workspace ?? dirname12(dirname12(u.cli)))}`,
|
|
7528
|
+
`shell.Run ${literal2(command)}, 0, False`,
|
|
7529
|
+
""
|
|
7530
|
+
].join(`\r
|
|
7030
7531
|
`);
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7532
|
+
}
|
|
7533
|
+
var utf16 = (text2) => Buffer.concat([Buffer.from([255, 254]), Buffer.from(text2, "utf16le")]);
|
|
7534
|
+
function launcherWorkspace(text2) {
|
|
7535
|
+
if (text2?.includes("' Cadence workbench gateway; no project workspace"))
|
|
7536
|
+
return null;
|
|
7537
|
+
return text2?.match(/^shell\.CurrentDirectory = "((?:[^"]|"")*)"\r?$/m)?.[1].replaceAll('""', '"');
|
|
7538
|
+
}
|
|
7539
|
+
var runEntry = (path2, s) => `"${wscript(s.env)}" "${path2}"`;
|
|
7540
|
+
var present = (s, key) => s.reg(["query", key, "/v", RUN_VALUE]).status === 0;
|
|
7541
|
+
function approvedOff(s) {
|
|
7542
|
+
const r = s.reg(["query", APPROVED_KEY, "/v", RUN_VALUE]);
|
|
7543
|
+
const first = r.status === 0 && /REG_BINARY\s+([0-9A-F]{2})/i.exec(r.stdout);
|
|
7544
|
+
return !!first && (Number.parseInt(first[1], 16) & 1) === 1;
|
|
7545
|
+
}
|
|
7546
|
+
async function configureLoginRun(s, mode, requested) {
|
|
7547
|
+
const path2 = launcherPath(s);
|
|
7548
|
+
const previous = readGenerated(path2, MARKER3);
|
|
7549
|
+
if (previous === null)
|
|
7550
|
+
return { status: "foreign", unit: path2 };
|
|
7551
|
+
if (previous === undefined && present(s, RUN_KEY))
|
|
7552
|
+
return { status: "foreign", unit: `${RUN_KEY}\\${RUN_VALUE}` };
|
|
7553
|
+
if (mode === "restart" && previous === undefined)
|
|
7554
|
+
return { status: "absent" };
|
|
7555
|
+
const sdk = sdkPackages(s.root);
|
|
7556
|
+
const kept = launcherWorkspace(previous);
|
|
7557
|
+
const { workspace, redirectedFrom } = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : kept);
|
|
7558
|
+
if (workspace !== null && !sdk)
|
|
7559
|
+
return { status: "missing-pi-sdk" };
|
|
7560
|
+
const refused2 = await settle(s, kept);
|
|
7561
|
+
if (refused2)
|
|
7562
|
+
return { ...refused2, unit: path2 };
|
|
7563
|
+
const environment = s.env.PI_CODING_AGENT_DIR ? [["PI_CODING_AGENT_DIR", agentDirectory(s)]] : [];
|
|
7564
|
+
write(path2, utf16(buildLauncher({
|
|
7565
|
+
node: s.node,
|
|
7566
|
+
cli: join14(s.root, "src", "service-cli.mjs"),
|
|
7567
|
+
workspace,
|
|
7568
|
+
serverConfig: serverConfigPath(s.home),
|
|
7569
|
+
environment
|
|
7570
|
+
})));
|
|
7571
|
+
const result = {
|
|
7572
|
+
unit: path2,
|
|
7573
|
+
workspace,
|
|
7574
|
+
...redirectedFrom ? { redirectedFrom } : {}
|
|
7052
7575
|
};
|
|
7576
|
+
if (mode === "install") {
|
|
7577
|
+
const entry = ["/v", RUN_VALUE, "/t", "REG_SZ", "/d", runEntry(path2, s)];
|
|
7578
|
+
if (s.reg(["add", RUN_KEY, ...entry, "/f"]).status !== 0)
|
|
7579
|
+
return { status: "failed", ...result };
|
|
7580
|
+
s.reg(["delete", APPROVED_KEY, "/v", RUN_VALUE, "/f"]);
|
|
7581
|
+
}
|
|
7582
|
+
if (workspace !== null && !regularFile(join14(agentDirectory(s), "auth.json")))
|
|
7583
|
+
return { status: "waiting-for-pi-login", ...result };
|
|
7584
|
+
return { status: s.launch(path2) ? "started" : "failed", ...result };
|
|
7053
7585
|
}
|
|
7054
|
-
async function
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7586
|
+
async function stopLoginRun(s) {
|
|
7587
|
+
const path2 = launcherPath(s);
|
|
7588
|
+
const previous = readGenerated(path2, MARKER3);
|
|
7589
|
+
if (previous === null)
|
|
7590
|
+
return { status: "foreign", unit: path2 };
|
|
7591
|
+
if (previous === undefined)
|
|
7592
|
+
return { status: "absent" };
|
|
7593
|
+
const workspace = launcherWorkspace(previous);
|
|
7594
|
+
const refused2 = await settle(s, workspace);
|
|
7595
|
+
return { ...refused2 ?? { status: "stopped", workspace }, unit: path2 };
|
|
7596
|
+
}
|
|
7597
|
+
async function removeLoginRun(s) {
|
|
7598
|
+
const path2 = launcherPath(s);
|
|
7599
|
+
const previous = readGenerated(path2, MARKER3);
|
|
7600
|
+
if (previous === null)
|
|
7601
|
+
return { status: "foreign", unit: path2 };
|
|
7602
|
+
if (previous === undefined)
|
|
7603
|
+
return { status: "absent" };
|
|
7604
|
+
const refused2 = await settle(s, launcherWorkspace(previous));
|
|
7605
|
+
if (refused2)
|
|
7606
|
+
return { ...refused2, unit: path2 };
|
|
7607
|
+
for (const key of [RUN_KEY, APPROVED_KEY])
|
|
7608
|
+
if (present(s, key))
|
|
7609
|
+
s.reg(["delete", key, "/v", RUN_VALUE, "/f"]);
|
|
7610
|
+
rmSync3(path2);
|
|
7611
|
+
return { status: "removed", unit: path2 };
|
|
7612
|
+
}
|
|
7613
|
+
async function loginRunStatus(s) {
|
|
7614
|
+
const path2 = launcherPath(s);
|
|
7615
|
+
const previous = readGenerated(path2, MARKER3);
|
|
7616
|
+
if (previous === null)
|
|
7617
|
+
return { status: "foreign", unit: path2 };
|
|
7618
|
+
if (previous === undefined)
|
|
7619
|
+
return { status: "absent" };
|
|
7620
|
+
const workspace = launcherWorkspace(previous);
|
|
7621
|
+
const owner = workspace !== undefined ? await s.ownerStatus(workspace, serverConfigPath(s.home)) : "unavailable";
|
|
7622
|
+
return {
|
|
7623
|
+
status: owner === "alive" ? "active" : "inactive",
|
|
7624
|
+
enabled: present(s, RUN_KEY) && !approvedOff(s) ? "enabled" : "disabled",
|
|
7625
|
+
unit: path2,
|
|
7626
|
+
workspace
|
|
7067
7627
|
};
|
|
7068
|
-
if (operation === "inspect")
|
|
7069
|
-
return view;
|
|
7070
|
-
process.stdout.write(`${JSON.stringify(view)}
|
|
7071
|
-
`);
|
|
7072
|
-
if (expected.owner !== "absent")
|
|
7073
|
-
throw new ContractError("quarantine");
|
|
7074
|
-
const assertFresh = await confirm(expected);
|
|
7075
|
-
return resolveRecovery(c, {
|
|
7076
|
-
expected,
|
|
7077
|
-
acknowledgeEscapedProcesses: true,
|
|
7078
|
-
processes: "externally-resolved",
|
|
7079
|
-
credentials: "independently-resolved",
|
|
7080
|
-
storage: "independently-resolved",
|
|
7081
|
-
assertLocalAuthority: (choice) => {
|
|
7082
|
-
assertFresh();
|
|
7083
|
-
if (JSON.stringify(choice) !== JSON.stringify(expected))
|
|
7084
|
-
throw new ContractError("stale");
|
|
7085
|
-
}
|
|
7086
|
-
}, history);
|
|
7087
7628
|
}
|
|
7088
7629
|
|
|
7089
7630
|
// src/service/system-service.ts
|
|
7090
|
-
import { existsSync as existsSync2, realpathSync as
|
|
7091
|
-
import { isAbsolute as isAbsolute8, join as
|
|
7631
|
+
import { existsSync as existsSync2, realpathSync as realpathSync4, rmSync as rmSync4 } from "node:fs";
|
|
7632
|
+
import { isAbsolute as isAbsolute8, join as join15 } from "node:path";
|
|
7092
7633
|
var MARKER4 = "# Generated by @abelxiaoxing/cadence";
|
|
7093
7634
|
function boundary(s) {
|
|
7094
7635
|
if (s.platform !== "linux" || s.scope !== "system")
|
|
@@ -7130,8 +7671,8 @@ function sameStateEnvironment(s, previous) {
|
|
|
7130
7671
|
return false;
|
|
7131
7672
|
values.set(key, value.slice(at + 1).replaceAll("%%", "%"));
|
|
7132
7673
|
}
|
|
7133
|
-
const
|
|
7134
|
-
return values.get("HOME") === s.home && values.get("XDG_STATE_HOME") === (
|
|
7674
|
+
const state2 = s.env.XDG_STATE_HOME;
|
|
7675
|
+
return values.get("HOME") === s.home && values.get("XDG_STATE_HOME") === (state2 && isAbsolute8(state2) ? state2 : undefined);
|
|
7135
7676
|
} catch {
|
|
7136
7677
|
return false;
|
|
7137
7678
|
}
|
|
@@ -7169,12 +7710,12 @@ async function configureSystemService(s, mode, requested) {
|
|
|
7169
7710
|
const selected2 = selectWorkspace(s, requested, mode === "install" && requested === undefined ? null : unitWorkspace(previous), false);
|
|
7170
7711
|
if (selected2.workspace !== null && !sdk)
|
|
7171
7712
|
return { status: "missing-pi-sdk", scope: "system" };
|
|
7172
|
-
const selectedReal = selected2.workspace && existsSync2(selected2.workspace) ?
|
|
7713
|
+
const selectedReal = selected2.workspace && existsSync2(selected2.workspace) ? realpathSync4(selected2.workspace) : undefined;
|
|
7173
7714
|
const agentDir = agentDirectory(s);
|
|
7174
7715
|
const content = buildUnit({
|
|
7175
7716
|
system: true,
|
|
7176
7717
|
node: s.node,
|
|
7177
|
-
cli:
|
|
7718
|
+
cli: join15(s.root, "src", "service-cli.mjs"),
|
|
7178
7719
|
workspace: selected2.workspace,
|
|
7179
7720
|
serverConfig: serviceConfigPath(s),
|
|
7180
7721
|
agentDir,
|
|
@@ -7206,7 +7747,7 @@ async function configureSystemService(s, mode, requested) {
|
|
|
7206
7747
|
return { status: "foreign", ...result };
|
|
7207
7748
|
if (selectedReal && selected2.workspace) {
|
|
7208
7749
|
try {
|
|
7209
|
-
if (
|
|
7750
|
+
if (realpathSync4(selected2.workspace) !== selectedReal)
|
|
7210
7751
|
return { status: "workspace-changed", ...result };
|
|
7211
7752
|
} catch {
|
|
7212
7753
|
return { status: "workspace-changed", ...result };
|
|
@@ -7223,7 +7764,7 @@ async function configureSystemService(s, mode, requested) {
|
|
|
7223
7764
|
return { status: "failed", ...result, ...selected2 };
|
|
7224
7765
|
if (mode === "install" && s.systemctl(["enable", s.unit]).status !== 0)
|
|
7225
7766
|
return { status: "failed", ...result, ...selected2 };
|
|
7226
|
-
if (selected2.workspace !== null && !regularFile(
|
|
7767
|
+
if (selected2.workspace !== null && !regularFile(join15(agentDir, "auth.json")))
|
|
7227
7768
|
return { status: "waiting-for-pi-login", ...result, ...selected2 };
|
|
7228
7769
|
s.systemctl(["reset-failed", s.unit]);
|
|
7229
7770
|
return {
|
|
@@ -7355,6 +7896,13 @@ function nativeCall(c, operation, reveal = false, controlBody) {
|
|
|
7355
7896
|
}
|
|
7356
7897
|
var pause = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
7357
7898
|
async function serviceStatus(c, diagnose = false) {
|
|
7899
|
+
const registryLock = c.registryLockDiagnostic?.();
|
|
7900
|
+
if (registryLock)
|
|
7901
|
+
return {
|
|
7902
|
+
status: "unavailable",
|
|
7903
|
+
...registryLock,
|
|
7904
|
+
recovery: "cadence recover inspect --workspace <project> [--state-home <state-home>]"
|
|
7905
|
+
};
|
|
7358
7906
|
const observed2 = c.inspect();
|
|
7359
7907
|
if (observed2.status === "absent" || observed2.status === "stopped")
|
|
7360
7908
|
return observed2;
|
|
@@ -7378,7 +7926,12 @@ async function stopService(c) {
|
|
|
7378
7926
|
}
|
|
7379
7927
|
const deadline = performance.now() + LIMITS.stopWaitMs;
|
|
7380
7928
|
while (performance.now() < deadline) {
|
|
7381
|
-
|
|
7929
|
+
let observed2;
|
|
7930
|
+
try {
|
|
7931
|
+
observed2 = c.inspect();
|
|
7932
|
+
} catch {
|
|
7933
|
+
observed2 = { status: "unavailable" };
|
|
7934
|
+
}
|
|
7382
7935
|
if ("epoch" in observed2 && observed2.epoch === epoch && observed2.status === "stopped")
|
|
7383
7936
|
return { status: "stopped", epoch };
|
|
7384
7937
|
if (observed2.status === "quarantined")
|
|
@@ -7392,11 +7945,34 @@ async function stopService(c) {
|
|
|
7392
7945
|
function failedStatus(result) {
|
|
7393
7946
|
return !!(result && typeof result === "object" && ("status" in result) && ["unconfirmed", "unavailable", "quarantined", "refused"].includes(String(result.status)));
|
|
7394
7947
|
}
|
|
7948
|
+
function managementExitCode(result, foreground = false) {
|
|
7949
|
+
if (foreground) {
|
|
7950
|
+
try {
|
|
7951
|
+
decodeStartupReport(result);
|
|
7952
|
+
return 78;
|
|
7953
|
+
} catch {}
|
|
7954
|
+
}
|
|
7955
|
+
return 1;
|
|
7956
|
+
}
|
|
7957
|
+
function observedServerResult(result, service) {
|
|
7958
|
+
return {
|
|
7959
|
+
...result,
|
|
7960
|
+
service,
|
|
7961
|
+
...result.status === "started" && service !== "alive" ? {
|
|
7962
|
+
managerStatus: result.status,
|
|
7963
|
+
status: service === "quarantined" ? "quarantined" : "unconfirmed"
|
|
7964
|
+
} : {}
|
|
7965
|
+
};
|
|
7966
|
+
}
|
|
7395
7967
|
function serverConfigHint(error) {
|
|
7396
7968
|
return `Cadence changed nothing: ${error.path} ${error.reason}.
|
|
7397
7969
|
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.
|
|
7398
7970
|
`;
|
|
7399
7971
|
}
|
|
7972
|
+
function registryGuidance(workspace, stateHome) {
|
|
7973
|
+
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.
|
|
7974
|
+
`;
|
|
7975
|
+
}
|
|
7400
7976
|
function startupGuidance(result, input) {
|
|
7401
7977
|
let report;
|
|
7402
7978
|
try {
|
|
@@ -7404,6 +7980,8 @@ function startupGuidance(result, input) {
|
|
|
7404
7980
|
} catch {
|
|
7405
7981
|
return "";
|
|
7406
7982
|
}
|
|
7983
|
+
if (report.error === "registry-locked")
|
|
7984
|
+
return registryGuidance(input.workspace, input.stateHome);
|
|
7407
7985
|
const agent = input.config.agentDir;
|
|
7408
7986
|
const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
|
|
7409
7987
|
`;
|
|
@@ -7415,10 +7993,10 @@ function startupGuidance(result, input) {
|
|
|
7415
7993
|
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
7416
7994
|
`;
|
|
7417
7995
|
case "pi-auth":
|
|
7418
|
-
return `${
|
|
7996
|
+
return `${join16(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
7419
7997
|
`;
|
|
7420
7998
|
case "pi-models":
|
|
7421
|
-
return `${
|
|
7999
|
+
return `${join16(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.
|
|
7422
8000
|
`;
|
|
7423
8001
|
case "pi-runtime":
|
|
7424
8002
|
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
@@ -7431,6 +8009,15 @@ ${inspect}` : "";
|
|
|
7431
8009
|
async function startService(options, foreground, hostUrl) {
|
|
7432
8010
|
const network = networkSnapshot(process.env);
|
|
7433
8011
|
const env = hostEnvironment(process.env);
|
|
8012
|
+
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
8013
|
+
if (c.registryLockDiagnostic()) {
|
|
8014
|
+
process.stderr.write(registryGuidance(c.store.root, c.stateHome));
|
|
8015
|
+
return {
|
|
8016
|
+
status: "refused",
|
|
8017
|
+
error: "registry-locked",
|
|
8018
|
+
stage: "service"
|
|
8019
|
+
};
|
|
8020
|
+
}
|
|
7434
8021
|
let input;
|
|
7435
8022
|
try {
|
|
7436
8023
|
input = decodeHostInput(selectStart(options, process.env));
|
|
@@ -7443,7 +8030,6 @@ async function startService(options, foreground, hostUrl) {
|
|
|
7443
8030
|
`;
|
|
7444
8031
|
if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
|
|
7445
8032
|
throw new ContractError("oversize");
|
|
7446
|
-
const c = new ServiceCoordination(input.workspace, input.stateHome);
|
|
7447
8033
|
const existing = await serviceStatus(c);
|
|
7448
8034
|
if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
|
|
7449
8035
|
const running = readHostConfig(c);
|
|
@@ -7540,6 +8126,8 @@ async function launchHost(input, c, foreground, hostUrl, cwd, env, network) {
|
|
|
7540
8126
|
process.stdout.write(`${JSON.stringify(result)}
|
|
7541
8127
|
`);
|
|
7542
8128
|
await closure;
|
|
8129
|
+
if (failedStatus(result))
|
|
8130
|
+
process.exitCode = managementExitCode(result, true);
|
|
7543
8131
|
return null;
|
|
7544
8132
|
}
|
|
7545
8133
|
return result;
|
|
@@ -7626,7 +8214,7 @@ async function serverAction(s, action, requested, linger) {
|
|
|
7626
8214
|
}
|
|
7627
8215
|
}
|
|
7628
8216
|
function serverGuidance(s, action, result, link) {
|
|
7629
|
-
const cli = piUserInstall(s) ? `node ${JSON.stringify(
|
|
8217
|
+
const cli = piUserInstall(s) ? `node ${JSON.stringify(join16(s.root, "src", "service-cli.mjs"))}` : "cadence";
|
|
7630
8218
|
const note = result.redirectedFrom ? `Cadence workspace: ${result.workspace} (${result.redirectedFrom} contains Cadence's private state)
|
|
7631
8219
|
` : "";
|
|
7632
8220
|
return note + serverStatusGuidance(s, action, result, cli, link);
|
|
@@ -7658,6 +8246,11 @@ function managerDetail(s) {
|
|
|
7658
8246
|
}
|
|
7659
8247
|
function serverStatusGuidance(s, action, result, cli, link) {
|
|
7660
8248
|
const workspace = JSON.stringify(result.workspace ?? "");
|
|
8249
|
+
if (result.workspace === null && ["quarantined", "unconfirmed", "unavailable"].includes(result.status))
|
|
8250
|
+
return `Cadence workbench is not ready (${result.status}); no successful startup or Stop is inferred.
|
|
8251
|
+
Inspect locally: ${cli} workbench recover inspect --server-config ${JSON.stringify(serviceConfigPath(s))}
|
|
8252
|
+
Use the definition's same account and state home; eligible recovery requires recover resolve in a local terminal. Preserve locks and history.
|
|
8253
|
+
`;
|
|
7661
8254
|
if (result.workspace === null && result.status === "started")
|
|
7662
8255
|
return `Cadence unified workbench: ${link ?? result.endpoint ?? result.service ?? "unconfirmed"}
|
|
7663
8256
|
No default workspace was created. Projects are opened explicitly.
|
|
@@ -7688,7 +8281,7 @@ ${manage}
|
|
|
7688
8281
|
${linger}`;
|
|
7689
8282
|
}
|
|
7690
8283
|
case "waiting-for-pi-login":
|
|
7691
|
-
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${
|
|
8284
|
+
return `Cadence is registered for ${result.workspace}; it starts once Pi has ${join16(agentDirectory(s), "auth.json")}.
|
|
7692
8285
|
Log in to Pi, then: ${cli} server restart${flags}
|
|
7693
8286
|
${config}
|
|
7694
8287
|
${lingerGuidance(result.linger)}`;
|
|
@@ -7734,7 +8327,10 @@ ${lingerGuidance(result.linger)}`;
|
|
|
7734
8327
|
`;
|
|
7735
8328
|
case "unconfirmed":
|
|
7736
8329
|
case "quarantined":
|
|
7737
|
-
return `Cadence
|
|
8330
|
+
return `Cadence owner reports ${result.status}; successful startup or orderly Stop is not confirmed.
|
|
8331
|
+
Inspect: ${cli} diagnose --workspace ${workspace}
|
|
8332
|
+
Local recovery: ${cli} recover inspect --workspace ${workspace}
|
|
8333
|
+
Preserve uncertain state; recovery requires separate fresh terminal confirmation.
|
|
7738
8334
|
`;
|
|
7739
8335
|
case "failed":
|
|
7740
8336
|
return `Could not register or start Cadence's service; ${managerDetail(s)}.
|
|
@@ -7838,9 +8434,26 @@ async function serverMain(argv, hostUrl) {
|
|
|
7838
8434
|
}
|
|
7839
8435
|
let link;
|
|
7840
8436
|
if (result.status === "started" && result.workspace !== undefined) {
|
|
7841
|
-
|
|
8437
|
+
const workspace = result.workspace;
|
|
8438
|
+
const observation = await observeStarted(workspace, serviceConfigPath(s));
|
|
8439
|
+
result = {
|
|
8440
|
+
...observedServerResult(result, observation.service),
|
|
8441
|
+
...observation
|
|
8442
|
+
};
|
|
7842
8443
|
if (result.service === "alive")
|
|
7843
|
-
link = await privateLink(
|
|
8444
|
+
link = await privateLink(workspace, undefined, serviceConfigPath(s));
|
|
8445
|
+
}
|
|
8446
|
+
if (action === "status" && result.workspace !== undefined && ["active", "activating", "failed"].includes(result.status)) {
|
|
8447
|
+
const target2 = result.workspace === null ? new GatewayStore(serviceConfigPath(s)) : new ServiceCoordination(result.workspace);
|
|
8448
|
+
const observed2 = await serviceStatus(target2);
|
|
8449
|
+
if (observed2 && typeof observed2 === "object" && "status" in observed2) {
|
|
8450
|
+
const service = String(observed2.status);
|
|
8451
|
+
result = {
|
|
8452
|
+
...result,
|
|
8453
|
+
service,
|
|
8454
|
+
...service === "quarantined" ? { managerStatus: result.status, status: service } : {}
|
|
8455
|
+
};
|
|
8456
|
+
}
|
|
7844
8457
|
}
|
|
7845
8458
|
process.stderr.write(serverGuidance(s, action, result, link));
|
|
7846
8459
|
process.stdout.write(`${JSON.stringify(result)}
|
|
@@ -7891,7 +8504,7 @@ async function projectMain(argv, hostUrl) {
|
|
|
7891
8504
|
mkdirSync6(options.workspace, { recursive: true });
|
|
7892
8505
|
effectiveWorkspace(options.workspace, options.stateHome);
|
|
7893
8506
|
const c = new ServiceCoordination(options.workspace, options.stateHome);
|
|
7894
|
-
const kept = readHostConfig(c);
|
|
8507
|
+
const kept = c.registryLockDiagnostic() ? undefined : readHostConfig(c);
|
|
7895
8508
|
if (!kept)
|
|
7896
8509
|
options.port = 0;
|
|
7897
8510
|
const result = await startService(options, false, hostUrl);
|
|
@@ -7913,6 +8526,8 @@ async function projectMain(argv, hostUrl) {
|
|
|
7913
8526
|
process.exitCode = 1;
|
|
7914
8527
|
}
|
|
7915
8528
|
async function managementMain(argv, hostUrl) {
|
|
8529
|
+
const foregroundStart = argv.includes("--foreground") && (argv[0] === "start" || argv[0] === "workbench" && argv[1] === "start");
|
|
8530
|
+
const recoveryCommand = argv[0] === "recover" || argv[0] === "workbench" && argv[1] === "recover";
|
|
7916
8531
|
try {
|
|
7917
8532
|
if (!process.versions.node || process.versions.bun)
|
|
7918
8533
|
throw new ContractError("unsupported-context");
|
|
@@ -7927,15 +8542,16 @@ async function managementMain(argv, hostUrl) {
|
|
|
7927
8542
|
"diagnose",
|
|
7928
8543
|
"stop",
|
|
7929
8544
|
"token-link",
|
|
7930
|
-
"token-rotate"
|
|
8545
|
+
"token-rotate",
|
|
8546
|
+
"recover"
|
|
7931
8547
|
].includes(command ?? "") && !argv.includes("--workspace")) {
|
|
7932
8548
|
await gatewayMain([command, ...argv], hostUrl);
|
|
7933
8549
|
return;
|
|
7934
8550
|
}
|
|
7935
8551
|
if (command === "--help" && !argv.length) {
|
|
7936
8552
|
process.stdout.write(`cadence start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
7937
|
-
Without --workspace these commands address the unified workbench
|
|
7938
|
-
cadence workbench start|status|diagnose|stop|token-link --reveal|token-rotate [--server-config PATH] [--state-home PATH]
|
|
8553
|
+
Without --workspace these commands (including recover) address the unified workbench, never cwd; no default project directory is created. Project recovery requires explicit --workspace PATH. Recovery checks the account-wide registry lock first.
|
|
8554
|
+
cadence workbench start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--server-config PATH] [--state-home PATH]
|
|
7939
8555
|
cadence server install [--workspace PATH] [--no-linger]|status|restart|stop|uninstall
|
|
7940
8556
|
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.
|
|
7941
8557
|
cadence project open [PATH] [--create] [--agent-dir PATH] [--state-home PATH]
|
|
@@ -8039,11 +8655,18 @@ Recovery resolve requires this local native terminal, exact unchanged dead-owner
|
|
|
8039
8655
|
process.stdout.write(`${JSON.stringify(result)}
|
|
8040
8656
|
`);
|
|
8041
8657
|
if (failedStatus(result))
|
|
8042
|
-
process.exitCode =
|
|
8043
|
-
} catch {
|
|
8044
|
-
process.stderr.write(
|
|
8658
|
+
process.exitCode = managementExitCode(result, foreground);
|
|
8659
|
+
} catch (error) {
|
|
8660
|
+
process.stderr.write(`${JSON.stringify({
|
|
8661
|
+
error: "service-refused",
|
|
8662
|
+
...recoveryCommand ? {
|
|
8663
|
+
code: error instanceof ContractError ? error.code : "unavailable",
|
|
8664
|
+
stage: "recovery",
|
|
8665
|
+
notice: "Use workbench recover for account/workbench state or explicit --workspace for an existing project; preserve state on refusal."
|
|
8666
|
+
} : {}
|
|
8667
|
+
})}
|
|
8045
8668
|
`);
|
|
8046
|
-
process.exitCode = 1;
|
|
8669
|
+
process.exitCode = foregroundStart && (error instanceof ContractError || error instanceof ServerConfigRefusal) ? 78 : 1;
|
|
8047
8670
|
}
|
|
8048
8671
|
}
|
|
8049
8672
|
|