@agent-relay/sandbox 0.1.2 → 0.1.5

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.
Files changed (161) hide show
  1. package/README.md +247 -5
  2. package/dist/agent37/client.d.ts +115 -0
  3. package/dist/agent37/client.d.ts.map +1 -0
  4. package/dist/agent37/client.js +270 -0
  5. package/dist/agent37/client.js.map +1 -0
  6. package/dist/agent37/index.d.ts +5 -0
  7. package/dist/agent37/index.d.ts.map +1 -0
  8. package/dist/agent37/index.js +3 -0
  9. package/dist/agent37/index.js.map +1 -0
  10. package/dist/agent37/runtime.d.ts +415 -0
  11. package/dist/agent37/runtime.d.ts.map +1 -0
  12. package/dist/agent37/runtime.js +855 -0
  13. package/dist/agent37/runtime.js.map +1 -0
  14. package/dist/agentcore/bench.d.ts +93 -0
  15. package/dist/agentcore/bench.d.ts.map +1 -0
  16. package/dist/agentcore/bench.js +125 -0
  17. package/dist/agentcore/bench.js.map +1 -0
  18. package/dist/agentcore/capabilities.d.ts +169 -0
  19. package/dist/agentcore/capabilities.d.ts.map +1 -0
  20. package/dist/agentcore/capabilities.js +153 -0
  21. package/dist/agentcore/capabilities.js.map +1 -0
  22. package/dist/agentcore/config.d.ts +156 -0
  23. package/dist/agentcore/config.d.ts.map +1 -0
  24. package/dist/agentcore/config.js +12 -0
  25. package/dist/agentcore/config.js.map +1 -0
  26. package/dist/agentcore/index.d.ts +6 -0
  27. package/dist/agentcore/index.d.ts.map +1 -0
  28. package/dist/agentcore/index.js +3 -0
  29. package/dist/agentcore/index.js.map +1 -0
  30. package/dist/agentcore/internal/sdk.d.ts +181 -0
  31. package/dist/agentcore/internal/sdk.d.ts.map +1 -0
  32. package/dist/agentcore/internal/sdk.js +223 -0
  33. package/dist/agentcore/internal/sdk.js.map +1 -0
  34. package/dist/agentcore/runtime.d.ts +177 -0
  35. package/dist/agentcore/runtime.d.ts.map +1 -0
  36. package/dist/agentcore/runtime.js +706 -0
  37. package/dist/agentcore/runtime.js.map +1 -0
  38. package/dist/core/index.d.ts +13 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +11 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/daytona/index.d.ts +5 -0
  43. package/dist/daytona/index.d.ts.map +1 -0
  44. package/dist/daytona/index.js +3 -0
  45. package/dist/daytona/index.js.map +1 -0
  46. package/dist/daytona/runtime.d.ts +23 -0
  47. package/dist/daytona/runtime.d.ts.map +1 -1
  48. package/dist/daytona/runtime.js +167 -5
  49. package/dist/daytona/runtime.js.map +1 -1
  50. package/dist/daytona/wire-supplement.d.ts +25 -0
  51. package/dist/daytona/wire-supplement.d.ts.map +1 -0
  52. package/dist/daytona/wire-supplement.js +16 -0
  53. package/dist/daytona/wire-supplement.js.map +1 -0
  54. package/dist/e2b/index.d.ts +3 -0
  55. package/dist/e2b/index.d.ts.map +1 -0
  56. package/dist/e2b/index.js +2 -0
  57. package/dist/e2b/index.js.map +1 -0
  58. package/dist/e2b/runtime.d.ts +107 -72
  59. package/dist/e2b/runtime.d.ts.map +1 -1
  60. package/dist/e2b/runtime.js +675 -190
  61. package/dist/e2b/runtime.js.map +1 -1
  62. package/dist/freestyle/capabilities.d.ts +28 -0
  63. package/dist/freestyle/capabilities.d.ts.map +1 -0
  64. package/dist/freestyle/capabilities.js +42 -0
  65. package/dist/freestyle/capabilities.js.map +1 -0
  66. package/dist/freestyle/config.d.ts +36 -0
  67. package/dist/freestyle/config.d.ts.map +1 -0
  68. package/dist/freestyle/config.js +2 -0
  69. package/dist/freestyle/config.js.map +1 -0
  70. package/dist/freestyle/index.d.ts +6 -0
  71. package/dist/freestyle/index.d.ts.map +1 -0
  72. package/dist/freestyle/index.js +3 -0
  73. package/dist/freestyle/index.js.map +1 -0
  74. package/dist/freestyle/internal/sdk.d.ts +69 -0
  75. package/dist/freestyle/internal/sdk.d.ts.map +1 -0
  76. package/dist/freestyle/internal/sdk.js +19 -0
  77. package/dist/freestyle/internal/sdk.js.map +1 -0
  78. package/dist/freestyle/runtime.d.ts +163 -0
  79. package/dist/freestyle/runtime.d.ts.map +1 -0
  80. package/dist/freestyle/runtime.js +798 -0
  81. package/dist/freestyle/runtime.js.map +1 -0
  82. package/dist/index.d.ts +34 -13
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +19 -10
  85. package/dist/index.js.map +1 -1
  86. package/dist/local/index.d.ts +3 -0
  87. package/dist/local/index.d.ts.map +1 -0
  88. package/dist/local/index.js +2 -0
  89. package/dist/local/index.js.map +1 -0
  90. package/dist/microsandbox/index.d.ts +3 -0
  91. package/dist/microsandbox/index.d.ts.map +1 -0
  92. package/dist/microsandbox/index.js +2 -0
  93. package/dist/microsandbox/index.js.map +1 -0
  94. package/dist/microsandbox/runtime.d.ts +793 -0
  95. package/dist/microsandbox/runtime.d.ts.map +1 -0
  96. package/dist/microsandbox/runtime.js +2303 -0
  97. package/dist/microsandbox/runtime.js.map +1 -0
  98. package/dist/modal/bench.d.ts +199 -0
  99. package/dist/modal/bench.d.ts.map +1 -0
  100. package/dist/modal/bench.js +450 -0
  101. package/dist/modal/bench.js.map +1 -0
  102. package/dist/modal/capabilities.d.ts +180 -0
  103. package/dist/modal/capabilities.d.ts.map +1 -0
  104. package/dist/modal/capabilities.js +159 -0
  105. package/dist/modal/capabilities.js.map +1 -0
  106. package/dist/modal/config.d.ts +192 -0
  107. package/dist/modal/config.d.ts.map +1 -0
  108. package/dist/modal/config.js +169 -0
  109. package/dist/modal/config.js.map +1 -0
  110. package/dist/modal/index.d.ts +9 -0
  111. package/dist/modal/index.d.ts.map +1 -0
  112. package/dist/modal/index.js +5 -0
  113. package/dist/modal/index.js.map +1 -0
  114. package/dist/modal/internal/sdk.d.ts +141 -0
  115. package/dist/modal/internal/sdk.d.ts.map +1 -0
  116. package/dist/modal/internal/sdk.js +51 -0
  117. package/dist/modal/internal/sdk.js.map +1 -0
  118. package/dist/modal/runtime.d.ts +271 -0
  119. package/dist/modal/runtime.d.ts.map +1 -0
  120. package/dist/modal/runtime.js +871 -0
  121. package/dist/modal/runtime.js.map +1 -0
  122. package/dist/mount-script.d.ts +16 -1
  123. package/dist/mount-script.d.ts.map +1 -1
  124. package/dist/mount-script.js +76 -15
  125. package/dist/mount-script.js.map +1 -1
  126. package/dist/orchestrator.d.ts +15 -0
  127. package/dist/orchestrator.d.ts.map +1 -1
  128. package/dist/orchestrator.js +13 -1
  129. package/dist/orchestrator.js.map +1 -1
  130. package/dist/port.d.ts +144 -6
  131. package/dist/port.d.ts.map +1 -1
  132. package/dist/port.js +27 -5
  133. package/dist/port.js.map +1 -1
  134. package/dist/types.d.ts +50 -1
  135. package/dist/types.d.ts.map +1 -1
  136. package/dist/vercel/bench.d.ts +169 -0
  137. package/dist/vercel/bench.d.ts.map +1 -0
  138. package/dist/vercel/bench.js +337 -0
  139. package/dist/vercel/bench.js.map +1 -0
  140. package/dist/vercel/capabilities.d.ts +106 -0
  141. package/dist/vercel/capabilities.d.ts.map +1 -0
  142. package/dist/vercel/capabilities.js +128 -0
  143. package/dist/vercel/capabilities.js.map +1 -0
  144. package/dist/vercel/config.d.ts +119 -0
  145. package/dist/vercel/config.d.ts.map +1 -0
  146. package/dist/vercel/config.js +8 -0
  147. package/dist/vercel/config.js.map +1 -0
  148. package/dist/vercel/index.d.ts +6 -0
  149. package/dist/vercel/index.d.ts.map +1 -0
  150. package/dist/vercel/index.js +3 -0
  151. package/dist/vercel/index.js.map +1 -0
  152. package/dist/vercel/internal/sdk.d.ts +173 -0
  153. package/dist/vercel/internal/sdk.d.ts.map +1 -0
  154. package/dist/vercel/internal/sdk.js +117 -0
  155. package/dist/vercel/internal/sdk.js.map +1 -0
  156. package/dist/vercel/runtime.d.ts +305 -0
  157. package/dist/vercel/runtime.d.ts.map +1 -0
  158. package/dist/vercel/runtime.js +1169 -0
  159. package/dist/vercel/runtime.js.map +1 -0
  160. package/docs/freestyle.md +110 -0
  161. package/package.json +41 -5
package/README.md CHANGED
@@ -10,10 +10,8 @@ run commands, stream results back, and tear it down.
10
10
  The point of the port is that the orchestration layer holds no provider
11
11
  knowledge. Swapping providers is a change of adapter, not a change of caller.
12
12
 
13
- > **Status: pre-release scaffold.** This repository currently contains the
14
- > package skeleton — build, typecheck, test, and release wiring. The runtime
15
- > adapters and orchestrator land in subsequent changes, and the public API is
16
- > not stable until a `1.0.0` release.
13
+ > **Status: pre-1.0.** The public API may still change before a `1.0.0`
14
+ > release.
17
15
 
18
16
  ## Install
19
17
 
@@ -23,6 +21,68 @@ npm install @agent-relay/sandbox
23
21
 
24
22
  Provider SDKs are peer dependencies: install the one you intend to use. A
25
23
  consumer that only runs local sandboxes does not need a remote provider SDK.
24
+ Adapters for providers that publish no JavaScript SDK speak their HTTP API
25
+ directly and add no dependency at all; they take an injectable `fetch` instead.
26
+
27
+ Import provider-neutral APIs and the selected adapter from separate entrypoints:
28
+
29
+ ```ts
30
+ import { SandboxOrchestrator } from "@agent-relay/sandbox/core";
31
+ import { DaytonaRuntime } from "@agent-relay/sandbox/daytona";
32
+ ```
33
+
34
+ The provider entrypoints are `agent37`, `agentcore`, `daytona`, `e2b`,
35
+ `freestyle`, `local`, `microsandbox`, `modal`, and `vercel`. Each points at its
36
+ own barrel, so a bundler does not have to resolve the other optional provider SDKs.
37
+ The root `@agent-relay/sandbox` barrel still exports the complete historical
38
+ surface for compatibility, but new runtime imports should use `core` plus one
39
+ provider subpath.
40
+
41
+ The export map uses `./* -> ./dist/*/index.js` rather than enumerating today’s
42
+ providers. To add another provider safely, add `src/<provider>/index.ts` as its
43
+ isolated public barrel and declare its SDK as an optional peer. The package
44
+ contract test requires every runtime directory (and every optional peer owner)
45
+ to have such a barrel, and the packed-package smoke test verifies those barrels
46
+ ship and import without installing any optional peers.
47
+
48
+ ### Provider constraints
49
+
50
+ Each adapter inherits its provider SDK's requirements, and they are not all the
51
+ same as this package's:
52
+
53
+ | Adapter | Peer dependency | Requirements beyond this package's |
54
+ | --- | --- | --- |
55
+ | `DaytonaRuntime` | `@daytonaio/sdk` 0.180–0.205 | — |
56
+ | `E2BSandboxRuntime` | `e2b` | — |
57
+ | `MicrosandboxRuntime` | `microsandbox` | **Node.js 22+**, a platform-specific native addon (macOS arm64, Linux x64/arm64, Windows x64/arm64), and — for its `local` backend — hardware virtualization: KVM on Linux, Apple Silicon on macOS, or WHP on Windows 10+ |
58
+ | `LocalSandboxRuntime` | — | A reachable local sandbox service |
59
+
60
+ The package itself keeps a Node 20 floor, because a consumer that never touches
61
+ the microsandbox adapter never loads that SDK: it is imported lazily, at first
62
+ use, and a load failure is reported with the constraint that most often
63
+ explains it.
64
+
65
+ ### Microsandbox capabilities are backend-sensitive
66
+
67
+ `MicrosandboxRuntime.capabilities` is derived from the backend the instance is
68
+ bound to, not reported as a single process-wide constant:
69
+
70
+ | Capability | `local` | `cloud` | Why |
71
+ | --- | --- | --- | --- |
72
+ | `snapshots` | `true` | `false` | A snapshot is a host-local artifact: the installed SDK's typings describe `Snapshot` as an artifact on disk and resolve one under `~/.microsandbox/snapshots/<name>/`. This adapter consumes such an artifact from the calling host and never transfers it, so a create issued against a remote backend has nothing to resolve. Configuring `snapshot` with a cloud backend is refused in the constructor, before any SDK call. |
73
+ | `isolation` | `'strong'` | `'unknown'` | Locally the SDK boots a microVM with its own guest kernel on a virtualization-capable host, and the installed package states that requirement itself, so `'strong'` rests on something checkable here. This adapter observes and measures nothing about the cloud backend's isolation. |
74
+
75
+ Both values describe what this package has **established**, not what any
76
+ provider documents. `'unknown'` is not a synonym for weak and is not a claim
77
+ that the guarantee is missing — it means this package has not established one,
78
+ so a caller that requires a specific guarantee must decide for itself rather
79
+ than read an unverified `'strong'`.
80
+
81
+ Cloud region placement and resource enforcement are likewise not represented as
82
+ measured facts. Custom or published **ports are not supported**: the SDK builder
83
+ exposes `port()`/`portBind()`, but the ports this package targets have no
84
+ public-port surface, so the adapter never calls them and never implies a
85
+ reachable port.
26
86
 
27
87
  ## Design
28
88
 
@@ -40,6 +100,187 @@ is a required argument supplied by the caller. This keeps the package usable
40
100
  outside the environment it was extracted from, and keeps credential handling in
41
101
  the caller where it belongs.
42
102
 
103
+ Each adapter also declares what it genuinely supports rather than what its
104
+ method names imply. `resolveSandboxRuntimeCapabilities` reads that declaration,
105
+ so a caller learns up front whether a provider can reattach to a sandbox by id,
106
+ poll a background command, hand back a still-booting sandbox, or search by
107
+ label — instead of discovering the answer from a failure at run time.
108
+
109
+ ### Daytona restart recovery
110
+
111
+ `DaytonaRuntime.start()` does not trust the provider state transition alone. It
112
+ rehydrates the SDK sandbox and runs a bounded `true` readiness probe after `start`,
113
+ because Daytona can report `STARTED` while its Toolbox exec daemon remains
114
+ unavailable. A healthy restart keeps the same sandbox ID. A failure during that
115
+ rehydration itself (auth, rate limit, or network) is not proof the exec daemon
116
+ is dead, so it is propagated as-is and never triggers a replacement.
117
+
118
+ When the post-start readiness probe fails, the runtime defaults to creating and proving
119
+ a replacement before deleting the unusable sandbox. The returned handle is
120
+ updated in place and can therefore have a new `id`; callers must persist that
121
+ returned ID. The replacement preserves the configured snapshot plus provider
122
+ labels, environment, lifecycle, volume, and network settings, but non-volume
123
+ filesystem changes in the old sandbox are not copied. Stateful callers that
124
+ prefer a hard failure to that trade-off can set `recreateOnFailedStart: false`.
125
+
126
+ ### E2B runtime contract
127
+
128
+ `E2BSandboxRuntime` implements both the outer orchestration port and the live
129
+ `WorkflowRuntime` surface: metadata/state lookup, reattachment, synchronous and
130
+ durable asynchronous execution, upload/download, home-directory resolution,
131
+ pause/resume, and owned-resource teardown. It deliberately does not expose a
132
+ detached-launch method because E2B's public create call waits for a running
133
+ sandbox. Its live capability descriptor likewise reports PTY and streaming logs
134
+ as unsupported because this adapter exposes neither behavior, even though the
135
+ provider SDK has lower-level APIs for them.
136
+
137
+ The adapter applies an explicit sandbox lifetime on create, reconnect, and
138
+ synchronous use; `sandboxLifetimeMs` defaults to the configured asynchronous
139
+ run budget.
140
+
141
+ Synchronous runs always carry an explicit command lifetime cap, because E2B —
142
+ alone among the providers here — applies a 60-second default when the field is
143
+ omitted, while the orchestrator omits `timeoutMs` on most execs. A run without a
144
+ caller timeout therefore uses `syncRunBudgetMs`, which defaults to the
145
+ configured `runBudgetMs` (30 minutes unless set). An explicit caller timeout
146
+ always wins, and is the only thing that extends the sandbox past
147
+ `sandboxLifetimeMs`; the implicit budget never does.
148
+
149
+ Asynchronous session IDs are immutable idempotency keys. A retry
150
+ reconciles the durable admission record with E2B's process list and will not
151
+ erase the session directory or submit another copy. Status is pending only
152
+ while the matching provider process is present. If that process disappears
153
+ without publishing its exit sidecar, status becomes terminal with
154
+ `E2B_ASYNC_PROCESS_LOST_EXIT_CODE` (`255`).
155
+
156
+ ### Daytona wire-supplement
157
+
158
+ Daytona's Sandbox wire response carries two fields — `sandboxClass` (the
159
+ sandbox's class/tier) and `warmPoolId` (set while a sandbox is an unclaimed
160
+ warm-pool member) — that exist on the low-level `@daytona/api-client` DTOs but
161
+ that the vendored `@daytonaio/sdk`'s `Sandbox` class does not copy onto
162
+ itself. `DaytonaRuntime.getWireSupplement(handle)` fetches both directly via
163
+ the SDK's low-level `sandboxApi`, the same reach pattern `runtime.ts` already
164
+ uses for detached create.
165
+
166
+ This is a narrow, deliberately temporary gap, not a fork of the SDK: the
167
+ other fields once suspected missing (`autoDestroyAt`, `autoPauseInterval`,
168
+ `spot`) already ship on the public `Sandbox` class as of `@daytonaio/sdk`
169
+ 0.200.0–0.205.0 — a dependency bump alone covers those. Tracked upstream at
170
+ [daytona/clients#207](https://github.com/daytona/clients/issues/207)
171
+ (precedent: [#192](https://github.com/daytona/clients/pull/192), which added
172
+ `spot` the same way). Retire `src/daytona/wire-supplement.ts` once
173
+ `processSandboxDto()` copies `sandboxClass`/`warmPoolId` and a subsequent SDK
174
+ bump picks that up — `runtime.test.ts`'s `DaytonaRuntime smoke` suite has a
175
+ load-bearing regression test that fails once that happens.
176
+ ### Freestyle runtime contract
177
+
178
+ `FreestyleRuntime` uses an explicit API key, home directory, ownership-name
179
+ prefix, and persistence policy. It never reads ambient credentials. Freestyle
180
+ does not expose labels on VM creation, so label lookup and warm leasing remain
181
+ unsupported; ownership and cleanup are scoped to collision-safe names under the
182
+ configured prefix. Deleted list rows are treated as gone only when the provider
183
+ sets `deleted: true`.
184
+
185
+ The adapter exposes buffered exec, file transfer, reattachment, owned-resource
186
+ listing, and verified deletion. Stop/start methods exist as a conservative
187
+ probe surface, but lifecycle remains undeclared because the live validation
188
+ account could not create a persistent VM. PTY, snapshots, streaming logs, fork,
189
+ and never-idle behavior are likewise not advertised without the required live
190
+ proof through this package's public port.
191
+
192
+ The official SDK is isolated under `src/freestyle/internal/`; public
193
+ configuration and capability metadata do not import vendor types. All create,
194
+ lookup, exec, lifecycle, and deletion operations have explicit deadlines. See
195
+ [the Freestyle adapter notes](./docs/freestyle.md) for dependency provenance,
196
+ provider constraints, and capability evidence.
197
+
198
+ ### Vercel Sandbox runtime contract
199
+
200
+ `VercelSandboxRuntime` takes an explicit token, team id, project id, ownership
201
+ name prefix, and home directory, and passes credentials on every SDK call so the
202
+ vendor's own environment and on-disk OAuth fallbacks can never supply an
203
+ operator's identity.
204
+
205
+ A Vercel sandbox is addressed by its **name**, not an opaque id, so
206
+ `RuntimeHandle.id` carries the name and the configured prefix is a real
207
+ ownership boundary: stop, start, and delete refuse any name outside it.
208
+ `Sandbox.list` supports server-side `namePrefix` and `tags` filters, so label
209
+ lookup is a real search here — but every row is re-checked against the requested
210
+ tags in process, because a server filter that were ever ignored would hand back
211
+ someone else's sandbox as a warm lease. `getById` is implemented over `list`
212
+ rather than `Sandbox.get`, which resumes and bills the sandbox as a side effect.
213
+
214
+ Deletion is verified by absence, and a row that reappears under the same name
215
+ with a different `createdAt` counts as proof our sandbox is gone rather than as
216
+ a survivor. Failed verification retains the registration so cleanup stays
217
+ retryable. `acquire()` returns an `AsyncDisposable` for `await using` scopes.
218
+
219
+ Commands run as `sh -c` with `cwd` and `env` passed through provider fields, so
220
+ no caller value is spliced into a command string. Every operation carries its
221
+ own deadline, under an absolute `retryDeadlineMs` ceiling that spans the SDK's
222
+ internal retries.
223
+
224
+ Warm leasing, lifecycle, PTY, snapshots, streaming logs, fork, and verified
225
+ cleanup are all declared `false` pending live evidence; never-idle is settled
226
+ false, because every Vercel sandbox carries a termination deadline. The official
227
+ SDK is isolated under `src/vercel/internal/`. See
228
+ [the Vercel adapter notes](./docs/vercel.md) for dependency provenance,
229
+ capability evidence, and the active-CPU-versus-wall-clock pricing analysis.
230
+
231
+ Capabilities are also declared structurally. PTY and snapshots resolve to
232
+ `"not-exposed"` — real in the SDK, unreachable through this port, and so not
233
+ something a live probe may promote — while `lifetime` resolves to `"deadline"`,
234
+ the settled reason a Vercel sandbox has no never-idle tier. `filesystem` is
235
+ deliberately left `"unknown"`: durability is per-instance configuration
236
+ (`persistent`), and surviving a stop/resume is the same round trip `lifecycle`
237
+ is still awaiting live proof of.
238
+
239
+ ### Modal runtime contract
240
+
241
+ `ModalRuntime` takes an explicit Modal **token pair** (`tokenId` and
242
+ `tokenSecret` — Modal does not use a single bearer key), an App name, an image
243
+ tag, a home directory, and an ownership-name prefix. It never reads ambient
244
+ credentials or a local Modal profile.
245
+
246
+ A Modal Sandbox is a child of an App, built from an Image, and it has a
247
+ **maximum lifetime after which the provider terminates it** — the SDK's own
248
+ default is five minutes. `maxLifetimeMs` is therefore required configuration and
249
+ is always sent explicitly. `createTimeoutSeconds` on `launch` is a deadline on
250
+ the create call and is deliberately not forwarded to that lifetime.
251
+
252
+ Modal exposes no stop/start for a Sandbox; `terminate` is the only lifecycle
253
+ transition and it is terminal. `start` and `stop` are absent rather than
254
+ no-ops, and `lifecycle` is declared false permanently. Async exec is likewise
255
+ not implemented: Modal cannot re-resolve a running exec by id, so the
256
+ `startScript`/`getScriptStatus`/`getScriptLogs` trio is omitted entirely instead
257
+ of being half-supported.
258
+
259
+ Ownership rides on Modal's native server-side tags rather than on a naming
260
+ convention. Every sandbox carries an ownership tag, every lookup filters on it
261
+ server-side, and reattachment and deletion both re-check it. Warm leasing is
262
+ implemented against that real tag filter but remains undeclared until a live
263
+ probe confirms it. Snapshots, volumes, PTY, and tunnels exist in the provider
264
+ and are documented, but are not advertised because this package's port exposes
265
+ no operation for them.
266
+
267
+ That last distinction is now stated structurally rather than in prose. The
268
+ adapter declares `declaredCapabilityModes`, so PTY and snapshots resolve to
269
+ `"not-exposed"` — a fact about this package's port, which `isPendingEvidence()`
270
+ reports as unmovable — rather than to a bare `false` a later canary might read
271
+ as merely unverified. `lifetime` resolves to `"deadline"`, which is the
272
+ structural reason a Modal sandbox can never be never-idle. Output is
273
+ `"buffered"`: Modal streams, the adapter drains. Warm leasing deliberately gets
274
+ no mode, because modes describe a capability's shape and not its verification
275
+ state.
276
+
277
+ The official SDK is isolated under `src/modal/internal/`, and because that SDK
278
+ speaks gRPC rather than HTTP there is no injectable transport seam; the boundary
279
+ is a structural mirror that is checked at build time instead. All create,
280
+ lookup, exec, upload, and deletion operations have explicit deadlines. See
281
+ [the Modal adapter notes](./docs/modal.md) for dependency provenance, provider
282
+ constraints, cost model, and capability evidence.
283
+
43
284
  ## Development
44
285
 
45
286
  ```bash
@@ -49,7 +290,8 @@ npm run typecheck
49
290
  npm test # node:test
50
291
  ```
51
292
 
52
- Requires Node.js 20 or newer.
293
+ Requires Node.js 20 or newer. The microsandbox adapter's own tests need Node 22+
294
+ to load the real SDK; without it, its SDK-contract checks skip rather than fail.
53
295
 
54
296
  ## Releasing
55
297
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Minimal HTTP client for the Agent37 Cloud API.
3
+ *
4
+ * There is no official Agent37 JavaScript SDK to depend on: the only published
5
+ * npm package (`agent37`) is a command-line interface that exposes a `bin`, no
6
+ * library entry point, and no runtime dependencies. So this adapter speaks the
7
+ * documented HTTP contract directly over `fetch`, and takes an injectable
8
+ * `fetch` seam instead of a peer dependency. That is deliberate — an optional
9
+ * peer dependency pointing at a CLI would be a dependency on something that
10
+ * cannot be imported.
11
+ *
12
+ * Agent37 exposes two planes that share one credential but not one header:
13
+ *
14
+ * - the **hosting plane** (`{baseUrl}/v1/instances/...`), authenticated with
15
+ * `Authorization: Bearer <key>`, which creates, lists, execs, and tears down
16
+ * instances; and
17
+ * - the **instance plane** (the `url` an instance object reports),
18
+ * authenticated with `X-Agent37-Key: <key>`, which serves that one
19
+ * instance's filesystem.
20
+ *
21
+ * The instance plane's origin is never constructed here. It is read off the
22
+ * instance object, so no provider hostname is baked into this package.
23
+ */
24
+ /** Response shape this client needs. Structurally satisfied by global `Response`. */
25
+ export type Agent37FetchResponse = {
26
+ readonly ok: boolean;
27
+ readonly status: number;
28
+ text(): Promise<string>;
29
+ arrayBuffer(): Promise<ArrayBuffer>;
30
+ };
31
+ /** Request shape this client sends. Structurally satisfied by global `RequestInit`. */
32
+ export type Agent37FetchInit = {
33
+ method?: string;
34
+ headers?: Record<string, string>;
35
+ body?: string | Uint8Array;
36
+ signal?: AbortSignal;
37
+ };
38
+ /** Injection seam for the transport. Global `fetch` is assignable to this. */
39
+ export type Agent37Fetch = (url: string, init?: Agent37FetchInit) => Promise<Agent37FetchResponse>;
40
+ /**
41
+ * A non-2xx response from either Agent37 plane.
42
+ *
43
+ * `code` is the machine-readable identifier the API documents callers should
44
+ * branch on — never the message text, and never the HTTP status alone, because
45
+ * one status carries several codes (409 is both `try_again` and
46
+ * `instance_limit_reached`).
47
+ */
48
+ export declare class Agent37ApiError extends Error {
49
+ readonly status: number;
50
+ readonly code: string;
51
+ /** Method and path only. Never the query string, body, or any header. */
52
+ readonly request: string;
53
+ readonly retryable: boolean;
54
+ constructor(options: {
55
+ status: number;
56
+ code: string;
57
+ message: string;
58
+ request: string;
59
+ retryable: boolean;
60
+ });
61
+ }
62
+ export type Agent37ClientOptions = {
63
+ /**
64
+ * An `sk_live_` workspace key. Required, and never read from the environment
65
+ * by this package: credential sourcing belongs to the caller.
66
+ */
67
+ apiKey: string;
68
+ /**
69
+ * Origin of the Agent37 hosting API, without a trailing slash — for example
70
+ * the provider's public control plane, or a proxy in front of it. Required:
71
+ * this package ships no endpoint defaults.
72
+ */
73
+ baseUrl: string;
74
+ /** Transport seam. Defaults to the global `fetch`. */
75
+ fetch?: Agent37Fetch;
76
+ /** Total attempts for a retryable failure, including the first. Default 3. */
77
+ maxAttempts?: number;
78
+ /** First backoff step; doubles per attempt. Default 250ms. */
79
+ retryBaseDelayMs?: number;
80
+ /** Sleep seam, so retry backoff is deterministic under test. */
81
+ sleep?: (ms: number) => Promise<void>;
82
+ };
83
+ export declare class Agent37Client {
84
+ private readonly apiKey;
85
+ private readonly baseUrl;
86
+ private readonly fetchImpl;
87
+ private readonly maxAttempts;
88
+ private readonly retryBaseDelayMs;
89
+ private readonly sleep;
90
+ constructor(options: Agent37ClientOptions);
91
+ /** JSON call against the hosting plane (`Authorization: Bearer`). */
92
+ hosting<T>(method: string, path: string, options?: {
93
+ body?: unknown;
94
+ query?: Record<string, string | undefined>;
95
+ timeoutMs?: number;
96
+ }): Promise<T>;
97
+ /** JSON call against one instance's own plane (`X-Agent37-Key`). */
98
+ instance<T>(instanceUrl: string, method: string, path: string, options?: {
99
+ body?: string | Uint8Array;
100
+ contentType?: string;
101
+ query?: Record<string, string | undefined>;
102
+ timeoutMs?: number;
103
+ headers?: Record<string, string>;
104
+ }): Promise<T>;
105
+ /** Byte-stream call against one instance's own plane, for file downloads. */
106
+ instanceBytes(instanceUrl: string, method: string, path: string, options?: {
107
+ query?: Record<string, string | undefined>;
108
+ timeoutMs?: number;
109
+ }): Promise<Uint8Array>;
110
+ private send;
111
+ private buildHeaders;
112
+ }
113
+ /** Exported for tests and for callers that want to mirror the retry policy. */
114
+ export declare function isRetryableAgent37Code(code: string): boolean;
115
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/agent37/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Minimal HTTP client for the Agent37 Cloud API.
3
+ *
4
+ * There is no official Agent37 JavaScript SDK to depend on: the only published
5
+ * npm package (`agent37`) is a command-line interface that exposes a `bin`, no
6
+ * library entry point, and no runtime dependencies. So this adapter speaks the
7
+ * documented HTTP contract directly over `fetch`, and takes an injectable
8
+ * `fetch` seam instead of a peer dependency. That is deliberate — an optional
9
+ * peer dependency pointing at a CLI would be a dependency on something that
10
+ * cannot be imported.
11
+ *
12
+ * Agent37 exposes two planes that share one credential but not one header:
13
+ *
14
+ * - the **hosting plane** (`{baseUrl}/v1/instances/...`), authenticated with
15
+ * `Authorization: Bearer <key>`, which creates, lists, execs, and tears down
16
+ * instances; and
17
+ * - the **instance plane** (the `url` an instance object reports),
18
+ * authenticated with `X-Agent37-Key: <key>`, which serves that one
19
+ * instance's filesystem.
20
+ *
21
+ * The instance plane's origin is never constructed here. It is read off the
22
+ * instance object, so no provider hostname is baked into this package.
23
+ */
24
+ /**
25
+ * A non-2xx response from either Agent37 plane.
26
+ *
27
+ * `code` is the machine-readable identifier the API documents callers should
28
+ * branch on — never the message text, and never the HTTP status alone, because
29
+ * one status carries several codes (409 is both `try_again` and
30
+ * `instance_limit_reached`).
31
+ */
32
+ export class Agent37ApiError extends Error {
33
+ status;
34
+ code;
35
+ /** Method and path only. Never the query string, body, or any header. */
36
+ request;
37
+ retryable;
38
+ constructor(options) {
39
+ super(`Agent37 ${options.request} failed: ${options.status} ${options.code}: ${options.message}`);
40
+ this.name = "Agent37ApiError";
41
+ this.status = options.status;
42
+ this.code = options.code;
43
+ this.request = options.request;
44
+ this.retryable = options.retryable;
45
+ }
46
+ }
47
+ /**
48
+ * Codes the API documents as safe to retry with backoff.
49
+ *
50
+ * Everything absent from this set is retried never, not "retried cautiously".
51
+ * Two exclusions are load-bearing rather than conservative:
52
+ *
53
+ * - `provisioning_failed` (502) is also what a synchronous `exec` returns when
54
+ * it passes the provider's 280-second command cap. Retrying it would run the
55
+ * caller's command a second time, on an instance where the first copy is
56
+ * still running.
57
+ * - `container_unavailable` (502) means the instance is stopped. Retrying
58
+ * cannot change that; `start` can.
59
+ */
60
+ const RETRYABLE_CODES = new Set([
61
+ // hosting plane
62
+ "no_capacity",
63
+ "try_again",
64
+ // transport plane
65
+ "container_unreachable",
66
+ "upstream_unreachable",
67
+ "host_mesh_not_ready",
68
+ "instance_saturated",
69
+ "wake_timeout",
70
+ "upstream_timeout",
71
+ // agent plane
72
+ "rate_limited",
73
+ ]);
74
+ const DEFAULT_MAX_ATTEMPTS = 3;
75
+ const DEFAULT_RETRY_BASE_DELAY_MS = 250;
76
+ export class Agent37Client {
77
+ apiKey;
78
+ baseUrl;
79
+ fetchImpl;
80
+ maxAttempts;
81
+ retryBaseDelayMs;
82
+ sleep;
83
+ constructor(options) {
84
+ const apiKey = options.apiKey?.trim();
85
+ if (!apiKey) {
86
+ throw new Error("Agent37Client requires a non-empty apiKey");
87
+ }
88
+ const baseUrl = options.baseUrl?.trim();
89
+ if (!baseUrl) {
90
+ throw new Error("Agent37Client requires an explicit baseUrl: this package ships no endpoint defaults");
91
+ }
92
+ this.apiKey = apiKey;
93
+ this.baseUrl = stripTrailingSlash(baseUrl);
94
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
95
+ if (typeof this.fetchImpl !== "function") {
96
+ throw new Error("Agent37Client requires a fetch implementation (none found on globalThis)");
97
+ }
98
+ // Non-finite maxAttempts (NaN, ±Infinity) would poison the retry loop:
99
+ // `attempt >= NaN` is always false, so the throw guard never fires and the
100
+ // loop retries a retryable failure forever. Reject explicitly rather than
101
+ // silently coercing.
102
+ const requestedAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
103
+ if (!Number.isFinite(requestedAttempts)) {
104
+ throw new Error(`Agent37Client maxAttempts must be a finite number; got ${String(options.maxAttempts)}`);
105
+ }
106
+ this.maxAttempts = Math.max(1, Math.floor(requestedAttempts));
107
+ const requestedRetryBase = options.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
108
+ if (!Number.isFinite(requestedRetryBase) || requestedRetryBase < 0) {
109
+ throw new Error(`Agent37Client retryBaseDelayMs must be a finite, non-negative number; got ${String(options.retryBaseDelayMs)}`);
110
+ }
111
+ this.retryBaseDelayMs = requestedRetryBase;
112
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
113
+ }
114
+ /** JSON call against the hosting plane (`Authorization: Bearer`). */
115
+ async hosting(method, path, options = {}) {
116
+ const response = await this.send({
117
+ method,
118
+ origin: this.baseUrl,
119
+ path,
120
+ ...(options.query ? { query: options.query } : {}),
121
+ headerStyle: "bearer",
122
+ ...(options.body === undefined
123
+ ? {}
124
+ : { body: JSON.stringify(options.body), contentType: "application/json" }),
125
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
126
+ });
127
+ return (await readJson(response));
128
+ }
129
+ /** JSON call against one instance's own plane (`X-Agent37-Key`). */
130
+ async instance(instanceUrl, method, path, options = {}) {
131
+ const response = await this.send({
132
+ method,
133
+ origin: stripTrailingSlash(instanceUrl),
134
+ path,
135
+ ...(options.query ? { query: options.query } : {}),
136
+ headerStyle: "instance-key",
137
+ ...(options.body === undefined ? {} : { body: options.body }),
138
+ ...(options.contentType === undefined ? {} : { contentType: options.contentType }),
139
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
140
+ ...(options.headers ? { headers: options.headers } : {}),
141
+ });
142
+ return (await readJson(response));
143
+ }
144
+ /** Byte-stream call against one instance's own plane, for file downloads. */
145
+ async instanceBytes(instanceUrl, method, path, options = {}) {
146
+ const response = await this.send({
147
+ method,
148
+ origin: stripTrailingSlash(instanceUrl),
149
+ path,
150
+ ...(options.query ? { query: options.query } : {}),
151
+ headerStyle: "instance-key",
152
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
153
+ });
154
+ return new Uint8Array(await response.arrayBuffer());
155
+ }
156
+ // --- internals ----------------------------------------------------------
157
+ async send(options) {
158
+ // `request` is what surfaces in errors: method and path, never the query
159
+ // string (it carries filesystem paths) and never a header (it carries the
160
+ // credential).
161
+ const request = `${options.method} ${options.path}`;
162
+ const url = buildUrl(options.origin, options.path, options.query);
163
+ let attempt = 0;
164
+ for (;;) {
165
+ attempt += 1;
166
+ // A thrown transport failure is NOT retried. The outcome of a POST that
167
+ // never returned is unknown, and re-sending it is how one submitted
168
+ // command becomes two. Only a *classified* response is retried, because
169
+ // only then is it known that the server rejected rather than accepted.
170
+ const response = await this.fetchImpl(url, {
171
+ method: options.method,
172
+ headers: this.buildHeaders(options),
173
+ ...(options.body === undefined ? {} : { body: options.body }),
174
+ ...(options.timeoutMs === undefined || options.timeoutMs <= 0
175
+ ? {}
176
+ : { signal: AbortSignal.timeout(options.timeoutMs) }),
177
+ });
178
+ if (response.ok) {
179
+ return response;
180
+ }
181
+ const error = await toApiError(response, request);
182
+ if (!error.retryable || attempt >= this.maxAttempts) {
183
+ throw error;
184
+ }
185
+ await this.sleep(this.retryBaseDelayMs * 2 ** (attempt - 1));
186
+ }
187
+ }
188
+ buildHeaders(options) {
189
+ const headers = { ...(options.headers ?? {}) };
190
+ if (options.headerStyle === "bearer") {
191
+ headers.Authorization = `Bearer ${this.apiKey}`;
192
+ }
193
+ else {
194
+ headers["X-Agent37-Key"] = this.apiKey;
195
+ }
196
+ if (options.contentType) {
197
+ headers["Content-Type"] = options.contentType;
198
+ }
199
+ return headers;
200
+ }
201
+ }
202
+ // --- helpers --------------------------------------------------------------
203
+ function stripTrailingSlash(value) {
204
+ return value.replace(/\/+$/, "");
205
+ }
206
+ function buildUrl(origin, path, query) {
207
+ const params = new URLSearchParams();
208
+ for (const [key, value] of Object.entries(query ?? {})) {
209
+ if (value !== undefined) {
210
+ params.append(key, value);
211
+ }
212
+ }
213
+ const search = params.toString();
214
+ return `${origin}${path}${search ? `?${search}` : ""}`;
215
+ }
216
+ async function readJson(response) {
217
+ const text = await response.text();
218
+ if (!text) {
219
+ return {};
220
+ }
221
+ try {
222
+ return JSON.parse(text);
223
+ }
224
+ catch {
225
+ return {};
226
+ }
227
+ }
228
+ /**
229
+ * Normalize the three documented error envelopes into one error.
230
+ *
231
+ * - hosting/agent plane: `{ "error": { "code", "message", ... } }`
232
+ * - transport plane: `{ "error": "flat_string_code" }`
233
+ * - neither (HTML from a proxy, empty body): synthesize `http_<status>`, which
234
+ * is never in the retryable set, so an unparseable failure fails fast.
235
+ */
236
+ async function toApiError(response, request) {
237
+ let code = `http_${response.status}`;
238
+ let message = "";
239
+ try {
240
+ const parsed = JSON.parse(await response.text());
241
+ const raw = parsed.error;
242
+ if (typeof raw === "string" && raw) {
243
+ code = raw;
244
+ }
245
+ else if (raw && typeof raw === "object") {
246
+ const shaped = raw;
247
+ if (typeof shaped.code === "string" && shaped.code) {
248
+ code = shaped.code;
249
+ }
250
+ if (typeof shaped.message === "string") {
251
+ message = shaped.message;
252
+ }
253
+ }
254
+ }
255
+ catch {
256
+ // Leave the synthesized code in place.
257
+ }
258
+ return new Agent37ApiError({
259
+ status: response.status,
260
+ code,
261
+ message,
262
+ request,
263
+ retryable: RETRYABLE_CODES.has(code),
264
+ });
265
+ }
266
+ /** Exported for tests and for callers that want to mirror the retry policy. */
267
+ export function isRetryableAgent37Code(code) {
268
+ return RETRYABLE_CODES.has(code);
269
+ }
270
+ //# sourceMappingURL=client.js.map