@claxedo/sandbox-manager 0.10.2 → 0.10.3

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/README.md CHANGED
@@ -70,7 +70,8 @@ await manager.stop("workspace-1")
70
70
 
71
71
  Swap `createDockerSandboxDriver` for `createModalSandboxDriver`,
72
72
  `createVercelSandboxDriver`,
73
- `createCloudflareSandboxDriver`, or `createBoatSandboxDriver` (all under
73
+ `createCloudflareSandboxDriver`, `createBoatSandboxDriver`, or
74
+ `createAgent37SandboxDriver` (all under
74
75
  `@claxedo/sandbox-manager/drivers/*`) to place on a hosted provider instead —
75
76
  see [`docs/architecture.md`](docs/architecture.md) for the full driver
76
77
  comparison and each provider's required options. Swap `createMemoryLeaseStore`
@@ -83,6 +84,37 @@ tail and container exit information, with staged environment and registry secret
83
84
  values redacted before truncation. A diagnostic request failure preserves the
84
85
  original health failure.
85
86
 
87
+ ### Agent37: one container per instance
88
+
89
+ An Agent37 instance runs one container of a registered image in a gVisor
90
+ sandbox with a disk of its own, so the driver has no VM to prepare. It
91
+ registers the sandbox image as a template on the account the first time that
92
+ account boots it (34 s for the 3.7 GB image), named from the image and the
93
+ command, with a command that boots the runtime. The boot env travels as the
94
+ one instance variable `CLAXEDO_RUNTIME_ENV`, a sourceable script the command
95
+ evaluates and unsets: Agent37 merges an env edit into what it stores and never
96
+ returns the stored names, so separate variables could not be cleared at a later
97
+ boot. The runtime's port is a public port of the instance, a URL that asks for
98
+ no credential, and the runtime authenticates every route but `/global/health`.
99
+
100
+ A stop keeps the whole filesystem except `/tmp` and releases CPU and memory;
101
+ the disk is billed until the instance is deleted. A resume stores the boot's
102
+ env, network policy and labels, then starts the instance, which is what hands a
103
+ new container the stored env. An image update (`update` onto the new image's
104
+ template) keeps only the instance's mount path, `/claxedo-persistent`, so the
105
+ workspace, `~/.claxedo` and `~/.workspace-runtime` are symlinks into it; the
106
+ sandbox refuses bind mounts. A resume keeps the image the instance last ran
107
+ unless the person asked for the deployment's or its runtime is older than
108
+ `MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION`.
109
+
110
+ Measured 2026-11-11 with `scripts/agent37-live-check.ts` on the 2 vCPU / 4 GB
111
+ shape, from the driver call to the runtime's first healthy answer: 3.5 s cold
112
+ with the template registered (33 s the first time a host pulled the image),
113
+ 1.1 s to stop, 4.2 s to resume, 5.7 s to resume a running instance onto another
114
+ size. A runtime that cannot start is reported 13 s after the create with its
115
+ container's log. Not measured: Codex's command sandbox under gVisor, which
116
+ refused `unshare -m` as root and has no `bwrap` on the image's path.
117
+
86
118
  ### Boat: what survives a stop, and what survives an image update
87
119
 
88
120
  A Boat sandbox is a Linux VM whose disk Boat snapshots at every stop; the
@@ -109,7 +141,7 @@ for any other image, the first start seeds them. The seed runs once per image, a
109
141
  image, streams the trees out of a created container of it with `docker cp` and
110
142
  unpacks them as root, leaving out `~/.cache` (which Boat's own snapshots leave
111
143
  out too) and npm's cache. Boat's link to ghcr resets mid-pull (2 of 8 staging
112
- updates on 2026-10-22, `failed to copy: … connection reset by peer`), and
144
+ updates, `failed to copy: … connection reset by peer`), and
113
145
  Docker ends a pull at the first reset, keeping the layers it finished and none
114
146
  of the ones in flight. So the pull runs again after a transport failure
115
147
  (connection reset, unexpected EOF, a timeout) for as long as each run finishes
@@ -162,7 +194,7 @@ the connection. A wake whose machine holds only an older image keeps serving
162
194
  that image: its trees, stub and env are there, and the runtime in them is no
163
195
  older than `MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION`. The wake answers
164
196
  with that image's label, so the workspace reads as outdated, instead of waiting
165
- 60–100 s on the new image's pull and seed (staging, 2026-10-21). The update
197
+ 60–100 s on the new image's pull and seed (staging). The update
166
198
  runs only when a start asks for it with `updateImage` (a person's Restart to
167
199
  update): the new image is pulled and seeded as above, its stub replaces the
168
200
  old one, and the old image's trees are removed. That step is a start of its
@@ -171,7 +203,7 @@ update whose step fails leaves the lease serving its image and answers the
171
203
  start with the driver's reason, which the manager logs, and a start asking for
172
204
  the update of a workspace already serving the deployment's image answers it as
173
205
  it is. A cold start, and a wake whose older runtime is
174
- below the floor, takes the new image at once. On staging (2026-10-22, 0.10.1
206
+ below the floor, takes the new image at once. On staging (0.10.1
175
207
  to 0.10.2) the wake that kept the image answered in 22 s and the update's step
176
208
  ran 92.7 s.
177
209
 
@@ -252,9 +284,12 @@ no runtime state and no boot env (`boatTemplateResetCommand`). On Boat,
252
284
  Lifecycle, in `drivers/boat-template.ts`, run by
253
285
  `bun run --cwd packages/claxedo-server sandbox:boat-template --image=<image>`:
254
286
 
255
- 1. **Name.** `claxedo-rt-` and the first 12 hex digits of the SHA-256 of the
287
+ 1. **Name.** A prefix and the first 12 hex digits of the SHA-256 of the
256
288
  image reference. The reference is content-addressed (its tag names the
257
- build) and names the trees directory, so it is the template's key.
289
+ build) and names the trees directory, so it is the template's key. The
290
+ prefix defaults to `claxedo-rt-`; deploy-cloud.yml passes
291
+ `--prefix=claxedo-rt-<environment>-`, since one Boat account holds every
292
+ environment's templates and pruning (step 5) works within a prefix.
258
293
  2. **Build, once per image.** A ready template is reused and one still saving
259
294
  is waited for; a missing or failed one is built on a builder sandbox: the
260
295
  Boat driver boots the image there (pull, seed, stub, runtime healthy), the
@@ -369,28 +404,31 @@ spend the owner's delivered accounts (owner ruling, 2026-09-28).
369
404
  ## Egress containment
370
405
 
371
406
  `SandboxManagerInput.net` restricts what the sandbox can reach on the network.
372
- It is a *separate* control from brokered secrets, with a **different** posture,
373
- and the difference matters:
374
-
375
- - **Brokered secrets fail closed.** A driver that cannot broker refuses to
376
- provision (`error: "secret_brokering_unsupported"`).
377
- - **Egress does not.** Only `vercel` (names) can enforce an allowlist. Every
378
- other driver declares `metadata.egressControl: "none"`, and for those the
379
- manager **withholds** the policy and provisions anyway — the sandbox runs
380
- with unrestricted egress.
381
-
382
- Withholding rather than passing is deliberate: `docker`, `modal` and `boat`
383
- throw when handed a restricted policy, and `cloudflare` and the fetch
384
- bridge accept one and silently ignore it. Withholding at the manager means the
385
- throwing drivers never see a policy (their throws stay as their own last line of
386
- defence) and the silently-dropping ones stop pretending.
387
-
388
- The gap is never silent. `createSandboxManager` warns at composition, and again
389
- each time a policy is withheld:
407
+ It is a *separate* control from brokered secrets, and both fail closed:
408
+
409
+ - **Brokered secrets.** A driver that cannot broker refuses to provision
410
+ (`error: "secret_brokering_unsupported"`).
411
+ - **Egress.** `cloudflare` and `vercel` (names) and `agent37` (names and IPv4
412
+ ranges) enforce an allowlist. `docker`,
413
+ `modal` and `boat` declare `metadata.egressControl: "none"` and throw when
414
+ handed a restricted policy, so a start with one on those drivers is refused
415
+ before a lease exists (`error: "sandbox_egress_uncontained:<driver>"`), and
416
+ `sandboxStartFailureReason` turns that into the one line a person reads.
417
+
418
+ A start may instead pass `uncontainedEgress: "allow"` in its
419
+ `SandboxManagerInput`: the manager then **withholds** the policy and provisions
420
+ anyway, so the sandbox runs with unrestricted egress. The setting is read per
421
+ start, not composed into the manager, because it is the workspace owner's
422
+ decision (the hosted control plane reads its organization's opt-in at each
423
+ start), and a withdrawn allowance refuses the very next start. Withholding
424
+ rather than passing keeps the throwing drivers' throws as their own last line
425
+ of defence. That gap is never silent: the manager warns on the first withheld
426
+ start per driver per isolate:
390
427
 
391
428
  ```text
392
- [sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "cloudflare" declares
393
- egressControl: "none", so workspace ws_1 can reach ANY host on the internet …
429
+ [sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "boat" declares
430
+ egressControl: "none" and this start allows uncontained egress, so
431
+ workspace ws_1 can reach ANY host on the internet …
394
432
  ```
395
433
 
396
434
  Pass `onEgressUnenforced` to route that into telemetry instead of `console.warn`
@@ -398,9 +436,9 @@ Pass `onEgressUnenforced` to route that into telemetry instead of `console.warn`
398
436
  replaces the console warning, so only do it if the replacement is as visible.
399
437
 
400
438
  `driver.metadata.egressControl` is the machine-readable source of truth, and
401
- `sandboxEgressDisposition(control, net)` is the pure predicate the manager uses,
402
- so you can ask the same question before composing. One exception stays fail
403
- closed: a hosts-only driver handed an address-only policy is refused
439
+ `sandboxEgressDisposition(control, net, uncontained)` is the pure predicate the
440
+ manager uses, so you can ask the same question before composing. A hosts-only
441
+ driver handed an address-only policy is refused
404
442
  (`sandbox_egress_policy_unenforceable`) rather than degraded, because that
405
443
  driver *does* enforce egress — it just cannot express that encoding.
406
444
 
@@ -21,6 +21,11 @@ var PUBLIC_API_ERRORS = {
21
21
  org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
22
22
  org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
23
23
  org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
24
+ org_limit_reached: { family: "access", status: 403, message: "An account owns at most 3 organizations" },
25
+ org_creation_rate_limited: { family: "access", status: 429, message: "Organization creation limit exceeded" },
26
+ project_limit_reached: { family: "access", status: 402, message: "Free accounts have 3 projects. Get Cloud or Self-host for unlimited projects." },
27
+ seats_full: { family: "access", status: 402, message: "Add teammate seats to invite more people." },
28
+ email_verification_required: { family: "access", status: 403, message: "A verified email address is required" },
24
29
  org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
25
30
  org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
26
31
  team_not_found: { family: "access", status: 404, message: "Team not found" },
@@ -30,6 +35,12 @@ var PUBLIC_API_ERRORS = {
30
35
  project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
31
36
  project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
32
37
  resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
38
+ org_not_deletable: { family: "access", status: 409, message: "A personal organization goes with its account, and a deployment's with the deployment" },
39
+ org_has_workspaces: { family: "access", status: 409, message: "Delete the organization's workspaces first" },
40
+ account_owns_workspaces: { family: "access", status: 409, message: "Delete your workspaces first" },
41
+ account_founds_organization: { family: "access", status: 409, message: "Delete the organizations you founded first" },
42
+ account_has_cloud_subscription: { family: "access", status: 409, message: "Cancel Cloud before deleting this account" },
43
+ recent_sign_in_required: { family: "access", status: 403, message: "Sign in again to confirm this deletion" },
33
44
  session_not_found: { family: "session_share", status: 404, message: "This session is not on the control plane, so it cannot be shared from People yet." },
34
45
  session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
35
46
  session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
@@ -3,7 +3,7 @@ import type { SandboxMachineClass, SandboxPhaseTiming, SandboxSecretBrokering, S
3
3
  export type { SandboxMachineClass, SandboxSecretBrokering } from "@claxedo/sandbox-contract";
4
4
  import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult, type SandboxStopInput } from "./checkpoint-manager";
5
5
  import type { SandboxLifecycleEvent } from "./lease-lifecycle";
6
- import type { SandboxBrokeredSecret, SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy } from "./egress-policy";
6
+ import type { SandboxBrokeredSecret, SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy, SandboxUncontainedEgress } from "./egress-policy";
7
7
  import type { SandboxLeaseFailure } from "./lease-failure";
8
8
  export { DEFAULT_WORKSPACE_RUNTIME_PORT };
9
9
  export type SandboxRegion = string;
@@ -101,18 +101,20 @@ export type SandboxDriverMetadata = {
101
101
  * else; the encodings are alternatives, not additive requirements.
102
102
  *
103
103
  * - `"hosts-and-cidrs"` — the provider filters by name AND by address.
104
- * - `"hosts"` — the provider filters by hostname only (Vercel firewall).
105
- * - `"none"` — the driver cannot express an egress allowlist. This covers
106
- * BOTH drivers that throw when handed a restricted policy (docker, boat,
107
- * and modal for host policy) AND — more dangerously — drivers that
108
- * silently ignore `net` and run wide open (cloudflare, the fetch bridge).
109
- * Modal is `"none"` even though it can cut the network entirely: a total
110
- * blackout is not an allowlist and cannot serve a workspace that has to
111
- * clone a repo and reach a model provider.
104
+ * - `"hosts"` — the provider filters by hostname only (the Vercel firewall;
105
+ * the Cloudflare container started without internet, with each allowed
106
+ * host routed through the sandbox Worker).
107
+ * - `"none"` — the driver cannot express an egress allowlist: docker, boat
108
+ * and modal throw when handed a restricted policy. Modal is `"none"` even
109
+ * though it can cut the network entirely: a total blackout is not an
110
+ * allowlist and cannot serve a workspace that has to clone a repo and
111
+ * reach a model provider.
112
112
  *
113
113
  * The manager reads this to decide what the driver is handed:
114
- * `sandboxEgressDisposition` enforces where it can and WITHHOLDS (loudly)
115
- * where it cannot. See `SandboxEgressDisposition` for the posture and
114
+ * `sandboxEgressDisposition` enforces where it can, and where it cannot
115
+ * refuses the start unless the start allows uncontained egress
116
+ * (`SandboxManagerInput.uncontainedEgress`), in which case the policy is
117
+ * withheld and the gap reported. See `SandboxEgressDisposition` and
116
118
  * `public-docs/sandbox-egress.md` for the operator-facing consequence.
117
119
  */
118
120
  egressControl: SandboxEgressControl;
@@ -505,10 +507,20 @@ export type SandboxManagerInput = {
505
507
  source?: SandboxSource;
506
508
  exposure?: SandboxExposure;
507
509
  /**
508
- * @see SandboxDriverEnsureInput.net — enforced by a capable driver, withheld
509
- * (with a warning) by one that declares `egressControl: "none"`.
510
+ * @see SandboxDriverEnsureInput.net — enforced by a capable driver; on one
511
+ * that declares `egressControl: "none"` the start is refused, or the policy
512
+ * withheld with a warning where `uncontainedEgress` allows it.
510
513
  */
511
514
  net?: SandboxNetworkPolicy;
515
+ /**
516
+ * What this start does with a restricted `net` on a driver that declares
517
+ * `egressControl: "none"`: refused (`sandbox_egress_uncontained`, the
518
+ * default), or allowed to run with the policy withheld and the gap reported
519
+ * through `onEgressUnenforced`. Allowing is the workspace owner's explicit
520
+ * decision, read per start so a withdrawn allowance refuses the next one:
521
+ * the sandbox then holds a private checkout and can reach any host.
522
+ */
523
+ uncontainedEgress?: SandboxUncontainedEgress;
512
524
  snapshot?: string;
513
525
  /** @see SandboxDriverEnsureInput.updateImage */
514
526
  updateImage?: true;
@@ -682,9 +694,9 @@ export type SandboxManagerOptions = {
682
694
  now?: () => number;
683
695
  appLabel?: string;
684
696
  /**
685
- * Sink for the loud half of "enforce where we can, document where we can't".
686
- * Called at composition time and again every time a restricted policy is
687
- * withheld from a driver that cannot enforce it.
697
+ * Sink for the warning raised the first time per driver per isolate that a
698
+ * start's restricted policy is withheld from a driver that cannot enforce it
699
+ * (`SandboxManagerInput.uncontainedEgress: "allow"`).
688
700
  *
689
701
  * Defaults to `console.warn(event.message)`. Pass your own to route the gap
690
702
  * into a telemetry pipeline instead — but note the default is deliberately
@@ -23,6 +23,29 @@ export type SandboxDriverBilling = {
23
23
  excludes?: string;
24
24
  };
25
25
  export declare const boatBilling: SandboxDriverBilling;
26
+ /**
27
+ * The shape an Agent37 instance of each class is created at. Agent37 sells four
28
+ * shapes and none above 4 vCPU, so `large` is its 4 vCPU / 16 GB one. The disk
29
+ * holds the workspace and the runtime's state and is billed while stopped too.
30
+ */
31
+ export declare const agent37MachineShapes: {
32
+ readonly small: {
33
+ readonly cpu: 2;
34
+ readonly memory: 4;
35
+ readonly disk: 10;
36
+ };
37
+ readonly default: {
38
+ readonly cpu: 4;
39
+ readonly memory: 8;
40
+ readonly disk: 20;
41
+ };
42
+ readonly large: {
43
+ readonly cpu: 4;
44
+ readonly memory: 16;
45
+ readonly disk: 40;
46
+ };
47
+ };
48
+ export declare const agent37Billing: SandboxDriverBilling;
26
49
  export declare const cloudflareBilling: SandboxDriverBilling;
27
50
  /** The rate a sandbox of this class runs at; a driver with one size answers it for the class it was not asked for. */
28
51
  export declare function sandboxMachineRate(billing: SandboxDriverBilling | undefined, machineClass: SandboxMachineClass | null): SandboxMachineRate | undefined;
@@ -26,6 +26,27 @@ var boatBilling = {
26
26
  large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
27
27
  }
28
28
  };
29
+ var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
30
+ var AGENT37_MONTH_HOURS = 730;
31
+ var agent37MachineShapes = {
32
+ small: { cpu: 2, memory: 4, disk: 10 },
33
+ default: { cpu: 4, memory: 8, disk: 20 },
34
+ large: { cpu: 4, memory: 16, disk: 40 }
35
+ };
36
+ function agent37Rate(shape) {
37
+ const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
38
+ return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
39
+ }
40
+ var agent37Billing = {
41
+ source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
42
+ meter: "Agent37 meters an instance per minute at a monthly rate over 730 hours: $0.80 per vCPU, $0.70 per GB of memory and $0.09 per GB of disk while it runs, and the disk alone while it is stopped.",
43
+ rates: {
44
+ small: agent37Rate(agent37MachineShapes.small),
45
+ default: agent37Rate(agent37MachineShapes.default),
46
+ large: agent37Rate(agent37MachineShapes.large)
47
+ },
48
+ excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
49
+ };
29
50
  var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
30
51
  var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
31
52
  var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
@@ -61,7 +82,7 @@ function trimToUndefined(value) {
61
82
  const trimmed = value.trim();
62
83
  return trimmed.length > 0 ? trimmed : void 0;
63
84
  }
64
- var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "docker"];
85
+ var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "agent37", "docker"];
65
86
  var sandboxDriverCredentialFields = {
66
87
  modal: [
67
88
  { key: "token_id", label: "Token ID", secret: true },
@@ -77,6 +98,7 @@ var sandboxDriverCredentialFields = {
77
98
  { key: "worker_url", label: "Worker URL" }
78
99
  ],
79
100
  boat: [{ key: "api_key", label: "API Key", secret: true }],
101
+ agent37: [{ key: "api_key", label: "API Key", secret: true }],
80
102
  docker: [{ key: "image", label: "Image" }]
81
103
  };
82
104
  var sandboxDriverLabels = {
@@ -84,6 +106,7 @@ var sandboxDriverLabels = {
84
106
  vercel: "Vercel",
85
107
  cloudflare: "Cloudflare",
86
108
  boat: "Boat",
109
+ agent37: "Agent37",
87
110
  docker: "Docker"
88
111
  };
89
112
  function isSandboxDriverID(input) {
@@ -117,6 +140,10 @@ function sandboxDriverAuthValues(cfg, id, env = process.env) {
117
140
  const api_key = trimToUndefined(cfg?.auth?.boat?.api_key) ?? trimToUndefined(env.BOAT_API_KEY);
118
141
  return api_key ? { api_key } : void 0;
119
142
  }
143
+ if (id === "agent37") {
144
+ const api_key = trimToUndefined(cfg?.auth?.agent37?.api_key) ?? trimToUndefined(env.AGENT37_API_KEY);
145
+ return api_key ? { api_key } : void 0;
146
+ }
120
147
  if (!dockerSandboxDriverEnabled(env))
121
148
  return;
122
149
  const image = trimToUndefined(cfg?.auth?.docker?.image) ?? trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? trimToUndefined(env.CLAXEDO_SANDBOX_IMAGE);
@@ -233,8 +260,8 @@ var sandboxDriverCatalog = {
233
260
  hostWatchesIdle: true,
234
261
  targetAccess: "relay",
235
262
  secretBrokering: "native",
236
- // Credential handlers do not restrict unrelated destinations.
237
- egressControl: "none",
263
+ // The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
264
+ egressControl: "hosts",
238
265
  // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
239
266
  persistence: {
240
267
  resume: "replacement-restore",
@@ -272,6 +299,33 @@ var sandboxDriverCatalog = {
272
299
  machineClasses: ["small", "default", "large"]
273
300
  }
274
301
  },
302
+ agent37: {
303
+ id: "agent37",
304
+ label: sandboxDriverLabels.agent37,
305
+ credentialFields: sandboxDriverCredentialFields.agent37,
306
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
307
+ runtimeEnv: "callback",
308
+ preparedImage: true,
309
+ billing: agent37Billing,
310
+ metadata: {
311
+ driverRunsIn: ["worker", "node"],
312
+ hostStopBehavior: "suspends-host",
313
+ hostResumeBehavior: "same-host",
314
+ targetAccess: "relay",
315
+ secretBrokering: "none",
316
+ // An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
317
+ egressControl: "hosts-and-cidrs",
318
+ persistence: {
319
+ resume: "same-sandbox",
320
+ capture: "none",
321
+ clone: false,
322
+ captureSource: "not-applicable",
323
+ retention: "provider-managed",
324
+ restoreMount: "same-resource"
325
+ },
326
+ machineClasses: ["small", "default", "large"]
327
+ }
328
+ },
275
329
  docker: {
276
330
  id: "docker",
277
331
  label: sandboxDriverLabels.docker,
@@ -7,8 +7,9 @@ export declare function leaseHostId(driver: SandboxDriver, workspaceId: string,
7
7
  * what makes the egress guarantee structural rather than a promise kept by each
8
8
  * call site: `net` is copied across only when `sandboxEgressDisposition` says
9
9
  * the driver can enforce it. A driver declaring `egressControl: "none"` — every
10
- * driver that throws on a restricted policy, plus the two that silently drop it
11
- * — cannot be reached by one through any path.
10
+ * one of which throws on a restricted policy — cannot be reached by one through
11
+ * any path; `admit` has already refused the start, or the start allowed it to
12
+ * run uncontained.
12
13
  */
13
14
  export declare function ensureHostInput(input: {
14
15
  driver: SandboxDriver;
@@ -0,0 +1,87 @@
1
+ export type Agent37Fetch = (input: string, init?: RequestInit) => Promise<Response>;
2
+ declare const INSTANCE_STATUSES: readonly ["provisioning", "running", "stopping", "stopped", "starting", "restarting", "updating", "sleeping", "waking", "failed", "deleting", "deleted"];
3
+ export type Agent37InstanceStatus = (typeof INSTANCE_STATUSES)[number];
4
+ export type Agent37Resources = {
5
+ cpu: number;
6
+ memory: number;
7
+ disk: number;
8
+ };
9
+ /** `"deny-all"` cuts DNS too; an `allow` entry is a domain (HTTPS on 443 only) or an IPv4 address or range. */
10
+ export type Agent37Network = "allow-all" | "deny-all" | {
11
+ allow: string[];
12
+ };
13
+ export type Agent37Instance = {
14
+ id: string;
15
+ status: Agent37InstanceStatus;
16
+ /** The registry reference the instance's image was registered from. */
17
+ imageRef?: string;
18
+ resources: Agent37Resources;
19
+ metadata: Record<string, string>;
20
+ /** Each port's permanent URL that asks for no credential. */
21
+ publicPorts: {
22
+ port: number;
23
+ url: string;
24
+ }[];
25
+ };
26
+ export type Agent37ContainerHealth = {
27
+ logs: string;
28
+ running: boolean;
29
+ restartCount: number;
30
+ };
31
+ export declare class Agent37ApiError extends Error {
32
+ readonly code: string;
33
+ readonly status?: number | undefined;
34
+ /** Agent37 answers these to the key itself; an instance of another workspace reads as `not_found`. */
35
+ readonly providerKeyRefused: boolean;
36
+ constructor(message: string, code: string, status?: number | undefined);
37
+ }
38
+ export type Agent37ClientOptions = {
39
+ apiKey: string;
40
+ baseUrl?: string;
41
+ timeoutMs?: number;
42
+ fetchImpl?: Agent37Fetch;
43
+ };
44
+ export declare function createAgent37Client(options: Agent37ClientOptions): {
45
+ templateExists(name: string): Promise<boolean>;
46
+ /** A name another registration took first holds the same image and command, since the name is derived from both. */
47
+ registerTemplate(input: {
48
+ name: string;
49
+ imageRef: string;
50
+ entrypoint: string[];
51
+ }): Promise<void>;
52
+ create(input: {
53
+ template: string;
54
+ name: string;
55
+ metadata: Record<string, string>;
56
+ env: Record<string, string>;
57
+ network: Agent37Network;
58
+ resources: Agent37Resources;
59
+ publicPort: number;
60
+ mountPath: string;
61
+ }): Promise<Agent37Instance>;
62
+ get(id: string): Promise<Agent37Instance>;
63
+ list(): Promise<Agent37Instance[]>;
64
+ /** `env` reaches the container at its next start, restart, update or resize; `network` applies to a running one within seconds; `metadata` replaces what is stored. */
65
+ edit(id: string, input: {
66
+ env?: Record<string, string>;
67
+ network?: Agent37Network;
68
+ metadata?: Record<string, string>;
69
+ }): Promise<Agent37Instance>;
70
+ start: (id: string) => Promise<"deleted" | "deleting" | "failed" | "provisioning" | "restarting" | "running" | "sleeping" | "starting" | "stopped" | "stopping" | "updating" | "waking">;
71
+ stop: (id: string) => Promise<"deleted" | "deleting" | "failed" | "provisioning" | "restarting" | "running" | "sleeping" | "starting" | "stopped" | "stopping" | "updating" | "waking">;
72
+ restart: (id: string) => Promise<"deleted" | "deleting" | "failed" | "provisioning" | "restarting" | "running" | "sleeping" | "starting" | "stopped" | "stopping" | "updating" | "waking">;
73
+ /** Moves the instance onto `template`, keeping only its mount path; a stopped one stays stopped and boots that image at its next start. */
74
+ update: (id: string, template: string) => Promise<"deleted" | "deleting" | "failed" | "provisioning" | "restarting" | "running" | "sleeping" | "starting" | "stopped" | "stopping" | "updating" | "waking">;
75
+ /** Recreates the running container at the new size. Disk only grows. */
76
+ resize: (id: string, resources: Agent37Resources) => Promise<"deleted" | "deleting" | "failed" | "provisioning" | "restarting" | "running" | "sleeping" | "starting" | "stopped" | "stopping" | "updating" | "waking">;
77
+ publishPort(id: string, port: number): Promise<string>;
78
+ delete(id: string): Promise<void>;
79
+ /** Runs through `sh -c` as the image's user; a command that exits nonzero is an answer, not an error. */
80
+ exec(id: string, command: string): Promise<{
81
+ exitCode: number;
82
+ stdout: string;
83
+ }>;
84
+ /** Answers in any state; Agent37 starts a container that exited again, and `restartCount` counts each boot after the first. */
85
+ containerHealth(id: string, tail: number): Promise<Agent37ContainerHealth>;
86
+ };
87
+ export {};
@@ -0,0 +1,25 @@
1
+ import { type SandboxDriver, type SandboxDriverEnsureInput } from "../contract";
2
+ import { type WorkspaceRuntimeControlEnv } from "../runtime-env";
3
+ import { type Agent37Fetch } from "./agent37-client";
4
+ export type { Agent37Fetch };
5
+ export type Agent37SandboxDriverOptions = {
6
+ apiKey: string;
7
+ baseUrl?: string;
8
+ image: string;
9
+ runtimePort?: number;
10
+ runtimeCommand?: string;
11
+ workspaceDir?: string;
12
+ nativeHarness?: string;
13
+ controlEnv?: WorkspaceRuntimeControlEnv;
14
+ env?: (input: SandboxDriverEnsureInput, host: {
15
+ id: string;
16
+ }) => Record<string, string> | Promise<Record<string, string>>;
17
+ provisionTimeoutMs?: number;
18
+ healthTimeoutMs?: number;
19
+ healthIntervalMs?: number;
20
+ /** How long a boot waits between two reads of its container's restart count. */
21
+ containerCheckMs?: number;
22
+ operationTimeoutMs?: number;
23
+ fetchImpl?: Agent37Fetch;
24
+ };
25
+ export declare function createAgent37SandboxDriver(options: Agent37SandboxDriverOptions): SandboxDriver;