@abelxiaoxing/cadence 2.0.13 → 2.0.15

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