@abelxiaoxing/cadence 2.0.14 → 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.
@@ -327,6 +327,7 @@ var ERROR_CODES = [
327
327
  "quarantine",
328
328
  "capacity",
329
329
  "unavailable",
330
+ "registry-locked",
330
331
  "auth-unavailable",
331
332
  "recording-exhausted",
332
333
  "receipt-inconclusive"
@@ -3517,6 +3518,21 @@ function names(path2, allowed) {
3517
3518
  dir.closeSync();
3518
3519
  }
3519
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
+ ];
3520
3536
  function recoveryOwnerStatus(pid, host) {
3521
3537
  if (host !== hostname2())
3522
3538
  return "ambiguous";
@@ -3568,11 +3584,7 @@ class ServiceCoordination {
3568
3584
  registry() {
3569
3585
  if (!this.files.directory(this.registryDirectory))
3570
3586
  return;
3571
- names(this.registryDirectory, [
3572
- "format.json",
3573
- "roots.json",
3574
- "registry.lock"
3575
- ]);
3587
+ names(this.registryDirectory, registryNames);
3576
3588
  const format = this.files.read(this.registryPath("format.json"));
3577
3589
  const snapshot = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
3578
3590
  if (!format || !snapshot)
@@ -3597,10 +3609,10 @@ class ServiceCoordination {
3597
3609
  }
3598
3610
  };
3599
3611
  }
3600
- assertNamespace(walks) {
3601
- if (!this.files.directory(this.directory, false, walks))
3612
+ assertNamespace(walks, directory2 = this.directory) {
3613
+ if (!this.files.directory(directory2, false, walks))
3602
3614
  refused();
3603
- names(this.directory, [
3615
+ names(directory2, [
3604
3616
  "format.json",
3605
3617
  "config.json",
3606
3618
  "registration.json",
@@ -3612,15 +3624,16 @@ class ServiceCoordination {
3612
3624
  "history-v2",
3613
3625
  "conversations-v1"
3614
3626
  ]);
3615
- this.files.directory(join5(this.directory, "history-v2"), false, walks);
3627
+ this.files.directory(join5(directory2, "history-v2"), false, walks);
3616
3628
  }
3617
- selected() {
3629
+ selected(directory2 = this.directory, root = this.store.root, stoppedOnly = false) {
3618
3630
  const walks = new DirectoryWalks;
3619
- if (!this.files.directory(this.directory, false, walks))
3631
+ if (!this.files.directory(directory2, false, walks))
3620
3632
  return;
3621
- this.assertNamespace(walks);
3622
- const manifest = this.files.read(this.path("format.json"), undefined, walks);
3623
- 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);
3624
3637
  if (!manifest || !config)
3625
3638
  refused();
3626
3639
  const f = closed(json(manifest), [
@@ -3630,20 +3643,22 @@ class ServiceCoordination {
3630
3643
  "configIdentity"
3631
3644
  ]);
3632
3645
  literal(f.schema, "cadence-service-format/1");
3633
- literal(f.root, this.store.root);
3646
+ literal(f.root, root);
3634
3647
  literal(f.stateHome, this.stateHome);
3635
3648
  literal(f.configIdentity, config.identity);
3636
3649
  const c = decodeOwnerConfig(json(config));
3637
- const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
3650
+ const hostConfig = this.files.read(path2("host-config.json"), undefined, walks);
3638
3651
  if (hostConfig && hostConfig.identity !== c.policyIdentity)
3639
3652
  refused();
3640
- const registration = this.files.read(this.path("registration.json"), undefined, walks);
3641
- const owner = this.files.read(this.path("owner.lock"), undefined, walks);
3642
- const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
3643
- 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);
3644
3657
  const value = registration ? decodeRegistration(json(registration)) : undefined;
3645
- 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)))
3646
3659
  refused();
3660
+ if (stoppedOnly && (checkout || value && value.phase !== "stopped"))
3661
+ throw new ContractError("quarantine");
3647
3662
  if (value && value.phase !== "stopped") {
3648
3663
  if (!owner || !checkout || !capability)
3649
3664
  refused();
@@ -3723,18 +3738,21 @@ class ServiceCoordination {
3723
3738
  }
3724
3739
  withRegistry(operation) {
3725
3740
  this.files.directory(this.registryDirectory, true);
3726
- names(this.registryDirectory, [
3727
- "format.json",
3728
- "roots.json",
3729
- "registry.lock"
3730
- ]);
3741
+ names(this.registryDirectory, registryNames);
3731
3742
  const lockPath = this.registryPath("registry.lock");
3732
- const lock = this.files.create(lockPath, encoded({
3733
- schema: "cadence-service-registry-lock/1",
3734
- token: randomUUID3(),
3735
- pid: process.pid,
3736
- host: hostname2()
3737
- }));
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
+ }
3738
3756
  let dirty = false;
3739
3757
  try {
3740
3758
  let format = this.files.read(this.registryPath("format.json"));
@@ -3758,11 +3776,7 @@ class ServiceCoordination {
3758
3776
  }, () => {
3759
3777
  this.files.assert(lockPath, lock);
3760
3778
  this.files.assert(this.registryPath("format.json"), format);
3761
- names(this.registryDirectory, [
3762
- "format.json",
3763
- "roots.json",
3764
- "registry.lock"
3765
- ]);
3779
+ names(this.registryDirectory, registryNames);
3766
3780
  });
3767
3781
  this.files.remove(lockPath, lock);
3768
3782
  return result;
@@ -3894,6 +3908,154 @@ class ServiceCoordination {
3894
3908
  });
3895
3909
  });
3896
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
+ }
3897
4059
  prepareRecoveryRelease(expected, assertAuthority) {
3898
4060
  assertAuthority();
3899
4061
  if (this.recoverySnapshot().digest !== expected)
@@ -6991,7 +7153,7 @@ async function confirm(expected) {
6991
7153
  "storage"
6992
7154
  ],
6993
7155
  [
6994
- `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}`,
6995
7157
  `resolve ${expected.epoch}`
6996
7158
  ]
6997
7159
  ];
@@ -7054,6 +7216,42 @@ async function confirm(expected) {
7054
7216
  async function localRecovery(c, operation) {
7055
7217
  if (operation === "resolve")
7056
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
+ }
7057
7255
  const history = new HistoryRecovery(c);
7058
7256
  const expected = await inspectRecovery(c, history);
7059
7257
  const registration2 = c.recoverySnapshot().registration;
@@ -7355,6 +7553,13 @@ function nativeCall(c, operation, reveal = false, controlBody) {
7355
7553
  }
7356
7554
  var pause = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
7357
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
+ };
7358
7563
  const observed2 = c.inspect();
7359
7564
  if (observed2.status === "absent" || observed2.status === "stopped")
7360
7565
  return observed2;
@@ -7397,6 +7602,10 @@ function serverConfigHint(error) {
7397
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.
7398
7603
  `;
7399
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
+ }
7400
7609
  function startupGuidance(result, input) {
7401
7610
  let report;
7402
7611
  try {
@@ -7404,6 +7613,8 @@ function startupGuidance(result, input) {
7404
7613
  } catch {
7405
7614
  return "";
7406
7615
  }
7616
+ if (report.error === "registry-locked")
7617
+ return registryGuidance(input.workspace, input.stateHome);
7407
7618
  const agent = input.config.agentDir;
7408
7619
  const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
7409
7620
  `;
@@ -7431,6 +7642,15 @@ ${inspect}` : "";
7431
7642
  async function startService(options, foreground, hostUrl) {
7432
7643
  const network = networkSnapshot(process.env);
7433
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
+ }
7434
7654
  let input;
7435
7655
  try {
7436
7656
  input = decodeHostInput(selectStart(options, process.env));
@@ -7443,7 +7663,6 @@ async function startService(options, foreground, hostUrl) {
7443
7663
  `;
7444
7664
  if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
7445
7665
  throw new ContractError("oversize");
7446
- const c = new ServiceCoordination(input.workspace, input.stateHome);
7447
7666
  const existing = await serviceStatus(c);
7448
7667
  if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
7449
7668
  const running = readHostConfig(c);
@@ -7891,7 +8110,7 @@ async function projectMain(argv, hostUrl) {
7891
8110
  mkdirSync6(options.workspace, { recursive: true });
7892
8111
  effectiveWorkspace(options.workspace, options.stateHome);
7893
8112
  const c = new ServiceCoordination(options.workspace, options.stateHome);
7894
- const kept = readHostConfig(c);
8113
+ const kept = c.registryLockDiagnostic() ? undefined : readHostConfig(c);
7895
8114
  if (!kept)
7896
8115
  options.port = 0;
7897
8116
  const result = await startService(options, false, hostUrl);