@claxedo/sandbox-manager 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -314,7 +314,7 @@ function isPrivateIpAddress(ip) {
314
314
  return isPrivateIpv4(embedded);
315
315
  return false;
316
316
  }
317
- var sandboxCheckpointUnusableCauses = ["expired", "runtime_outdated", "runtime_modified", "not_restorable"];
317
+ var sandboxCheckpointUnusableCauses = ["expired", "runtime_modified", "not_restorable"];
318
318
  function isSandboxCheckpointUnusableCause(value) {
319
319
  return sandboxCheckpointUnusableCauses.some((cause) => cause === value);
320
320
  }
@@ -378,6 +378,17 @@ var SandboxRuntimeBootError = class extends Error {
378
378
  this.name = "SandboxRuntimeBootError";
379
379
  }
380
380
  };
381
+ function isSandboxImageDownloadFailure(err) {
382
+ return err instanceof Error && "imageDownloadFailed" in err && err.imageDownloadFailed === true;
383
+ }
384
+ var SandboxImageDownloadError = class extends Error {
385
+ imageDownloadFailed = true;
386
+ constructor(reason) {
387
+ super(reason);
388
+ this.name = "SandboxImageDownloadError";
389
+ }
390
+ };
391
+ var SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED = "update_download_interrupted";
381
392
  function isSandboxCheckpointUnusable(err) {
382
393
  return !!err && typeof err === "object" && isSandboxCheckpointUnusableCause(err.checkpointUnusable);
383
394
  }
@@ -516,7 +527,8 @@ var SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
516
527
  var SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
517
528
  var CODED_REASONS = /* @__PURE__ */ new Map([
518
529
  [SANDBOX_RUNTIME_UNHEALTHY, "The workspace machine stopped answering."],
519
- [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."]
530
+ [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."],
531
+ [SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED, "The update couldn't finish downloading; your workspace is still running. Try again."]
520
532
  ]);
521
533
  function firstLine(text) {
522
534
  return text.split("\n", 1)[0]?.trim() ?? text;
@@ -607,11 +619,19 @@ var PUBLIC_API_ERRORS = {
607
619
  enrollment_paused: { status: 403, message: "enrollment_paused" },
608
620
  enrollment_owner_ineligible: { status: 403, message: "enrollment_owner_ineligible" },
609
621
  enrollment_key_version_mismatch: { status: 403, message: "enrollment_key_version_mismatch" },
622
+ enrollment_denied: { status: 403, message: "enrollment_denied" },
623
+ enrollment_approval_expired: { status: 403, message: "enrollment_approval_expired" },
624
+ enrollment_awaiting_approval: { status: 403, message: "enrollment_awaiting_approval" },
610
625
  invitation_invalid: { status: 403, message: "invitation_invalid" },
611
626
  invitation_expired: { status: 410, message: "invitation_expired" },
612
627
  invitation_revoked: { status: 410, message: "invitation_revoked" },
613
628
  invitation_redeemed: { status: 409, message: "invitation_redeemed" },
614
629
  invitation_host_conflict: { status: 409, message: "invitation_host_conflict" },
630
+ pairing_invalid: { status: 403, message: "pairing_invalid" },
631
+ pairing_expired: { status: 410, message: "pairing_expired" },
632
+ pairing_used: { status: 409, message: "pairing_used" },
633
+ pairing_owner_signed_out: { status: 410, message: "pairing_owner_signed_out" },
634
+ pairing_decided: { status: 409, message: "pairing_decided" },
615
635
  enrollment_generation_superseded: { status: 409, message: "enrollment_generation_superseded" },
616
636
  host_assignment_outside_scope: { status: 400, message: "host_assignment_outside_scope" },
617
637
  host_sealing_key_undeclared: { status: 409, message: "host_sealing_key_undeclared" },
@@ -865,6 +885,51 @@ async function refusedReason(leaseStore, workspaceId, epoch) {
865
885
  return "runtime_lease_not_serving";
866
886
  }
867
887
 
888
+ // src/driver-ensure-input.ts
889
+ function sandboxId(driver, workspaceId) {
890
+ return `${driver}-${workspaceId}`;
891
+ }
892
+ function leaseHostId(driver, workspaceId, lease, managerInput) {
893
+ return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
894
+ }
895
+ function ensureHostInput(input) {
896
+ const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
897
+ const labels = {
898
+ app: input.appLabel,
899
+ workspaceId: input.workspaceId,
900
+ epoch: String(input.lease.epoch),
901
+ homeRegion: input.homeRegion,
902
+ ...input.managerInput?.labels
903
+ };
904
+ return {
905
+ workspaceId: input.workspaceId,
906
+ hostId: leaseHostId(input.driver, input.workspaceId, input.lease, input.managerInput),
907
+ homeRegion: input.homeRegion,
908
+ epoch: input.lease.epoch,
909
+ labels,
910
+ bootSource: input.managerInput?.bootSource ?? (input.lease.checkpoint && input.lease.status !== "ready" && input.driver.metadata.persistence.resume === "replacement-restore" ? { kind: "driver-snapshot", snapshotId: input.lease.checkpoint.providerReference } : { kind: "default" }),
911
+ workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
912
+ runtimeCwd: input.managerInput?.runtimeCwd,
913
+ workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
914
+ env: { ...input.managerInput?.env, ...input.leaseEnv },
915
+ // Brokered secrets ride their own channel — NEVER merged into labels or env.
916
+ // Presence of the key, not its length, is the signal: `[]` is the caller
917
+ // withdrawing every brokered secret, and dropping it would reach the driver
918
+ // as "preserve what you have".
919
+ ...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
920
+ source: input.managerInput?.source,
921
+ ...input.managerInput?.updateImage ? { updateImage: true } : {},
922
+ ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
923
+ exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
924
+ // Withheld, never downgraded: `undefined` here is the same shape a caller
925
+ // that asked for no containment produces, so the throwing drivers take
926
+ // their allow-all path and their throws stay intact for anyone who hands
927
+ // them a policy directly.
928
+ ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
929
+ snapshot: input.managerInput?.snapshot
930
+ };
931
+ }
932
+
868
933
  // src/egress-warnings.ts
869
934
  function egressUnenforcedMessage(input) {
870
935
  const scope = input.workspaceId ? `workspace ${input.workspaceId}` : "every sandbox it provisions";
@@ -997,13 +1062,15 @@ function createSandboxStartRecorder(options) {
997
1062
  * lease as it ends, so a poller reading the lease mid-boot sees how far the
998
1063
  * start has come. `opened` writes the epoch's `lease_decision` before the
999
1064
  * driver runs, so a stop that lands mid-boot finds the boot it ends on the
1000
- * row. A serving lease is not starting. An epoch acquired with a start it
1001
- * continues has that start and no phase yet; one acquired without begins
1002
- * its own here.
1065
+ * row. A serving lease is not starting, unless the step is a person's
1066
+ * update, which is a start of its own on the same epoch. An epoch acquired
1067
+ * with a start it continues has that start and no phase yet; one acquired
1068
+ * without begins its own here.
1003
1069
  */
1004
1070
  observe(lease, input) {
1005
- if (lease.status === "ready") return void 0;
1006
- let progress = lease.start ?? { requestedAt: input.enteredAt, startedAt: input.enteredAt, bootMode: input.bootMode, markedAt: input.enteredAt, phases: [] };
1071
+ if (lease.status === "ready" && !input.update) return void 0;
1072
+ const begun = { requestedAt: input.enteredAt, startedAt: input.enteredAt, bootMode: input.bootMode, markedAt: input.enteredAt, phases: [] };
1073
+ let progress = lease.status === "ready" ? begun : lease.start ?? begun;
1007
1074
  const ended = [];
1008
1075
  let markedAt = progress.markedAt;
1009
1076
  const mark = (phase) => {
@@ -1135,48 +1202,6 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
1135
1202
 
1136
1203
  // src/manager.ts
1137
1204
  var DEFAULT_APP_LABEL = "claxedo";
1138
- function sandboxId(driver, workspaceId) {
1139
- return `${driver}-${workspaceId}`;
1140
- }
1141
- function leaseHostId(driver, workspaceId, lease, managerInput) {
1142
- return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
1143
- }
1144
- function ensureHostInput(input) {
1145
- const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
1146
- const labels = {
1147
- app: input.appLabel,
1148
- workspaceId: input.workspaceId,
1149
- epoch: String(input.lease.epoch),
1150
- homeRegion: input.homeRegion,
1151
- ...input.managerInput?.labels
1152
- };
1153
- return {
1154
- workspaceId: input.workspaceId,
1155
- hostId: leaseHostId(input.driver, input.workspaceId, input.lease, input.managerInput),
1156
- homeRegion: input.homeRegion,
1157
- epoch: input.lease.epoch,
1158
- labels,
1159
- bootSource: input.managerInput?.bootSource ?? (input.lease.checkpoint && input.lease.status !== "ready" && input.driver.metadata.persistence.resume === "replacement-restore" ? { kind: "driver-snapshot", snapshotId: input.lease.checkpoint.providerReference } : { kind: "default" }),
1160
- workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
1161
- runtimeCwd: input.managerInput?.runtimeCwd,
1162
- workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
1163
- env: { ...input.managerInput?.env, ...input.leaseEnv },
1164
- // Brokered secrets ride their own channel — NEVER merged into labels or env.
1165
- // Presence of the key, not its length, is the signal: `[]` is the caller
1166
- // withdrawing every brokered secret, and dropping it would reach the driver
1167
- // as "preserve what you have".
1168
- ...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
1169
- source: input.managerInput?.source,
1170
- ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
1171
- exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
1172
- // Withheld, never downgraded: `undefined` here is the same shape a caller
1173
- // that asked for no containment produces, so the throwing drivers take
1174
- // their allow-all path and their throws stay intact for anyone who hands
1175
- // them a policy directly.
1176
- ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
1177
- snapshot: input.managerInput?.snapshot
1178
- };
1179
- }
1180
1205
  function createSandboxManager(options) {
1181
1206
  const persistence = validateSandboxPersistenceCapabilities(options.driver.metadata.persistence);
1182
1207
  if (!persistence.valid) throw new Error(`Invalid sandbox persistence capabilities: ${persistence.reason}`);
@@ -1286,7 +1311,7 @@ function createSandboxManager(options) {
1286
1311
  }
1287
1312
  const resuming = Boolean(lease.sandboxId && ensure.bootSource?.kind === "default" && options.driver.resumeHost);
1288
1313
  const bootMode = resuming && lease.url ? "resume" : ensure.bootSource?.kind === "driver-snapshot" ? "restore" : "cold-start";
1289
- const start = starts.observe(lease, { bootMode, enteredAt, labels: ensure.labels });
1314
+ const start = starts.observe(lease, { bootMode, enteredAt, labels: ensure.labels, ...managerInput.updateImage ? { update: true } : {} });
1290
1315
  await start?.opened();
1291
1316
  ensure.onResource = async (resource) => {
1292
1317
  const recorded = await options.leaseStore.recordTarget(workspaceId, lease.epoch, {
@@ -1352,6 +1377,10 @@ function createSandboxManager(options) {
1352
1377
  const error = checkpointUnusable ? sandboxCheckpointUnusableError(checkpointUnusable, message) : bootFailed ? sandboxRuntimeBootFailedError(message) : startLimit ? sandboxProviderRateLimitedError(options.driver.id, startLimit) : isSandboxProviderKeyRefused(err) ? sandboxProviderKeyRefusedError(options.driver.id, message) : isSandboxStartRefusal(err) || sandboxLeaseEndedUnderStart(message) ? message : sandboxProviderFailedError(options.driver.id, message);
1353
1378
  if (lease.status === "ready" && !bootFailed) {
1354
1379
  const updated = await options.leaseStore.update(workspaceId, lease.epoch, { lastError: error }, "ready");
1380
+ if (managerInput.updateImage) {
1381
+ console.warn("A workspace's update to the deployment's image failed; it keeps serving its image", { workspaceId, epoch: lease.epoch, error });
1382
+ return { status: "unavailable", error: isSandboxImageDownloadFailure(err) ? SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED : error, epoch: lease.epoch, homeRegion };
1383
+ }
1355
1384
  if (updated) {
1356
1385
  const resolved = await leaseTarget(updated);
1357
1386
  if (resolved.status === "ready") return { ...resolved, stale: true };
@@ -1698,8 +1727,10 @@ export {
1698
1727
  SANDBOX_RETRY_CAP_EXCEEDED,
1699
1728
  SANDBOX_RUNTIME_UNHEALTHY,
1700
1729
  SANDBOX_START_WINDOWS,
1730
+ SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED,
1701
1731
  SandboxCheckpointUnusableError,
1702
1732
  SandboxHostGoneError,
1733
+ SandboxImageDownloadError,
1703
1734
  SandboxProviderKeyRefusedError,
1704
1735
  SandboxProviderRateLimitedError,
1705
1736
  SandboxRuntimeBootError,
@@ -1719,6 +1750,7 @@ export {
1719
1750
  isSandboxCheckpointUnusable,
1720
1751
  isSandboxCheckpointUnusableCause,
1721
1752
  isSandboxHostGone,
1753
+ isSandboxImageDownloadFailure,
1722
1754
  isSandboxListingUnsupported,
1723
1755
  isSandboxProviderKeyRefused,
1724
1756
  isSandboxProviderRateLimited,
@@ -15,6 +15,23 @@ export declare class SandboxRuntimeBootError extends Error implements SandboxRun
15
15
  readonly runtimeBootFailed: true;
16
16
  constructor(reason: string);
17
17
  }
18
+ /**
19
+ * A driver throws this shape when the image a start pulls did not finish
20
+ * downloading because the registry link kept breaking. Nothing the machine
21
+ * served has changed, so an update that fails this way leaves the workspace
22
+ * serving its image.
23
+ */
24
+ export type SandboxImageDownloadFailure = {
25
+ imageDownloadFailed: true;
26
+ message: string;
27
+ };
28
+ export declare function isSandboxImageDownloadFailure(err: unknown): err is SandboxImageDownloadFailure;
29
+ export declare class SandboxImageDownloadError extends Error implements SandboxImageDownloadFailure {
30
+ readonly imageDownloadFailed: true;
31
+ constructor(reason: string);
32
+ }
33
+ /** The answer to a person's update whose image download kept breaking; the lease keeps the provider's own text. */
34
+ export declare const SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED = "update_download_interrupted";
18
35
  export { isSandboxCheckpointUnusableCause, sandboxCheckpointUnusableCauses, type SandboxCheckpointUnusableCause };
19
36
  /**
20
37
  * A driver throws this shape when the checkpoint a start restores cannot be
@@ -1,10 +1,24 @@
1
1
  export declare function workspaceRuntimeVersion(): string;
2
2
  /**
3
- * The oldest workspace-runtime version a driver may restore a workspace's
4
- * snapshot into running again. A filesystem snapshot carries the runtime it
5
- * was taken with, so a fix to the runtime reaches a workspace that keeps
6
- * waking only when this rises. Raise it, together with the runtime version,
7
- * for a security-relevant runtime change alone: every snapshot below it is
8
- * refused and its person must start the workspace fresh.
3
+ * The oldest workspace-runtime version this control plane and relay serve, for
4
+ * every driver: a workspace's snapshot restored, or the older image a Boat wake
5
+ * keeps serving instead of waiting on the deployment's. A filesystem snapshot
6
+ * carries the runtime it was taken with, and a kept image the runtime it
7
+ * shipped, so a fix to the runtime reaches a workspace that keeps waking only
8
+ * when this rises. Below it a Cloudflare restore is refused and its person
9
+ * must start the workspace fresh, and a Boat wake boots the deployment's image.
10
+ *
11
+ * Raise it, and the workspace-runtime version with it in the same commit, for
12
+ * a runtime security fix or for a control-plane or relay contract an older
13
+ * runtime cannot speak, and name the first cloud image published at the new
14
+ * version: the version-skew suite (`bun run --cwd packages/harness flows:skew`)
15
+ * runs the current control plane and relay against exactly that image.
16
+ * Bumping the version with it keeps the comparison honest: every image below
17
+ * the new version was built before the change. What it does not cover: the
18
+ * images built after the floor's at the same version are trusted, not tested.
9
19
  */
10
- export declare const MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
20
+ export declare const SERVED_WORKSPACE_RUNTIME_FLOOR: {
21
+ readonly version: "0.10.1";
22
+ readonly image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8";
23
+ };
24
+ export declare const MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION: "0.10.1";
@@ -3,8 +3,13 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
3
  function workspaceRuntimeVersion() {
4
4
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
5
  }
6
- var MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
6
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
7
+ version: "0.10.1",
8
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
9
+ };
10
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
7
11
  export {
8
- MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION,
12
+ MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION,
13
+ SERVED_WORKSPACE_RUNTIME_FLOOR,
9
14
  workspaceRuntimeVersion
10
15
  };
@@ -16,14 +16,16 @@ export declare function createSandboxStartRecorder(options: {
16
16
  * lease as it ends, so a poller reading the lease mid-boot sees how far the
17
17
  * start has come. `opened` writes the epoch's `lease_decision` before the
18
18
  * driver runs, so a stop that lands mid-boot finds the boot it ends on the
19
- * row. A serving lease is not starting. An epoch acquired with a start it
20
- * continues has that start and no phase yet; one acquired without begins
21
- * its own here.
19
+ * row. A serving lease is not starting, unless the step is a person's
20
+ * update, which is a start of its own on the same epoch. An epoch acquired
21
+ * with a start it continues has that start and no phase yet; one acquired
22
+ * without begins its own here.
22
23
  */
23
24
  observe(lease: SandboxLease, input: {
24
25
  bootMode: SandboxBootMode;
25
26
  enteredAt: number;
26
27
  labels: Record<string, string>;
28
+ update?: true;
27
29
  }): {
28
30
  opened: () => Promise<void>;
29
31
  end: (phase: SandboxStartPhase) => Promise<void>;
package/package.json CHANGED
@@ -163,7 +163,7 @@
163
163
  "access": "public"
164
164
  },
165
165
  "name": "@claxedo/sandbox-manager",
166
- "version": "0.10.1",
166
+ "version": "0.10.2",
167
167
  "scripts": {
168
168
  "build": "bun scripts/build.ts",
169
169
  "prepublishOnly": "npm run build",
@@ -172,8 +172,8 @@
172
172
  "test": "mkdir -p .artifacts/unit && node ../../script/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
173
173
  },
174
174
  "dependencies": {
175
- "@claxedo/helpers": "0.2.1",
176
- "@claxedo/sandbox-contract": "0.10.1",
175
+ "@claxedo/helpers": "0.2.2",
176
+ "@claxedo/sandbox-contract": "0.10.2",
177
177
  "@vercel/sandbox": "3.3.0",
178
178
  "modal": "0.10.1"
179
179
  },