@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.
@@ -612,6 +612,7 @@ var ERROR_CODES = [
612
612
  "quarantine",
613
613
  "capacity",
614
614
  "unavailable",
615
+ "registry-locked",
615
616
  "auth-unavailable",
616
617
  "recording-exhausted",
617
618
  "receipt-inconclusive"
@@ -5444,6 +5445,21 @@ function names(path, allowed) {
5444
5445
  dir.closeSync();
5445
5446
  }
5446
5447
  }
5448
+ function decodeRegistryLock(snapshot) {
5449
+ const o = closed(json(snapshot), ["schema", "token", "pid", "host"]);
5450
+ return {
5451
+ schema: literal(o.schema, "cadence-service-registry-lock/1"),
5452
+ token: decodeId(o.token),
5453
+ pid: integer(o.pid, 1, 2147483647),
5454
+ host: text(o.host, 255, true)
5455
+ };
5456
+ }
5457
+ var registryNames = [
5458
+ "format.json",
5459
+ "roots.json",
5460
+ "registry.lock",
5461
+ "recovery-v1"
5462
+ ];
5447
5463
  function recoveryOwnerStatus(pid, host) {
5448
5464
  if (host !== hostname2())
5449
5465
  return "ambiguous";
@@ -5495,11 +5511,7 @@ class ServiceCoordination {
5495
5511
  registry() {
5496
5512
  if (!this.files.directory(this.registryDirectory))
5497
5513
  return;
5498
- names(this.registryDirectory, [
5499
- "format.json",
5500
- "roots.json",
5501
- "registry.lock"
5502
- ]);
5514
+ names(this.registryDirectory, registryNames);
5503
5515
  const format = this.files.read(this.registryPath("format.json"));
5504
5516
  const snapshot = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
5505
5517
  if (!format || !snapshot)
@@ -5524,10 +5536,10 @@ class ServiceCoordination {
5524
5536
  }
5525
5537
  };
5526
5538
  }
5527
- assertNamespace(walks) {
5528
- if (!this.files.directory(this.directory, false, walks))
5539
+ assertNamespace(walks, directory = this.directory) {
5540
+ if (!this.files.directory(directory, false, walks))
5529
5541
  refused();
5530
- names(this.directory, [
5542
+ names(directory, [
5531
5543
  "format.json",
5532
5544
  "config.json",
5533
5545
  "registration.json",
@@ -5539,15 +5551,16 @@ class ServiceCoordination {
5539
5551
  "history-v2",
5540
5552
  "conversations-v1"
5541
5553
  ]);
5542
- this.files.directory(join6(this.directory, "history-v2"), false, walks);
5554
+ this.files.directory(join6(directory, "history-v2"), false, walks);
5543
5555
  }
5544
- selected() {
5556
+ selected(directory = this.directory, root = this.store.root, stoppedOnly = false) {
5545
5557
  const walks = new DirectoryWalks;
5546
- if (!this.files.directory(this.directory, false, walks))
5558
+ if (!this.files.directory(directory, false, walks))
5547
5559
  return;
5548
- this.assertNamespace(walks);
5549
- const manifest = this.files.read(this.path("format.json"), undefined, walks);
5550
- const config = this.files.read(this.path("config.json"), undefined, walks);
5560
+ this.assertNamespace(walks, directory);
5561
+ const path = (name) => join6(directory, name);
5562
+ const manifest = this.files.read(path("format.json"), undefined, walks);
5563
+ const config = this.files.read(path("config.json"), undefined, walks);
5551
5564
  if (!manifest || !config)
5552
5565
  refused();
5553
5566
  const f = closed(json(manifest), [
@@ -5557,20 +5570,22 @@ class ServiceCoordination {
5557
5570
  "configIdentity"
5558
5571
  ]);
5559
5572
  literal(f.schema, "cadence-service-format/1");
5560
- literal(f.root, this.store.root);
5573
+ literal(f.root, root);
5561
5574
  literal(f.stateHome, this.stateHome);
5562
5575
  literal(f.configIdentity, config.identity);
5563
5576
  const c = decodeOwnerConfig(json(config));
5564
- const hostConfig = this.files.read(this.path("host-config.json"), undefined, walks);
5577
+ const hostConfig = this.files.read(path("host-config.json"), undefined, walks);
5565
5578
  if (hostConfig && hostConfig.identity !== c.policyIdentity)
5566
5579
  refused();
5567
- const registration = this.files.read(this.path("registration.json"), undefined, walks);
5568
- const owner = this.files.read(this.path("owner.lock"), undefined, walks);
5569
- const capability = this.files.read(this.path("local-capability.json"), undefined, walks);
5570
- const checkout = this.files.read(this.store.lockPath, 8192, walks);
5580
+ const registration = this.files.read(path("registration.json"), undefined, walks);
5581
+ const owner = this.files.read(path("owner.lock"), undefined, walks);
5582
+ const capability = this.files.read(path("local-capability.json"), undefined, walks);
5583
+ const checkout = this.files.read(join6(dirname5(directory), "checkout.lock"), 8192, walks);
5571
5584
  const value = registration ? decodeRegistration(json(registration)) : undefined;
5572
- if (value && (value.root !== this.store.root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
5585
+ if (value && (value.root !== root || value.stateHome !== this.stateHome || value.configIdentity !== config.identity || !endpointMatches(c.endpoint, value.endpoint)))
5573
5586
  refused();
5587
+ if (stoppedOnly && (checkout || value && value.phase !== "stopped"))
5588
+ throw new ContractError("quarantine");
5574
5589
  if (value && value.phase !== "stopped") {
5575
5590
  if (!owner || !checkout || !capability)
5576
5591
  refused();
@@ -5650,18 +5665,21 @@ class ServiceCoordination {
5650
5665
  }
5651
5666
  withRegistry(operation) {
5652
5667
  this.files.directory(this.registryDirectory, true);
5653
- names(this.registryDirectory, [
5654
- "format.json",
5655
- "roots.json",
5656
- "registry.lock"
5657
- ]);
5668
+ names(this.registryDirectory, registryNames);
5658
5669
  const lockPath = this.registryPath("registry.lock");
5659
- const lock = this.files.create(lockPath, encoded({
5660
- schema: "cadence-service-registry-lock/1",
5661
- token: randomUUID4(),
5662
- pid: process.pid,
5663
- host: hostname2()
5664
- }));
5670
+ let lock;
5671
+ try {
5672
+ lock = this.files.create(lockPath, encoded({
5673
+ schema: "cadence-service-registry-lock/1",
5674
+ token: randomUUID4(),
5675
+ pid: process.pid,
5676
+ host: hostname2()
5677
+ }));
5678
+ } catch (error) {
5679
+ if (error instanceof Error && "code" in error && error.code === "EEXIST")
5680
+ throw new ContractError("registry-locked");
5681
+ throw error;
5682
+ }
5665
5683
  let dirty = false;
5666
5684
  try {
5667
5685
  let format = this.files.read(this.registryPath("format.json"));
@@ -5685,11 +5703,7 @@ class ServiceCoordination {
5685
5703
  }, () => {
5686
5704
  this.files.assert(lockPath, lock);
5687
5705
  this.files.assert(this.registryPath("format.json"), format);
5688
- names(this.registryDirectory, [
5689
- "format.json",
5690
- "roots.json",
5691
- "registry.lock"
5692
- ]);
5706
+ names(this.registryDirectory, registryNames);
5693
5707
  });
5694
5708
  this.files.remove(lockPath, lock);
5695
5709
  return result;
@@ -5821,6 +5835,154 @@ class ServiceCoordination {
5821
5835
  });
5822
5836
  });
5823
5837
  }
5838
+ registryLockDiagnostic() {
5839
+ try {
5840
+ const lock = this.files.read(this.registryPath("registry.lock"));
5841
+ if (!lock)
5842
+ return;
5843
+ const value = decodeRegistryLock(lock);
5844
+ return {
5845
+ reason: "registry-locked",
5846
+ owner: recoveryOwnerStatus(value.pid, value.host)
5847
+ };
5848
+ } catch {
5849
+ return { reason: "registry-locked", owner: "unreadable" };
5850
+ }
5851
+ }
5852
+ registryRecoveryState() {
5853
+ const lock = this.files.read(this.registryPath("registry.lock"));
5854
+ if (!lock)
5855
+ throw new ContractError("conflict");
5856
+ const value = decodeRegistryLock(lock);
5857
+ names(this.registryDirectory, registryNames);
5858
+ const format = this.files.read(this.registryPath("format.json"));
5859
+ const roots = this.files.read(this.registryPath("roots.json"), REGISTRY_BYTES);
5860
+ const registry = !format && !roots ? undefined : this.registry();
5861
+ const entries = registry?.value.entries ?? [];
5862
+ const protectedRoots = new Set(entries.map((row) => hash2(row.root)));
5863
+ const proofs = [];
5864
+ const directory = opendirSync(this.files.namespace);
5865
+ try {
5866
+ let count = 0;
5867
+ for (let entry = directory.readSync();entry; entry = directory.readSync()) {
5868
+ if (++count > 1024)
5869
+ throw new ContractError("capacity");
5870
+ if (!/^[a-f0-9]{64}$/.test(entry.name) || protectedRoots.has(entry.name))
5871
+ continue;
5872
+ const service = join6(this.files.namespace, entry.name, "service-v1");
5873
+ if (!this.files.directory(service))
5874
+ continue;
5875
+ const manifest = this.files.read(join6(service, "format.json"));
5876
+ if (!manifest)
5877
+ refused();
5878
+ const f = closed(json(manifest), [
5879
+ "schema",
5880
+ "root",
5881
+ "stateHome",
5882
+ "configIdentity"
5883
+ ]);
5884
+ const root = pathValue(f.root);
5885
+ if (hash2(root) !== entry.name)
5886
+ refused();
5887
+ const selected = this.selected(service, root, true);
5888
+ if (!selected)
5889
+ throw new ContractError("quarantine");
5890
+ proofs.push([
5891
+ entry.name,
5892
+ ...[
5893
+ selected.manifest,
5894
+ selected.config,
5895
+ selected.hostConfig,
5896
+ selected.registration
5897
+ ].map((file) => file ? [file.identity, file.stamp] : null)
5898
+ ]);
5899
+ }
5900
+ } finally {
5901
+ directory.closeSync();
5902
+ }
5903
+ proofs.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en"));
5904
+ const stateDigest = hash2(encoded([
5905
+ lock.identity,
5906
+ lock.stamp,
5907
+ format ? [format.identity, format.stamp] : null,
5908
+ roots ? [roots.identity, roots.stamp] : null,
5909
+ proofs
5910
+ ]));
5911
+ const epoch = hash2(encoded([lock.identity, lock.stamp]));
5912
+ const archive = this.registryPath(join6("recovery-v1", epoch));
5913
+ const attempts = [];
5914
+ let previousResolver = null;
5915
+ if (this.files.directory(archive)) {
5916
+ names(archive, Array.from({ length: 128 }, (_, n) => `${n}.json`));
5917
+ for (let n = 0;n < 128; n++) {
5918
+ const attempt = this.files.read(join6(archive, `${n}.json`), 32768);
5919
+ if (!attempt) {
5920
+ names(archive, attempts.map((_, i) => `${i}.json`));
5921
+ break;
5922
+ }
5923
+ const o = closed(json(attempt, 32768), [
5924
+ "schema",
5925
+ "lock",
5926
+ "pid",
5927
+ "host",
5928
+ "digest"
5929
+ ]);
5930
+ literal(o.schema, "cadence-registry-recovery/1");
5931
+ literal(o.lock, lock.bytes.toString("utf8"));
5932
+ decodeHash(o.digest);
5933
+ previousResolver = recoveryOwnerStatus(integer(o.pid, 1, 2147483647), text(o.host, 255, true));
5934
+ attempts.push(attempt);
5935
+ }
5936
+ }
5937
+ const inspection = {
5938
+ scope: "global-registry",
5939
+ epoch,
5940
+ coordinationDigest: hash2(encoded([stateDigest, attempts.map((a) => [a.identity, a.stamp])])),
5941
+ owner: recoveryOwnerStatus(value.pid, value.host),
5942
+ process: { pid: value.pid, host: value.host },
5943
+ reservations: entries.length,
5944
+ previousResolver,
5945
+ resolution: "local-interactive-only"
5946
+ };
5947
+ return { lock, archive, attempts, stateDigest, inspection };
5948
+ }
5949
+ inspectRegistryRecovery() {
5950
+ return this.registryRecoveryState().inspection;
5951
+ }
5952
+ resolveRegistryRecovery(expected, assertAuthority) {
5953
+ assertAuthority();
5954
+ const state = this.registryRecoveryState();
5955
+ if (encoded(state.inspection) !== encoded(expected))
5956
+ throw new ContractError("conflict");
5957
+ if (expected.owner !== "absent" || expected.previousResolver !== null && expected.previousResolver !== "absent")
5958
+ throw new ContractError("quarantine");
5959
+ if (state.attempts.length >= 128)
5960
+ throw new ContractError("capacity");
5961
+ assertAuthority();
5962
+ const receiptPath = join6(state.archive, `${state.attempts.length}.json`);
5963
+ const receipt = this.files.create(receiptPath, encoded({
5964
+ schema: "cadence-registry-recovery/1",
5965
+ lock: state.lock.bytes.toString("utf8"),
5966
+ pid: process.pid,
5967
+ host: hostname2(),
5968
+ digest: expected.coordinationDigest
5969
+ }), 32768);
5970
+ assertAuthority();
5971
+ const current = this.registryRecoveryState();
5972
+ if (current.stateDigest !== state.stateDigest || current.inspection.owner !== "absent" || current.attempts.length !== state.attempts.length + 1)
5973
+ throw new ContractError("conflict");
5974
+ for (const [n, attempt] of state.attempts.entries())
5975
+ this.files.assert(join6(state.archive, `${n}.json`), attempt, 32768);
5976
+ this.files.assert(receiptPath, receipt, 32768);
5977
+ assertAuthority();
5978
+ this.files.remove(this.registryPath("registry.lock"), state.lock);
5979
+ return {
5980
+ status: "resolved",
5981
+ scope: "global-registry",
5982
+ recoveryId: expected.epoch,
5983
+ notice: "registry-only-project-fences-preserved"
5984
+ };
5985
+ }
5824
5986
  prepareRecoveryRelease(expected, assertAuthority) {
5825
5987
  assertAuthority();
5826
5988
  if (this.recoverySnapshot().digest !== expected)
@@ -8433,6 +8595,13 @@ function nativeCall(c, operation, reveal = false, controlBody) {
8433
8595
  }
8434
8596
  var pause = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
8435
8597
  async function serviceStatus(c, diagnose = false) {
8598
+ const registryLock = c.registryLockDiagnostic?.();
8599
+ if (registryLock)
8600
+ return {
8601
+ status: "unavailable",
8602
+ ...registryLock,
8603
+ recovery: "cadence recover inspect --workspace <project> [--state-home <state-home>]"
8604
+ };
8436
8605
  const observed = c.inspect();
8437
8606
  if (observed.status === "absent" || observed.status === "stopped")
8438
8607
  return observed;
@@ -8475,6 +8644,10 @@ function serverConfigHint(error) {
8475
8644
  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.
8476
8645
  `;
8477
8646
  }
8647
+ function registryGuidance(workspace, stateHome) {
8648
+ 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.
8649
+ `;
8650
+ }
8478
8651
  function startupGuidance(result, input) {
8479
8652
  let report;
8480
8653
  try {
@@ -8482,6 +8655,8 @@ function startupGuidance(result, input) {
8482
8655
  } catch {
8483
8656
  return "";
8484
8657
  }
8658
+ if (report.error === "registry-locked")
8659
+ return registryGuidance(input.workspace, input.stateHome);
8485
8660
  const agent = input.config.agentDir;
8486
8661
  const inspect = `Inspect: cadence-service diagnose|recover inspect --workspace ${JSON.stringify(input.workspace)}
8487
8662
  `;
@@ -8509,6 +8684,15 @@ ${inspect}` : "";
8509
8684
  async function startService(options, foreground, hostUrl) {
8510
8685
  const network = networkSnapshot(process.env);
8511
8686
  const env = hostEnvironment(process.env);
8687
+ const c = new ServiceCoordination(options.workspace, options.stateHome);
8688
+ if (c.registryLockDiagnostic()) {
8689
+ process.stderr.write(registryGuidance(c.store.root, c.stateHome));
8690
+ return {
8691
+ status: "refused",
8692
+ error: "registry-locked",
8693
+ stage: "service"
8694
+ };
8695
+ }
8512
8696
  let input;
8513
8697
  try {
8514
8698
  input = decodeHostInput(selectStart(options, process.env));
@@ -8521,7 +8705,6 @@ async function startService(options, foreground, hostUrl) {
8521
8705
  `;
8522
8706
  if (Buffer.byteLength(serialized) > STARTUP_INPUT_BYTES)
8523
8707
  throw new ContractError("oversize");
8524
- const c = new ServiceCoordination(input.workspace, input.stateHome);
8525
8708
  const existing = await serviceStatus(c);
8526
8709
  if (existing && typeof existing === "object" && "status" in existing && existing.status === "alive") {
8527
8710
  const running = readHostConfig(c);
@@ -8632,8 +8815,8 @@ async function privateLink(workspace, stateHome, config) {
8632
8815
  }
8633
8816
  class ProjectOpenRefusal extends ContractError {
8634
8817
  noOwner;
8635
- constructor(noOwner) {
8636
- super("unavailable");
8818
+ constructor(noOwner, code2 = "unavailable") {
8819
+ super(code2);
8637
8820
  this.noOwner = noOwner;
8638
8821
  }
8639
8822
  }
@@ -8642,6 +8825,8 @@ async function openProjectDirectory(workspace, stateHome, agentDir, hostUrl) {
8642
8825
  throw new ContractError("forbidden");
8643
8826
  effectiveWorkspace(workspace, stateHome);
8644
8827
  const c = new ServiceCoordination(workspace, stateHome);
8828
+ if (c.registryLockDiagnostic())
8829
+ throw new ProjectOpenRefusal(true, "registry-locked");
8645
8830
  const kept = readHostConfig(c);
8646
8831
  const options = {
8647
8832
  workspace,
@@ -8650,7 +8835,7 @@ async function openProjectDirectory(workspace, stateHome, agentDir, hostUrl) {
8650
8835
  };
8651
8836
  const result = await startService(options, false, hostUrl);
8652
8837
  if (!result || typeof result !== "object" || !("status" in result) || result.status !== "alive")
8653
- throw new ProjectOpenRefusal(!!result && typeof result === "object" && "status" in result && result.status === "refused" && c.inspect().status === "absent");
8838
+ throw new ProjectOpenRefusal(!!result && typeof result === "object" && "status" in result && result.status === "refused" && (("error" in result) && result.error === "registry-locked" || c.inspect().status === "absent"), result && typeof result === "object" && "error" in result && result.error === "registry-locked" ? "registry-locked" : "unavailable");
8654
8839
  const link = await privateLink(c.store.root, stateHome);
8655
8840
  if (!link)
8656
8841
  throw new ContractError("unavailable");
@@ -10028,7 +10213,7 @@ async function createGateway(value, hostUrl, source) {
10028
10213
  const input = decodeGatewayInput(value), store = new GatewayStore(input.serverConfig, input.stateHome);
10029
10214
  const web = loadWebAssets(source?.webDirectory);
10030
10215
  const build2 = {
10031
- contractHash: "9c917c085d034e08764de359cee49aea748c395a63ed629fe67d149b97850fa1",
10216
+ contractHash: "944f3b53bd5b7f1d3f30868e0947d900e356ae9a2bcfb9f591edd988b3d5eb3a",
10032
10217
  assets: {
10033
10218
  sourceHash: web.manifest.sourceHash,
10034
10219
  contractHash: web.manifest.contractHash,
@@ -25661,7 +25846,7 @@ function refuseAt(stage, step) {
25661
25846
  }
25662
25847
  }
25663
25848
  async function createServiceHost(value, sourceBuild) {
25664
- const embedded = "9c917c085d034e08764de359cee49aea748c395a63ed629fe67d149b97850fa1";
25849
+ const embedded = "944f3b53bd5b7f1d3f30868e0947d900e356ae9a2bcfb9f591edd988b3d5eb3a";
25665
25850
  if (embedded && sourceBuild)
25666
25851
  throw new ContractError("unsupported-context");
25667
25852
  const contractHash = decodeHash(embedded ?? sourceBuild?.contractHash);
@@ -96,6 +96,7 @@ export const ERROR_CODES = [
96
96
  "quarantine",
97
97
  "capacity",
98
98
  "unavailable",
99
+ "registry-locked",
99
100
  "auth-unavailable",
100
101
  "recording-exhausted",
101
102
  "receipt-inconclusive",