@moinulmoin/eden 0.5.0 → 0.7.0

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.
@@ -7,10 +7,11 @@ import { createServer, } from "node:net";
7
7
  import { execFile, spawn, } from "node:child_process";
8
8
  import { fileURLToPath, } from "node:url";
9
9
  import { promisify, } from "node:util";
10
- import { EveCliError, } from "./eve.js";
11
- import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
10
+ import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
11
+ import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
12
12
  import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
13
13
  import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
14
+ import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
14
15
  const execFileAsync = promisify(execFile);
15
16
  const require = createRequire(import.meta.url);
16
17
  const wranglerEntrypoint = require.resolve("wrangler");
@@ -63,6 +64,8 @@ function mapPackagingCheck(value) {
63
64
  return "VAL-SEC-003";
64
65
  case "DOCKER_PLATFORM_BLOCKED":
65
66
  return "VAL-BUILD-005";
67
+ case "EVE_SANDBOX_RESUME_PATCH":
68
+ return value.id;
66
69
  default:
67
70
  return `EVE-${value.id}`;
68
71
  }
@@ -440,21 +443,11 @@ function exactContainerEntries(value, name) {
440
443
  typeof entry.name !== "string") {
441
444
  return undefined;
442
445
  }
443
- const id = entry.id;
444
446
  const entryName = entry.name;
445
447
  if (entryName === name) {
446
- const configuration = entry.configuration;
447
- const image = typeof entry.image === "string"
448
- ? entry.image
449
- : typeof configuration === "object" &&
450
- configuration !== null &&
451
- typeof configuration.image === "string"
452
- ? configuration.image
453
- : undefined;
454
448
  entries.push({
455
- id,
449
+ id: entry.id,
456
450
  name: entryName,
457
- ...(image === undefined ? {} : { image }),
458
451
  });
459
452
  }
460
453
  }
@@ -530,7 +523,7 @@ async function readWorkersDevSubdomain(accountId) {
530
523
  ? result.subdomain
531
524
  : undefined;
532
525
  return typeof subdomain === "string" &&
533
- /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(subdomain)
526
+ EVE_NAME_PATTERN.test(subdomain)
534
527
  ? subdomain
535
528
  : undefined;
536
529
  }
@@ -546,34 +539,34 @@ async function readWorkersDevSubdomain(accountId) {
546
539
  export function exactTargetContainerEntries(value, name) {
547
540
  return exactContainerEntries(value, boundedResourceName(name, "container"));
548
541
  }
549
- async function defaultCloudflareRead(request) {
550
- const account = await readWranglerJson(["whoami", "--json"]);
542
+ async function readExactTargetInventory(workerName, containerApp) {
551
543
  const containers = await readWranglerJson([
552
- "containers",
553
- "list",
554
- "--json",
555
- "--per-page",
556
- "100",
544
+ "containers", "list", "--json", "--per-page", "100",
557
545
  ]);
558
546
  const deployments = await readWranglerJson([
559
- "deployments",
560
- "list",
561
- "--name",
562
- request.name,
563
- "--json",
547
+ "deployments", "list", "--name", workerName, "--json",
564
548
  ]);
549
+ return {
550
+ containers,
551
+ deployments,
552
+ containerEntries: exactContainerEntries(containers.value, containerApp),
553
+ deploymentEntries: exactDeploymentEntries(deployments.value),
554
+ };
555
+ }
556
+ async function defaultCloudflareRead(request) {
557
+ const account = await readWranglerJson(["whoami", "--json"]);
558
+ const { containers, deployments, containerEntries, deploymentEntries: listedDeployments } = await readExactTargetInventory(request.name, boundedResourceName(request.name, "container"));
565
559
  const accountId = authenticatedAccountId(account.value);
566
560
  const accountAccess = account.failed || accountId === undefined
567
561
  ? "unavailable"
568
562
  : "available";
569
- const containerEntries = exactTargetContainerEntries(containers.value, request.name);
570
563
  const containerAccess = containers.failed || containerEntries === undefined
571
564
  ? "unavailable"
572
565
  : "available";
573
566
  const deploymentEntries = deployments.failed &&
574
567
  /(?:does not exist|not found)/iu.test(deployments.stderr ?? "")
575
568
  ? []
576
- : exactDeploymentEntries(deployments.value);
569
+ : listedDeployments;
577
570
  const workersDevSubdomain = await readWorkersDevSubdomain(accountId);
578
571
  if (accountAccess === "unavailable" ||
579
572
  containerAccess === "unavailable" ||
@@ -610,7 +603,7 @@ async function defaultCloudflareRead(request) {
610
603
  }
611
604
  const workerPresent = deploymentEntries.length > 0;
612
605
  const containerPresent = (containerEntries?.length ?? 0) > 0;
613
- const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0]?.image : undefined);
606
+ const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0] : undefined);
614
607
  return {
615
608
  accountAccess,
616
609
  containerAccess,
@@ -634,16 +627,40 @@ async function defaultCloudflareRead(request) {
634
627
  }
635
628
  /**
636
629
  * Reads provider-backed facts about the live target: the Container
637
- * application's configured image (from `wrangler containers list`) and the
638
- * `EDEN_EVE_DEPLOYMENT_ID` plain-text var Eden stamps into every Worker it
639
- * publishes (from the script settings API). Both are provider state — a
640
- * foreign Worker can forge `x-eden-*` response headers but cannot set a var
641
- * Eden recorded at publish time.
630
+ * application's identity from `wrangler containers list` plus `containers
631
+ * info` (scheduling policy and Durable Object namespace), and the
632
+ * `EDEN_EVE_DEPLOYMENT_ID` plain-text var plus the `EVE_CONTAINER` Durable
633
+ * Object namespace Eden stamps into every Worker it publishes (from the
634
+ * script settings API). All are provider state — a foreign Worker can forge
635
+ * `x-eden-*` response headers but cannot set vars or namespaces Eden
636
+ * recorded at publish time. The ownership proof is the Container
637
+ * application's Durable Object namespace matching the Worker's binding
638
+ * namespace.
642
639
  */
643
- async function readProviderEvidence(workerName, accountId, workerPresent, containerImage) {
640
+ async function readProviderEvidence(workerName, accountId, workerPresent, containerEntry) {
644
641
  const evidence = {
645
- ...(containerImage === undefined ? {} : { containerImage }),
642
+ ...(containerEntry === undefined
643
+ ? {}
644
+ : { containerApplicationId: containerEntry.id }),
646
645
  };
646
+ if (containerEntry !== undefined) {
647
+ const info = await readWranglerJson([
648
+ "containers",
649
+ "info",
650
+ containerEntry.id,
651
+ "--json",
652
+ ]);
653
+ if (!info.failed && typeof info.value === "object" && info.value !== null) {
654
+ const infoRecord = info.value;
655
+ if (typeof infoRecord.scheduling_policy === "string") {
656
+ evidence.containerSchedulingPolicy = infoRecord.scheduling_policy;
657
+ }
658
+ const namespaceId = infoRecord.durable_objects?.namespace_id;
659
+ if (typeof namespaceId === "string") {
660
+ evidence.containerNamespaceId = namespaceId;
661
+ }
662
+ }
663
+ }
647
664
  if (workerPresent && accountId !== undefined) {
648
665
  const token = process.env.CLOUDFLARE_API_TOKEN ??
649
666
  (await readWranglerOAuthToken());
@@ -656,10 +673,16 @@ async function readProviderEvidence(workerName, accountId, workerPresent, contai
656
673
  if (response.ok) {
657
674
  evidence.settingsReadable = true;
658
675
  const parsed = (await response.json());
659
- const binding = parsed.result?.bindings?.find((value) => value.type === "plain_text" &&
676
+ const bindings = parsed.result?.bindings ?? [];
677
+ const deploymentBinding = bindings.find((value) => value.type === "plain_text" &&
660
678
  value.name === "EDEN_EVE_DEPLOYMENT_ID");
661
- if (typeof binding?.text === "string") {
662
- evidence.workerDeploymentId = binding.text;
679
+ if (typeof deploymentBinding?.text === "string") {
680
+ evidence.workerDeploymentId = deploymentBinding.text;
681
+ }
682
+ const containerBinding = bindings.find((value) => value.type === "durable_object_namespace" &&
683
+ value.name === "EVE_CONTAINER");
684
+ if (typeof containerBinding?.namespace_id === "string") {
685
+ evidence.workerContainerNamespaceId = containerBinding.namespace_id;
663
686
  }
664
687
  }
665
688
  else if (response.status !== 404) {
@@ -698,19 +721,23 @@ async function resolveEveTargetOwnership(request, result) {
698
721
  }
699
722
  // Remote proof must come from provider state. A foreign Worker can echo
700
723
  // `x-eden-*` response headers, but it cannot plant the plain-text var Eden
701
- // publishes into script settings, nor point the Container application at an
702
- // image digest Eden recorded. The Worker is proven only by its own settings:
703
- // a matching Container image proves the Container, never the Worker, so
704
- // unreadable settings fail closed.
724
+ // publishes into script settings or claim the Durable Object namespace the
725
+ // Worker's EVE_CONTAINER binding resolves to. The Worker is proven only by
726
+ // its own settings: unreadable settings fail closed.
705
727
  const evidence = result.target.providerEvidence;
706
728
  const recordedDeploymentIds = new Set(record.deploymentIds);
707
729
  const workerProven = result.target.observed?.worker !== true ||
708
730
  (evidence?.settingsReadable === true &&
709
731
  evidence.workerDeploymentId !== undefined &&
710
732
  recordedDeploymentIds.has(evidence.workerDeploymentId));
733
+ // The Container proof is the application running the durable_object
734
+ // scheduling policy with a namespace that matches the Worker binding's
735
+ // namespace; anything else fails closed.
711
736
  const containerProven = result.target.observed?.container !== true ||
712
- (evidence?.containerImage !== undefined &&
713
- record.images.includes(evidence.containerImage));
737
+ (evidence?.containerSchedulingPolicy === "durable_object" &&
738
+ evidence.containerNamespaceId !== undefined &&
739
+ evidence.workerContainerNamespaceId !== undefined &&
740
+ evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
714
741
  const remoteProven = evidence !== undefined && workerProven && containerProven;
715
742
  if (!remoteProven) {
716
743
  return {
@@ -763,7 +790,6 @@ async function resolvedCloudflareOrigin(result, workerName) {
763
790
  return undefined;
764
791
  }
765
792
  try {
766
- const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
767
793
  const origin = resolveStableWorkersDevOrigin({
768
794
  workerName,
769
795
  workersDevSubdomain: result.workersDevSubdomain,
@@ -1130,8 +1156,6 @@ function defaultPublicationRunner() {
1130
1156
  "--env",
1131
1157
  request.identity.environment,
1132
1158
  "--strict",
1133
- "--containers-rollout",
1134
- "immediate",
1135
1159
  ], dirname(request.wranglerConfigPath));
1136
1160
  if (result.exitCode !== 0) {
1137
1161
  return {
@@ -1140,22 +1164,7 @@ function defaultPublicationRunner() {
1140
1164
  ownershipEvidenceRetained: true,
1141
1165
  };
1142
1166
  }
1143
- const containers = await readWranglerJson([
1144
- "containers",
1145
- "list",
1146
- "--json",
1147
- "--per-page",
1148
- "100",
1149
- ]);
1150
- const containerEntries = exactContainerEntries(containers.value, request.hostConfig.container.applicationName);
1151
- const deployments = await readWranglerJson([
1152
- "deployments",
1153
- "list",
1154
- "--name",
1155
- request.identity.workerName,
1156
- "--json",
1157
- ]);
1158
- const deploymentEntries = exactDeploymentEntries(deployments.value);
1167
+ const { containers, deployments, containerEntries, deploymentEntries } = await readExactTargetInventory(request.identity.workerName, request.hostConfig.container.applicationName);
1159
1168
  if (containers.failed ||
1160
1169
  containerEntries === undefined ||
1161
1170
  containerEntries.length !== 1 ||
@@ -1490,7 +1499,6 @@ async function runEveDeployment(request, options, collected) {
1490
1499
  }
1491
1500
  let stableWorkersDevOrigin;
1492
1501
  try {
1493
- const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
1494
1502
  stableWorkersDevOrigin = resolveStableWorkersDevOrigin({
1495
1503
  workerName: request.name,
1496
1504
  workersDevSubdomain,
@@ -1506,7 +1514,7 @@ async function runEveDeployment(request, options, collected) {
1506
1514
  throw deploymentFailure("EVE_ORIGIN_UNAVAILABLE", "The authenticated stable workers.dev origin did not match the exact Worker target.");
1507
1515
  }
1508
1516
  const imageDigest = runtimeEvidence.imageDigest;
1509
- if (imageDigest === undefined || !/^sha256:[0-9a-f]{64}$/u.test(imageDigest)) {
1517
+ if (imageDigest === undefined || !IMAGE_ID_PATTERN.test(imageDigest)) {
1510
1518
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
1511
1519
  }
1512
1520
  const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
@@ -1607,38 +1615,37 @@ async function runEveDeployment(request, options, collected) {
1607
1615
  ? {}
1608
1616
  : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
1609
1617
  };
1618
+ const baseConfigRequest = {
1619
+ accountId,
1620
+ workerName: identity.workerName,
1621
+ containerApplicationName: identity.containerApplicationName,
1622
+ stableContainerInstanceName: identity.stableContainerInstanceName,
1623
+ deploymentId: identity.deploymentId,
1624
+ generationId: identity.generationId,
1625
+ stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1626
+ containerImage: identity.containerImage,
1627
+ runtimeVariableNames: identity.runtimeVariableNames,
1628
+ ...(identity.runtimeRevisionHandle === undefined
1629
+ ? {}
1630
+ : { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
1631
+ schedules: collected.schedules ?? [],
1632
+ ...(collected.workflowWorld === undefined
1633
+ ? {}
1634
+ : { workflowWorld: collected.workflowWorld }),
1635
+ // In-place updates carry the prior Durable Object bindings and
1636
+ // migration history forward verbatim: classes are append-only and are
1637
+ // never silently re-declared or removed.
1638
+ ...(isUpdate && priorDurableObjects !== undefined
1639
+ ? { durableObjects: priorDurableObjects }
1640
+ : {}),
1641
+ ...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
1642
+ ? {}
1643
+ : { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
1644
+ };
1610
1645
  let hostConfig;
1611
1646
  try {
1612
- const { createEveHostConfig, } = await import("@moinulmoin/eden-runtime-cloudflare");
1613
1647
  hostConfig = createEveHostConfig({
1614
- accountId,
1615
- workerName: identity.workerName,
1616
- containerApplicationName: identity.containerApplicationName,
1617
- containerClassName: "EveHostContainer",
1618
- containerBindingName: "EVE_CONTAINER",
1619
- stableContainerInstanceName: identity.stableContainerInstanceName,
1620
- deploymentId: identity.deploymentId,
1621
- generationId: identity.generationId,
1622
- stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1623
- containerImage: identity.containerImage,
1624
- runtimeVariableNames: identity.runtimeVariableNames,
1625
- ...(identity.runtimeRevisionHandle === undefined
1626
- ? {}
1627
- : { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
1628
- schedules: collected.schedules ?? [],
1629
- ...(collected.workflowWorld === undefined
1630
- ? {}
1631
- : { workflowWorld: collected.workflowWorld }),
1632
- // In-place updates carry the prior Durable Object bindings and
1633
- // migration history forward verbatim: classes are append-only, never
1634
- // re-declared, and never deleted (a deleted_classes migration destroys
1635
- // the data).
1636
- ...(isUpdate && priorDurableObjects !== undefined
1637
- ? { durableObjects: priorDurableObjects }
1638
- : {}),
1639
- ...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
1640
- ? {}
1641
- : { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
1648
+ ...baseConfigRequest,
1642
1649
  });
1643
1650
  }
1644
1651
  catch (error) {
@@ -1646,7 +1653,6 @@ async function runEveDeployment(request, options, collected) {
1646
1653
  ? error.message
1647
1654
  : "The exact Eve Worker/Container host configuration was invalid.");
1648
1655
  }
1649
- const { generateEveHostWorkerSource } = await import("@moinulmoin/eden-runtime-cloudflare");
1650
1656
  const workerSource = generateEveHostWorkerSource({ config: hostConfig });
1651
1657
  const paths = await writeEveDeploymentArtifacts(candidate, hostConfig, workerSource);
1652
1658
  const publicationRequest = {
@@ -1656,13 +1662,26 @@ async function runEveDeployment(request, options, collected) {
1656
1662
  ...paths,
1657
1663
  ...(runtimeInjection === undefined ? {} : { runtimeInjection }),
1658
1664
  };
1665
+ /** Publishes one wrangler config; returns its terminal result. */
1666
+ const publishOne = async (request) => (options.publish ?? defaultPublicationRunner())(request);
1659
1667
  let publication;
1668
+ const writeFailureRecord = async (status, evidence) => {
1669
+ try {
1670
+ await writeEveDeploymentRecord(candidate, identity, hostConfig, status, {
1671
+ evidence,
1672
+ });
1673
+ }
1674
+ catch (error) {
1675
+ if (error.code !== "EEXIST")
1676
+ throw error;
1677
+ }
1678
+ };
1660
1679
  try {
1661
1680
  eveProgress(options, "publishing Worker and Container application");
1662
- publication = await (options.publish ?? defaultPublicationRunner())(publicationRequest);
1681
+ publication = await publishOne(publicationRequest);
1663
1682
  }
1664
1683
  catch {
1665
- await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-threw-before-terminal-result" });
1684
+ await writeFailureRecord("indeterminate", "publication-threw-before-terminal-result");
1666
1685
  const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
1667
1686
  return {
1668
1687
  ...collected.result,
@@ -1671,7 +1690,7 @@ async function runEveDeployment(request, options, collected) {
1671
1690
  };
1672
1691
  }
1673
1692
  if (publication.status === "indeterminate") {
1674
- await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: safeText(publication.reason) });
1693
+ await writeFailureRecord("indeterminate", safeText(publication.reason));
1675
1694
  return {
1676
1695
  ...collected.result,
1677
1696
  ok: false,
@@ -1862,25 +1881,26 @@ async function runEveDeployment(request, options, collected) {
1862
1881
  if (isUpdate) {
1863
1882
  // The superseded generation's registry repository is generation-scoped
1864
1883
  // and no longer referenced once CURRENT promotes this generation; only
1865
- // then is it safe to delete.
1866
- const previousImage = priorIdentity === undefined
1884
+ // then is it safe to delete. Every tag in the repository goes with it —
1885
+ // including the snapshot tags Cloudflare created under it.
1886
+ const previousRepository = priorIdentity === undefined
1867
1887
  ? undefined
1868
- : ownedRegistryImageRef(priorIdentity, request.environment, request.name, accountId);
1869
- if (previousImage !== undefined) {
1870
- eveProgress(options, `deleting superseded registry image ${previousImage}`);
1888
+ : ownedRegistryRepository(priorIdentity, request.environment, request.name, accountId);
1889
+ if (previousRepository !== undefined) {
1890
+ eveProgress(options, `deleting superseded registry repository ${previousRepository}`);
1871
1891
  let deleted = "indeterminate";
1872
1892
  try {
1873
- deleted = await (options.deleteRegistryImage ??
1874
- defaultRegistryImageDelete)({ image: previousImage });
1893
+ deleted = await (options.deleteRegistryRepository ??
1894
+ defaultRegistryRepositoryDelete)({ repository: previousRepository });
1875
1895
  }
1876
1896
  catch {
1877
1897
  deleted = "indeterminate";
1878
1898
  }
1879
1899
  supersededImageCleanup = deleted === "indeterminate"
1880
- ? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry image ${previousImage} could not be proven deleted.`, `Remove only the recorded image ref ${previousImage}; do not retry or run broad registry cleanup.`)
1900
+ ? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry repository ${previousRepository} could not be proven deleted.`, `Remove only the recorded repository's tags ${previousRepository}; do not retry or run broad registry cleanup.`)
1881
1901
  : check("VAL-LIFE-007", "passed", deleted === "deleted"
1882
- ? "The superseded generation's registry image was deleted after promotion."
1883
- : "The superseded generation's registry image was already absent.");
1902
+ ? "The superseded generation's registry repository (all tags, including snapshots) was deleted after promotion."
1903
+ : "The superseded generation's registry repository was already absent.");
1884
1904
  }
1885
1905
  }
1886
1906
  let runtimeImageCleanup;
@@ -1992,7 +2012,6 @@ async function collectEvePreflight(request, options) {
1992
2012
  snapshot: null,
1993
2013
  toolchain: null,
1994
2014
  image: null,
1995
- runtime: null,
1996
2015
  secrets: {
1997
2016
  runtimeVariableNames: [],
1998
2017
  valuesRecorded: false,
@@ -2005,6 +2024,7 @@ async function collectEvePreflight(request, options) {
2005
2024
  redactionRegisteredBeforeChildren: false,
2006
2025
  },
2007
2026
  checks: [],
2027
+ sandboxResumePatch: "absent",
2008
2028
  candidateImageId: null,
2009
2029
  candidateImageRetainedLocally: false,
2010
2030
  writtenPaths: [],
@@ -2012,6 +2032,13 @@ async function collectEvePreflight(request, options) {
2012
2032
  };
2013
2033
  }
2014
2034
  checks.push(...packaging.checks.map(mapPackagingCheck));
2035
+ if (packaging.sandboxResumePatch === "skipped") {
2036
+ warnings.push({
2037
+ id: "EVE_SANDBOX_RESUME_PATCH",
2038
+ message: "Eve's just-bash file is not a version Eden knows; conversations may lose their sandbox after a skill change (eve#4440)",
2039
+ remediation: "Pin Eve to a version Eden knows, or update Eden so its known Eve versions include yours.",
2040
+ });
2041
+ }
2015
2042
  const discardPackagingImage = async () => {
2016
2043
  if (packaging.candidateImageId === null ||
2017
2044
  builder.discard === undefined) {
@@ -2058,9 +2085,6 @@ async function collectEvePreflight(request, options) {
2058
2085
  }
2059
2086
  warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
2060
2087
  const schedules = await readCompiledSchedules(candidate);
2061
- // The runtime package is imported lazily so `eden` commands that never
2062
- // reach deploy checks stay fast; the module specifier is fixed.
2063
- const { parseEveScheduleCron } = await import("@moinulmoin/eden-runtime-cloudflare");
2064
2088
  const expressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) !== undefined);
2065
2089
  const unexpressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) === undefined);
2066
2090
  if (unexpressible.length > 0) {
@@ -2208,7 +2232,7 @@ async function collectEvePreflight(request, options) {
2208
2232
  "VAL-BUILD-007",
2209
2233
  ];
2210
2234
  const runtimeProofComplete = requiredRuntimeChecks.every((id) => runtimeEvidence?.checks.some((value) => value.id === id && value.status === "passed"));
2211
- const runtimeImageIdentityValid = /^sha256:[0-9a-f]{64}$/u.test(runtimeEvidence.imageDigest ?? "");
2235
+ const runtimeImageIdentityValid = IMAGE_ID_PATTERN.test(runtimeEvidence.imageDigest ?? "");
2212
2236
  if (!runtimeProofComplete) {
2213
2237
  checks.push(check("VAL-BUILD-005", "failed", "The runtime seam did not return complete proof of Linux/amd64 image construction, official Eve-start boot, and real health.", "Return passed VAL-BUILD-005, VAL-BUILD-006, and VAL-BUILD-007 checks from the disposable runtime runner."));
2214
2238
  }
@@ -2496,7 +2520,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2496
2520
  const entries = await readdir(generationsRoot).catch(() => []);
2497
2521
  const scanned = [];
2498
2522
  const deploymentIds = new Set();
2499
- const images = new Set();
2500
2523
  for (const entry of entries) {
2501
2524
  const generationRoot = join(generationsRoot, entry);
2502
2525
  const recordPath = join(generationRoot, "deployment.json");
@@ -2519,9 +2542,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2519
2542
  const deploymentId = parsed.deploymentId;
2520
2543
  if (deploymentId !== undefined)
2521
2544
  deploymentIds.add(deploymentId);
2522
- if (typeof parsed.identity.containerImage === "string") {
2523
- images.add(parsed.identity.containerImage);
2524
- }
2525
2545
  }
2526
2546
  }
2527
2547
  scanned.sort((left, right) => right.modifiedAt - left.modifiedAt);
@@ -2557,7 +2577,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2557
2577
  identity: live.record.identity,
2558
2578
  status: live.record.status,
2559
2579
  deploymentIds: [...deploymentIds],
2560
- images: [...images],
2561
2580
  worldCloudflare: live.record.worldCloudflare,
2562
2581
  durableObjects: historySource?.record.durableObjects,
2563
2582
  };
@@ -2607,19 +2626,20 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2607
2626
  const migrations = [];
2608
2627
  const durableObjectsValid = Array.isArray(rawBindings) &&
2609
2628
  Array.isArray(rawMigrations);
2629
+ const invalidHistory = () => ({
2630
+ recordPath,
2631
+ identity: base.identity,
2632
+ status,
2633
+ deploymentId,
2634
+ worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
2635
+ durableObjects: undefined,
2636
+ });
2610
2637
  if (durableObjectsValid) {
2611
2638
  for (const binding of rawBindings) {
2612
2639
  if (typeof binding !== "object" || binding === null ||
2613
2640
  typeof binding.name !== "string" ||
2614
2641
  typeof binding.class_name !== "string") {
2615
- return {
2616
- recordPath,
2617
- identity: base.identity,
2618
- status,
2619
- deploymentId,
2620
- worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
2621
- durableObjects: undefined,
2622
- };
2642
+ return invalidHistory();
2623
2643
  }
2624
2644
  bindings.push(binding);
2625
2645
  }
@@ -2627,15 +2647,8 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2627
2647
  if (typeof migration !== "object" || migration === null ||
2628
2648
  typeof migration.tag !== "string" ||
2629
2649
  !Array.isArray(migration.new_sqlite_classes) ||
2630
- migration.new_sqlite_classes.some((className) => typeof className !== "string")) {
2631
- return {
2632
- recordPath,
2633
- identity: base.identity,
2634
- status,
2635
- deploymentId,
2636
- worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
2637
- durableObjects: undefined,
2638
- };
2650
+ (migration.new_sqlite_classes ?? []).some((className) => typeof className !== "string")) {
2651
+ return invalidHistory();
2639
2652
  }
2640
2653
  migrations.push(migration);
2641
2654
  }
@@ -2651,37 +2664,30 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2651
2664
  }
2652
2665
  async function defaultDestroyTargetRead(request) {
2653
2666
  const account = await readWranglerJson(["whoami", "--json"]);
2654
- const deployments = await readWranglerJson([
2655
- "deployments",
2656
- "list",
2657
- "--name",
2658
- request.workerName,
2659
- "--json",
2660
- ]);
2661
- const containers = await readWranglerJson([
2662
- "containers",
2663
- "list",
2664
- "--json",
2665
- "--per-page",
2666
- "100",
2667
- ]);
2667
+ const { deployments, containerEntries: entries, deploymentEntries } = await readExactTargetInventory(request.workerName, request.containerApplicationName);
2668
2668
  const accountId = authenticatedAccountId(account.value);
2669
2669
  const workerExists = !deployments.failed &&
2670
- (exactDeploymentEntries(deployments.value)?.length ?? 0) > 0;
2671
- const entries = exactContainerEntries(containers.value, request.containerApplicationName);
2670
+ (deploymentEntries?.length ?? 0) > 0;
2672
2671
  const firstContainer = entries === undefined ? undefined : entries[0];
2673
- const ownedImages = request.registryImages;
2674
- let registryImagesPresent;
2675
- if (ownedImages !== undefined && ownedImages.length > 0) {
2672
+ const ownedRepositories = request.registryRepositories;
2673
+ let registryTagsPresent;
2674
+ if (ownedRepositories !== undefined && ownedRepositories.length > 0) {
2676
2675
  const images = await readWranglerJson([
2677
2676
  "containers",
2678
2677
  "images",
2679
2678
  "list",
2680
2679
  "--json",
2681
2680
  ]);
2682
- const present = registryImageNames(images.value);
2683
- if (!images.failed && present !== undefined) {
2684
- registryImagesPresent = ownedImages.filter((image) => present.has(image));
2681
+ const tagsByRepository = registryTagsByRepository(images.value);
2682
+ if (!images.failed && tagsByRepository !== undefined) {
2683
+ const present = [];
2684
+ for (const [repository, tags] of tagsByRepository) {
2685
+ if (!ownedRepositories.includes(repository))
2686
+ continue;
2687
+ for (const tag of tags)
2688
+ present.push(`${repository}:${tag}`);
2689
+ }
2690
+ registryTagsPresent = present;
2685
2691
  }
2686
2692
  }
2687
2693
  return {
@@ -2690,21 +2696,22 @@ async function defaultDestroyTargetRead(request) {
2690
2696
  ? {}
2691
2697
  : { containerApplicationId: firstContainer.id }),
2692
2698
  ...(accountId === undefined ? {} : { accountId }),
2693
- ...(registryImagesPresent === undefined
2699
+ ...(registryTagsPresent === undefined
2694
2700
  ? {}
2695
- : { registryImagesPresent }),
2701
+ : { registryTagsPresent }),
2696
2702
  };
2697
2703
  }
2698
2704
  /**
2699
2705
  * `wrangler containers images list --json` reports `[{name, tags}]` entries
2700
- * with the account prefix already stripped from the repository name. The set
2701
- * carries `repository:tag` refs for the owned tags Eden publishes.
2706
+ * with the account prefix already stripped from the repository name. The map
2707
+ * carries every tag per repository so owned-repository cleanup can delete
2708
+ * snapshot tags alongside the pushed `candidate` tag.
2702
2709
  */
2703
- function registryImageNames(value) {
2710
+ function registryTagsByRepository(value) {
2704
2711
  const collection = jsonCollection(value);
2705
2712
  if (collection === undefined)
2706
2713
  return undefined;
2707
- const names = new Set();
2714
+ const tagsByRepository = new Map();
2708
2715
  for (const entry of collection) {
2709
2716
  if (typeof entry !== "object" || entry === null || !("name" in entry)) {
2710
2717
  return undefined;
@@ -2713,13 +2720,15 @@ function registryImageNames(value) {
2713
2720
  const tags = "tags" in entry ? entry.tags : undefined;
2714
2721
  if (typeof name !== "string" || !Array.isArray(tags))
2715
2722
  return undefined;
2723
+ const validTags = [];
2716
2724
  for (const tag of tags) {
2717
2725
  if (typeof tag !== "string")
2718
2726
  return undefined;
2719
- names.add(`${name}:${tag}`);
2727
+ validTags.push(tag);
2720
2728
  }
2729
+ tagsByRepository.set(name, validTags);
2721
2730
  }
2722
- return names;
2731
+ return tagsByRepository;
2723
2732
  }
2724
2733
  async function defaultWorkerDelete(request) {
2725
2734
  const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
@@ -2739,23 +2748,59 @@ async function defaultContainerDelete(request) {
2739
2748
  }
2740
2749
  return "indeterminate";
2741
2750
  }
2742
- async function defaultRegistryImageDelete(request) {
2743
- const result = await runWranglerWithInput(["containers", "images", "delete", request.image, "--skip-confirmation"], process.cwd());
2744
- if (result.exitCode === 0)
2745
- return "deleted";
2746
- if (/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2751
+ /**
2752
+ * Deletes every tag in an owned registry repository — the `candidate` tag
2753
+ * Eden pushed plus any `rootfs-snapshot-*`/`rootfs-set-*` tags container
2754
+ * snapshots created under the source image repository. Deleting only
2755
+ * `candidate` would leave snapshot storage behind, so each tag in the
2756
+ * repository is deleted individually.
2757
+ */
2758
+ async function defaultRegistryRepositoryDelete(request) {
2759
+ const images = await readWranglerJson([
2760
+ "containers",
2761
+ "images",
2762
+ "list",
2763
+ "--json",
2764
+ ]);
2765
+ const collection = jsonCollection(images.value);
2766
+ if (images.failed || collection === undefined)
2767
+ return "indeterminate";
2768
+ const entry = collection.find((candidate) => typeof candidate === "object" &&
2769
+ candidate !== null &&
2770
+ candidate.name === request.repository);
2771
+ if (entry === undefined)
2747
2772
  return "absent";
2773
+ const tags = entry.tags;
2774
+ if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) {
2775
+ return "indeterminate";
2748
2776
  }
2749
- return "indeterminate";
2777
+ let deletedAny = false;
2778
+ for (const tag of tags) {
2779
+ const result = await runWranglerWithInput([
2780
+ "containers",
2781
+ "images",
2782
+ "delete",
2783
+ `${request.repository}:${tag}`,
2784
+ "--skip-confirmation",
2785
+ ], process.cwd());
2786
+ if (result.exitCode === 0) {
2787
+ deletedAny = true;
2788
+ continue;
2789
+ }
2790
+ if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2791
+ return "indeterminate";
2792
+ }
2793
+ }
2794
+ return deletedAny ? "deleted" : "absent";
2750
2795
  }
2751
2796
  /**
2752
- * The exact `<repository>:${EVE_REGISTRY_IMAGE_TAG}` ref this generation's
2753
- * deployment record proves Eden pushed, or undefined. Ownership holds only
2754
- * when the recorded digest reference points at the repository derived from
2755
- * the same identity that produced it; a digest-bearing reference to any other
2756
- * repository was published outside Eden's push and is never owned here.
2797
+ * The registry repository this generation's deployment record proves Eden
2798
+ * pushed to, or undefined. Ownership holds only when the recorded digest
2799
+ * reference points at the repository derived from the same identity that
2800
+ * produced it; a digest-bearing reference to any other repository was
2801
+ * published outside Eden's push and is never owned here.
2757
2802
  */
2758
- function ownedRegistryImageRef(identity, environment, name, accountId) {
2803
+ function ownedRegistryRepository(identity, environment, name, accountId) {
2759
2804
  if (identity.projectId === undefined ||
2760
2805
  identity.generationId === undefined ||
2761
2806
  identity.containerImage === undefined ||
@@ -2768,14 +2813,14 @@ function ownedRegistryImageRef(identity, environment, name, accountId) {
2768
2813
  imageDigestFromReference(identity.containerImage) === undefined) {
2769
2814
  return undefined;
2770
2815
  }
2771
- return `${repository}:${EVE_REGISTRY_IMAGE_TAG}`;
2816
+ return repository;
2772
2817
  }
2773
2818
  /**
2774
- * The exact image ref an indeterminate deploy attempt provably pushed. The
2775
- * retained attempt evidence names the target's repository; the account and
2776
- * target key must match the verified destroy record.
2819
+ * The registry repository an indeterminate deploy attempt provably pushed.
2820
+ * The retained attempt evidence names the target's repository; the account
2821
+ * and target key must match the verified destroy record.
2777
2822
  */
2778
- async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2823
+ async function attemptRegistryRepository(attemptPath, accountId, targetKey) {
2779
2824
  const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
2780
2825
  if (raw === undefined)
2781
2826
  return undefined;
@@ -2795,7 +2840,7 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2795
2840
  (!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
2796
2841
  "accountId" in parsed &&
2797
2842
  parsed.accountId === accountId) {
2798
- return `${parsed.imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
2843
+ return parsed.imageRepository;
2799
2844
  }
2800
2845
  if ("imageReference" in parsed &&
2801
2846
  typeof parsed.imageReference === "string" &&
@@ -2805,39 +2850,40 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
2805
2850
  if (repository !== undefined &&
2806
2851
  repository.startsWith(expected) &&
2807
2852
  (!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
2808
- return `${repository.slice(`registry.cloudflare.com/${accountId}/`.length)}:${EVE_REGISTRY_IMAGE_TAG}`;
2853
+ return repository.slice(`registry.cloudflare.com/${accountId}/`.length);
2809
2854
  }
2810
2855
  }
2811
2856
  return undefined;
2812
2857
  }
2813
2858
  /**
2814
- * Every registry image tag this exact target provably owns: the recorded
2859
+ * Every registry repository this exact target provably owns: the recorded
2815
2860
  * digest references in matching deployment records plus the repository
2816
2861
  * evidence retained by indeterminate pushes. Never derived by prefix-matching
2817
- * the registry; each ref is computed from the immutable generation records.
2862
+ * the registry; each repository is computed from the immutable generation
2863
+ * records.
2818
2864
  */
2819
- async function collectEveDestroyRegistryImages(projectRoot, environment, name, record) {
2865
+ async function collectEveDestroyRegistryRepositories(projectRoot, environment, name, record) {
2820
2866
  const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2821
2867
  const entries = await readdir(generationsRoot).catch(() => []);
2822
- const images = new Set();
2868
+ const repositories = new Set();
2823
2869
  const targetKey = record.projectId === undefined
2824
2870
  ? undefined
2825
2871
  : stableDeploymentKey(record.projectId, record.accountId, environment, name);
2826
2872
  for (const entry of entries) {
2827
2873
  const generationRoot = join(generationsRoot, entry);
2828
2874
  const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2829
- const deployedImage = parsed === undefined
2875
+ const deployedRepository = parsed === undefined
2830
2876
  ? undefined
2831
- : ownedRegistryImageRef(parsed.identity, environment, name, record.accountId);
2832
- if (deployedImage !== undefined)
2833
- images.add(deployedImage);
2877
+ : ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
2878
+ if (deployedRepository !== undefined)
2879
+ repositories.add(deployedRepository);
2834
2880
  if (targetKey === undefined)
2835
2881
  continue;
2836
- const attemptImage = await attemptRegistryImageRef(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
2837
- if (attemptImage !== undefined)
2838
- images.add(attemptImage);
2882
+ const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
2883
+ if (attemptRepository !== undefined)
2884
+ repositories.add(attemptRepository);
2839
2885
  }
2840
- return [...images];
2886
+ return [...repositories];
2841
2887
  }
2842
2888
  export async function runEveDestroy(request, options = {}) {
2843
2889
  const startedAt = Date.now();
@@ -2890,11 +2936,11 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2890
2936
  }
2891
2937
  checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
2892
2938
  eveProgress(options, "verifying the immutable ownership record");
2893
- const ownedRegistryImages = await collectEveDestroyRegistryImages(request.projectRoot, request.environment, request.name, record.identity);
2939
+ const ownedRegistryRepositories = await collectEveDestroyRegistryRepositories(request.projectRoot, request.environment, request.name, record.identity);
2894
2940
  const readTarget = () => Promise.resolve(reader({
2895
2941
  workerName: expectedWorker,
2896
2942
  containerApplicationName: expectedContainer,
2897
- registryImages: ownedRegistryImages,
2943
+ registryRepositories: ownedRegistryRepositories,
2898
2944
  }));
2899
2945
  const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
2900
2946
  eveProgress(options, "reading the exact remote inventory");
@@ -2904,9 +2950,9 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2904
2950
  }
2905
2951
  if (!before.workerExists &&
2906
2952
  before.containerApplicationId === undefined &&
2907
- (ownedRegistryImages.length === 0 ||
2908
- (before.registryImagesPresent !== undefined &&
2909
- before.registryImagesPresent.length === 0))) {
2953
+ (ownedRegistryRepositories.length === 0 ||
2954
+ (before.registryTagsPresent !== undefined &&
2955
+ before.registryTagsPresent.length === 0))) {
2910
2956
  emit({
2911
2957
  ok: true,
2912
2958
  status: "absent",
@@ -2963,17 +3009,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2963
3009
  ? "The exact owned Container application was deleted."
2964
3010
  : "The exact Container application was already absent."));
2965
3011
  }
2966
- const imageDeletes = new Map();
2967
- if (ownedRegistryImages.length > 0) {
2968
- const deleteRegistryImage = options.deleteRegistryImage ?? defaultRegistryImageDelete;
2969
- const deletable = before.registryImagesPresent === undefined
2970
- ? ownedRegistryImages
2971
- : ownedRegistryImages.filter((image) => before.registryImagesPresent?.includes(image) === true);
3012
+ const repositoryDeletes = new Map();
3013
+ if (ownedRegistryRepositories.length > 0) {
3014
+ const deleteRegistryRepository = options.deleteRegistryRepository ?? defaultRegistryRepositoryDelete;
3015
+ // Every tag inside an owned repository is deleted (candidate plus
3016
+ // snapshot tags). When the registry could not be enumerated, still
3017
+ // attempt the exact owned repositories; absence verification below
3018
+ // re-reads the registry when it becomes readable.
3019
+ const presentRepositories = new Set((before.registryTagsPresent ?? []).map((ref) => ref.split(":")[0] ?? ""));
3020
+ const deletable = before.registryTagsPresent === undefined
3021
+ ? ownedRegistryRepositories
3022
+ : ownedRegistryRepositories.filter((repository) => presentRepositories.has(repository));
2972
3023
  if (deletable.length > 0) {
2973
- eveProgress(options, `deleting ${deletable.length} registry image${deletable.length === 1 ? "" : "s"}`);
3024
+ eveProgress(options, `deleting ${deletable.length} registry repositor${deletable.length === 1 ? "y" : "ies"}`);
2974
3025
  }
2975
- for (const image of deletable) {
2976
- imageDeletes.set(image, await deleteRegistryImage({ image }));
3026
+ for (const repository of deletable) {
3027
+ repositoryDeletes.set(repository, await deleteRegistryRepository({ repository }));
2977
3028
  }
2978
3029
  }
2979
3030
  eveProgress(options, "verifying the exact target is absent");
@@ -2991,15 +3042,16 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
2991
3042
  });
2992
3043
  fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
2993
3044
  }
2994
- const retainedImages = ownedRegistryImages.filter((image) => after.registryImagesPresent === undefined
2995
- ? imageDeletes.get(image) === "indeterminate"
2996
- : after.registryImagesPresent.includes(image));
2997
- if (ownedRegistryImages.length > 0) {
2998
- checks.push(retainedImages.length === 0
3045
+ const presentTagsAfter = after.registryTagsPresent ?? [];
3046
+ const retainedRepositories = ownedRegistryRepositories.filter((repository) => presentTagsAfter.some((ref) => ref.startsWith(`${repository}:`)) ||
3047
+ (after.registryTagsPresent === undefined &&
3048
+ repositoryDeletes.get(repository) === "indeterminate"));
3049
+ if (ownedRegistryRepositories.length > 0) {
3050
+ checks.push(retainedRepositories.length === 0
2999
3051
  ? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
3000
- : check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${retainedImages.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed image refs with wrangler containers images delete."));
3052
+ : check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image tag(s) remain after destroy: ${retainedRepositories.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed repositories' tags with wrangler containers images delete."));
3001
3053
  }
3002
- if (retainedImages.length > 0) {
3054
+ if (retainedRepositories.length > 0) {
3003
3055
  emit({
3004
3056
  ok: false,
3005
3057
  status: "failed",
@@ -3007,7 +3059,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
3007
3059
  name: request.name,
3008
3060
  checks,
3009
3061
  });
3010
- fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${retainedImages.join(", ")}.`);
3062
+ fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image tag(s) remain in: ${retainedRepositories.join(", ")}.`);
3011
3063
  }
3012
3064
  checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
3013
3065
  const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");