@effect-uai/core 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +1 -1
  2. package/dist/{Items-BH8xUkoR.d.mts → Items-pXR5BAgW.d.mts} +12 -12
  3. package/dist/Items-pXR5BAgW.d.mts.map +1 -0
  4. package/dist/SandboxError-BvKypkJm.d.mts +158 -0
  5. package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
  6. package/dist/{Tool-87ViKCCO.d.mts → Tool-DP-AwKOf.d.mts} +11 -11
  7. package/dist/{Tool-87ViKCCO.d.mts.map → Tool-DP-AwKOf.d.mts.map} +1 -1
  8. package/dist/domain/Items.d.mts +2 -2
  9. package/dist/domain/Items.mjs +15 -15
  10. package/dist/domain/Items.mjs.map +1 -1
  11. package/dist/domain/Turn.d.mts +388 -2
  12. package/dist/domain/Turn.d.mts.map +1 -0
  13. package/dist/domain/Turn.mjs +13 -13
  14. package/dist/domain/Turn.mjs.map +1 -1
  15. package/dist/index.d.mts +11 -6
  16. package/dist/index.mjs +8 -3
  17. package/dist/language-model/LanguageModel.d.mts +5 -5
  18. package/dist/language-model/LanguageModel.d.mts.map +1 -1
  19. package/dist/language-model/LanguageModel.mjs.map +1 -1
  20. package/dist/loop/Loop.d.mts +37 -90
  21. package/dist/loop/Loop.d.mts.map +1 -1
  22. package/dist/loop/Loop.mjs +39 -93
  23. package/dist/loop/Loop.mjs.map +1 -1
  24. package/dist/loop/Loop.test.mjs +46 -54
  25. package/dist/loop/Loop.test.mjs.map +1 -1
  26. package/dist/sandbox/Memory.d.mts +53 -0
  27. package/dist/sandbox/Memory.d.mts.map +1 -0
  28. package/dist/sandbox/Memory.mjs +68 -0
  29. package/dist/sandbox/Memory.mjs.map +1 -0
  30. package/dist/sandbox/Sandbox.d.mts +717 -0
  31. package/dist/sandbox/Sandbox.d.mts.map +1 -0
  32. package/dist/sandbox/Sandbox.mjs +192 -0
  33. package/dist/sandbox/Sandbox.mjs.map +1 -0
  34. package/dist/sandbox/SandboxError.d.mts +2 -0
  35. package/dist/sandbox/SandboxError.mjs +89 -0
  36. package/dist/sandbox/SandboxError.mjs.map +1 -0
  37. package/dist/sandbox/SandboxImage.d.mts +28 -0
  38. package/dist/sandbox/SandboxImage.d.mts.map +1 -0
  39. package/dist/sandbox/SandboxImage.mjs +32 -0
  40. package/dist/sandbox/SandboxImage.mjs.map +1 -0
  41. package/dist/sandbox/SandboxNetwork.d.mts +34 -0
  42. package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
  43. package/dist/sandbox/SandboxNetwork.mjs +36 -0
  44. package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
  45. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +98 -2
  46. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
  47. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
  48. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  49. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +56 -2
  50. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
  51. package/dist/testing/MockProvider.d.mts +3 -3
  52. package/dist/testing/MockProvider.d.mts.map +1 -1
  53. package/dist/testing/MockProvider.mjs.map +1 -1
  54. package/dist/testing/MockSandbox.d.mts +604 -0
  55. package/dist/testing/MockSandbox.d.mts.map +1 -0
  56. package/dist/testing/MockSandbox.mjs +160 -0
  57. package/dist/testing/MockSandbox.mjs.map +1 -0
  58. package/dist/testing/MockSpeechSynthesizer.d.mts +26 -11
  59. package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
  60. package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
  61. package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
  62. package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
  63. package/dist/tool/Approval.d.mts.map +1 -0
  64. package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
  65. package/dist/tool/Approval.mjs.map +1 -0
  66. package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
  67. package/dist/tool/Approval.test.mjs.map +1 -0
  68. package/dist/tool/HistoryCheck.d.mts +6 -6
  69. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  70. package/dist/tool/HistoryCheck.mjs +5 -5
  71. package/dist/tool/HistoryCheck.mjs.map +1 -1
  72. package/dist/tool/Tool.d.mts +2 -2
  73. package/dist/tool/Tool.mjs +4 -4
  74. package/dist/tool/Tool.mjs.map +1 -1
  75. package/dist/tool/ToolEvent.d.mts +19 -19
  76. package/dist/tool/ToolEvent.d.mts.map +1 -1
  77. package/dist/tool/ToolEvent.mjs +8 -8
  78. package/dist/tool/ToolEvent.mjs.map +1 -1
  79. package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
  80. package/dist/tool/ToolResult.d.mts.map +1 -0
  81. package/dist/tool/ToolResult.mjs +61 -0
  82. package/dist/tool/ToolResult.mjs.map +1 -0
  83. package/dist/tool/Toolkit.d.mts +58 -26
  84. package/dist/tool/Toolkit.d.mts.map +1 -1
  85. package/dist/tool/Toolkit.mjs +62 -34
  86. package/dist/tool/Toolkit.mjs.map +1 -1
  87. package/dist/tool/Toolkit.test.mjs +13 -13
  88. package/dist/tool/Toolkit.test.mjs.map +1 -1
  89. package/package.json +31 -7
  90. package/src/domain/Items.ts +9 -9
  91. package/src/domain/Turn.ts +16 -16
  92. package/src/index.ts +7 -2
  93. package/src/language-model/LanguageModel.ts +3 -3
  94. package/src/loop/Loop.test.ts +93 -91
  95. package/src/loop/Loop.ts +72 -170
  96. package/src/sandbox/Memory.ts +128 -0
  97. package/src/sandbox/Sandbox.ts +639 -0
  98. package/src/sandbox/SandboxError.ts +140 -0
  99. package/src/sandbox/SandboxImage.ts +26 -0
  100. package/src/sandbox/SandboxNetwork.ts +34 -0
  101. package/src/speech-synthesizer/SpeechSynthesizer.test.ts +89 -0
  102. package/src/speech-synthesizer/SpeechSynthesizer.ts +124 -0
  103. package/src/testing/MockProvider.ts +21 -20
  104. package/src/testing/MockSandbox.ts +342 -0
  105. package/src/testing/MockSpeechSynthesizer.ts +137 -77
  106. package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
  107. package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
  108. package/src/tool/HistoryCheck.ts +10 -13
  109. package/src/tool/Tool.ts +13 -15
  110. package/src/tool/ToolEvent.ts +7 -7
  111. package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
  112. package/src/tool/Toolkit.test.ts +13 -13
  113. package/src/tool/Toolkit.ts +97 -73
  114. package/dist/Items-BH8xUkoR.d.mts.map +0 -1
  115. package/dist/Turn-0CwCAyVe.d.mts +0 -388
  116. package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
  117. package/dist/tool/Outcome.d.mts.map +0 -1
  118. package/dist/tool/Outcome.mjs +0 -61
  119. package/dist/tool/Outcome.mjs.map +0 -1
  120. package/dist/tool/Resolvers.d.mts.map +0 -1
  121. package/dist/tool/Resolvers.mjs.map +0 -1
  122. package/dist/tool/Resolvers.test.mjs.map +0 -1
  123. /package/dist/tool/{Resolvers.test.d.mts → Approval.test.d.mts} +0 -0
@@ -0,0 +1,639 @@
1
+ import { Brand, Context, Data, type Duration, Effect, Redacted, Scope, Stream } from "effect"
2
+ import type * as SandboxError from "./SandboxError.js"
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Identity
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * Sandbox identifier. Branded `string` so providers don't accidentally
10
+ * accept a raw user-supplied id. Each provider mints these per its own
11
+ * naming scheme (uuid for E2B/Vercel, user-supplied name for
12
+ * Microsandbox/Modal, …); the core code treats them opaquely.
13
+ *
14
+ * Mint via the value-level `SandboxId(...)` smart constructor instead
15
+ * of `as SandboxId` casts.
16
+ */
17
+ export type SandboxId = Brand.Branded<string, "SandboxId">
18
+ export const SandboxId = Brand.nominal<SandboxId>()
19
+
20
+ /**
21
+ * Reference returned by `list`. Carries just enough to call `attach` —
22
+ * provider-specific metadata (createdAt, region, tags, …) lives on the
23
+ * narrowed per-provider ref.
24
+ */
25
+ export type SandboxRef = {
26
+ readonly id: SandboxId
27
+ readonly name?: string
28
+ }
29
+
30
+ export type VolumeId = Brand.Branded<string, "VolumeId">
31
+ export const VolumeId = Brand.nominal<VolumeId>()
32
+
33
+ export type SnapshotId = Brand.Branded<string, "SnapshotId">
34
+ export const SnapshotId = Brand.nominal<SnapshotId>()
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Image / runtime selection
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * What runtime / filesystem to boot the sandbox from.
42
+ *
43
+ * - `Default` — provider picks its house image (Vercel: Amazon Linux
44
+ * 2023; Microsandbox: requires user image but a builder default may
45
+ * apply, …). Use when you don't care what's pre-installed.
46
+ * - `Registry` — an OCI image reference. Supported by every provider
47
+ * except those with a fixed base (Vercel ignores this and the
48
+ * request-schema rejects at decode).
49
+ * - `Snapshot` — restore from a captured state. Gated by
50
+ * {@link SandboxSnapshots} at the call site — the marker is provided
51
+ * by adapters whose provider supports snapshot-as-image.
52
+ * - `Dockerfile` — provider builds a custom image from a Dockerfile.
53
+ * Gated by {@link SandboxCustomImage}.
54
+ */
55
+ export type ImageRef = Data.TaggedEnum<{
56
+ Default: {}
57
+ Registry: { readonly ref: string }
58
+ Snapshot: { readonly id: SnapshotId }
59
+ Dockerfile: { readonly contents: string }
60
+ }>
61
+
62
+ export const ImageRef = Data.taggedEnum<ImageRef>()
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Network policy + secrets
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Outbound network policy for a sandbox.
70
+ *
71
+ * - `Open` — no egress restriction. The provider's defaults apply
72
+ * (e.g. Microsandbox's "public-only" still blocks private ranges).
73
+ * - `Blocked` — fully airgapped, no outbound connectivity at all.
74
+ * - `Allowlist` — only the listed hosts / CIDRs may be reached.
75
+ *
76
+ * Providers vary in what they honour: hostname-allowlist requires
77
+ * {@link SandboxHostnameAllowlist}; CIDR-only providers ignore `hosts`
78
+ * and the adapter rejects at decode if `hosts` is non-empty.
79
+ */
80
+ export type NetworkPolicy = Data.TaggedEnum<{
81
+ Open: {}
82
+ Blocked: {}
83
+ Allowlist: {
84
+ readonly hosts?: ReadonlyArray<string>
85
+ readonly cidrs?: ReadonlyArray<string>
86
+ }
87
+ }>
88
+
89
+ export const NetworkPolicy = Data.taggedEnum<NetworkPolicy>()
90
+
91
+ /**
92
+ * A secret bound to a sandbox at create time. The code inside the
93
+ * sandbox sees only a placeholder (`$MSB_<NAME>` for Microsandbox,
94
+ * `{name}` for Vercel header-injection, etc.); the real `value` lives
95
+ * on the provider's proxy and is substituted on outbound HTTPS to
96
+ * `hosts`.
97
+ *
98
+ * Requires the {@link SandboxSecretInjection} capability marker —
99
+ * provider Layers without proxy-layer secret rewriting do not ship the
100
+ * marker and consumer code fails at `Effect.provide` with a type
101
+ * error.
102
+ *
103
+ * `header` is the request header to inject (default
104
+ * `"Authorization: Bearer <value>"`). Providers that do not let
105
+ * callers pick the header (Microsandbox's `secretEnv` shorthand)
106
+ * silently ignore this field; providers that require an explicit
107
+ * header (Vercel's `transform.headers`) fail at decode if it's absent.
108
+ */
109
+ export type BoundSecret = {
110
+ readonly name: string
111
+ readonly value: Redacted.Redacted<string>
112
+ readonly hosts: ReadonlyArray<string>
113
+ readonly header?: string
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Volumes (capability-gated on `SandboxVolumes`)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ export type VolumeMount = {
121
+ readonly id: VolumeId
122
+ readonly mountPath: string
123
+ readonly readonly?: boolean
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Create / exec request shapes
128
+ // ---------------------------------------------------------------------------
129
+
130
+ /**
131
+ * Cross-provider create request. Provider-specific knobs (region,
132
+ * cpus, memory, template id, …) live on each provider's narrowed
133
+ * request type which extends this.
134
+ *
135
+ * Fields a provider can't honour fail at **decode time** (per the
136
+ * per-provider request schema), not silently at runtime. The
137
+ * ComputeSDK anti-pattern of "drop unsupported fields" is the
138
+ * footgun we're avoiding.
139
+ */
140
+ export type CommonCreateRequest = {
141
+ /** Image / snapshot to boot from. Omit for the provider's default. */
142
+ readonly image?: ImageRef
143
+ /**
144
+ * Idle / hard timeout. Accepts a number (millis), a string like
145
+ * `"30 seconds"`, or a `Duration`. Providers default differently.
146
+ */
147
+ readonly timeout?: Duration.Input
148
+ /**
149
+ * Env vars exposed to the sandbox. NOT for secrets you want hidden
150
+ * from the running code — use {@link BoundSecret} instead.
151
+ */
152
+ readonly env?: Readonly<Record<string, string>>
153
+ /** Outbound network policy. */
154
+ readonly network?: NetworkPolicy
155
+ /**
156
+ * Bound secrets — code inside the sandbox sees a placeholder; the
157
+ * proxy substitutes the real value on outbound HTTPS to listed hosts.
158
+ * Capability-gated on {@link SandboxSecretInjection}.
159
+ */
160
+ readonly secrets?: ReadonlyArray<BoundSecret>
161
+ /**
162
+ * Persistent volumes to mount. Volumes are created via
163
+ * `Sandbox.volumes.create` and survive sandbox lifetime.
164
+ * Capability-gated on {@link SandboxVolumes}.
165
+ */
166
+ readonly volumes?: ReadonlyArray<VolumeMount>
167
+ }
168
+
169
+ /**
170
+ * Cross-provider exec request. Background / long-running processes
171
+ * use `spawn` instead of `exec`; the semantics differ enough that
172
+ * overloading `exec` with a `detached` flag was rejected during
173
+ * design.
174
+ */
175
+ export type CommonExecRequest = {
176
+ /**
177
+ * Argv-style command. Pass a string for "run this through the
178
+ * shell" semantics (provider-dependent — most use `sh -c`); pass
179
+ * an array for direct argv with no shell parsing.
180
+ */
181
+ readonly cmd: string | ReadonlyArray<string>
182
+ readonly cwd?: string
183
+ readonly env?: Readonly<Record<string, string>>
184
+ readonly stdin?: string | Uint8Array
185
+ /**
186
+ * Per-exec timeout. Accepts a number (millis), a string like
187
+ * `"5 seconds"`, or a `Duration`.
188
+ */
189
+ readonly timeout?: Duration.Input
190
+ }
191
+
192
+ /**
193
+ * Spawn request — extends {@link CommonExecRequest} with the option
194
+ * to provide stdin as a `Stream<Uint8Array>`. The provider takes one
195
+ * chunk at a time and sends EOF when the stream completes. Useful
196
+ * for piping LLM output into a sandboxed process, or feeding stdin
197
+ * from another stream source. `exec` does not accept streams.
198
+ */
199
+ export type CommonSpawnRequest = Omit<CommonExecRequest, "stdin"> & {
200
+ readonly stdin?: string | Uint8Array | Stream.Stream<Uint8Array, never, never>
201
+ }
202
+
203
+ export type ExecResult = {
204
+ readonly exitCode: number
205
+ readonly stdout: string
206
+ readonly stderr: string
207
+ readonly durationMs: number
208
+ }
209
+
210
+ /**
211
+ * Streaming exec output. `Complete` is terminal — the stream
212
+ * completes after emitting it.
213
+ */
214
+ export type ExecEvent = Data.TaggedEnum<{
215
+ Stdout: { readonly chunk: Uint8Array }
216
+ Stderr: { readonly chunk: Uint8Array }
217
+ Complete: { readonly exitCode: number; readonly durationMs: number }
218
+ }>
219
+
220
+ export const ExecEvent = Data.taggedEnum<ExecEvent>()
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Filesystem
224
+ // ---------------------------------------------------------------------------
225
+
226
+ export type FileEntryKind = "file" | "directory" | "symlink" | "other"
227
+
228
+ export type FileEntry = {
229
+ readonly path: string
230
+ readonly kind: FileEntryKind
231
+ }
232
+
233
+ export type SandboxFilesystem = {
234
+ readonly read: (path: string) => Effect.Effect<Uint8Array, SandboxError.SandboxError>
235
+ readonly write: (
236
+ path: string,
237
+ contents: Uint8Array | string,
238
+ ) => Effect.Effect<void, SandboxError.SandboxError>
239
+ readonly remove: (path: string) => Effect.Effect<void, SandboxError.SandboxError>
240
+ readonly mkdir: (path: string) => Effect.Effect<void, SandboxError.SandboxError>
241
+ readonly list: (
242
+ path: string,
243
+ ) => Effect.Effect<ReadonlyArray<FileEntry>, SandboxError.SandboxError>
244
+ readonly exists: (path: string) => Effect.Effect<boolean, SandboxError.SandboxError>
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Process handle (returned by `spawn`)
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /**
252
+ * Long-running process handle. Returned by `spawn` in a `Scope.Scope`
253
+ * — the process is killed on scope close. To outlive the calling
254
+ * scope, do `Effect.forkScoped` against a wider scope or use
255
+ * `attach`-style reconnection per the provider's escape hatch.
256
+ */
257
+ export type ProcessHandle = {
258
+ readonly pid: number
259
+ readonly events: Stream.Stream<ExecEvent, SandboxError.SandboxError>
260
+ readonly kill: Effect.Effect<void, SandboxError.SandboxError>
261
+ readonly exit: Effect.Effect<{ readonly exitCode: number }, SandboxError.SandboxError>
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // SandboxInstance — the live handle returned from `create` / `attach`
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /**
269
+ * Live sandbox handle. Methods are plain `Effect` / `Stream` values
270
+ * — no nullary thunks (`destroy()`, `pause()`) — to match Effect's
271
+ * `Queue.shutdown` / `Fiber.interrupt` style. Destruction is the
272
+ * scope finalizer, not a method here; for the rare "kill from
273
+ * another fiber" case use the service-level escape hatch
274
+ * `Sandbox.destroy(id)`.
275
+ */
276
+ export type SandboxInstance = {
277
+ readonly id: SandboxId
278
+ readonly exec: (
279
+ request: CommonExecRequest,
280
+ ) => Effect.Effect<ExecResult, SandboxError.SandboxError>
281
+ readonly execStream: (
282
+ request: CommonExecRequest,
283
+ ) => Stream.Stream<ExecEvent, SandboxError.SandboxError>
284
+ /**
285
+ * Spawn a background / long-running process. The returned handle is
286
+ * scoped — the process is killed on scope close. Use this for
287
+ * "dev server / watcher / tail -f" workloads. For one-shot
288
+ * commands prefer `exec`.
289
+ */
290
+ readonly spawn: (
291
+ request: CommonSpawnRequest,
292
+ ) => Effect.Effect<ProcessHandle, SandboxError.SandboxError, Scope.Scope>
293
+ readonly files: SandboxFilesystem
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Snapshot / volume / port sub-services (capability-gated)
298
+ // ---------------------------------------------------------------------------
299
+
300
+ export type SandboxSnapshotsApi = {
301
+ readonly create: (
302
+ from: SandboxInstance,
303
+ name?: string,
304
+ ) => Effect.Effect<SnapshotId, SandboxError.SandboxError>
305
+ readonly destroy: (id: SnapshotId) => Effect.Effect<void, SandboxError.SandboxError>
306
+ readonly list: Effect.Effect<
307
+ ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,
308
+ SandboxError.SandboxError
309
+ >
310
+ }
311
+
312
+ export type SandboxVolumesApi = {
313
+ readonly create: (
314
+ name: string,
315
+ options?: { readonly quotaBytes?: number },
316
+ ) => Effect.Effect<VolumeId, SandboxError.SandboxError>
317
+ readonly destroy: (id: VolumeId) => Effect.Effect<void, SandboxError.SandboxError>
318
+ readonly list: Effect.Effect<
319
+ ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,
320
+ SandboxError.SandboxError
321
+ >
322
+ }
323
+
324
+ export type SandboxPortsApi = {
325
+ /**
326
+ * Expose an internal sandbox port as a publicly reachable URL.
327
+ * Gated by {@link SandboxPortExposure} on the free helper; adapters
328
+ * that don't ship the marker leave this sub-API out of the public
329
+ * surface entirely (callers can't construct an Effect against it).
330
+ */
331
+ readonly expose: (
332
+ instance: SandboxInstance,
333
+ port: number,
334
+ ) => Effect.Effect<{ readonly url: string }, SandboxError.SandboxError>
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Service definition
339
+ // ---------------------------------------------------------------------------
340
+
341
+ /**
342
+ * Cross-provider sandbox service.
343
+ *
344
+ * Lifetime model: `create` returns a sandbox handle in `Scope.Scope`.
345
+ * The scope finalizer destroys the sandbox. Use `Effect.scoped` to
346
+ * bound a sandbox's lifetime to a single Effect, or manage the scope
347
+ * explicitly via `Scope.make` for sandboxes that span many calls.
348
+ * This is why there's no `destroy()` method on `SandboxInstance` —
349
+ * the scope is the lifecycle handle.
350
+ *
351
+ * `snapshots` and `volumes` sub-APIs are present on the service but
352
+ * are still capability-gated: provider Layers that don't support
353
+ * them ship implementations that always fail with
354
+ * `SandboxUnsupported`. The free helpers ({@link snapshot},
355
+ * {@link createVolume}) additionally require the corresponding
356
+ * capability marker on R, so misuses surface as type errors at
357
+ * `Effect.provide`, not as runtime failures.
358
+ */
359
+ export type SandboxService = {
360
+ /**
361
+ * Provision a new sandbox. The returned handle's lifetime is
362
+ * bound to the caller's scope; the sandbox is destroyed on scope
363
+ * close. To survive the scope, use `attach`-style reconnection or
364
+ * promote into a wider scope.
365
+ */
366
+ readonly create: (
367
+ request: CommonCreateRequest,
368
+ ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>
369
+
370
+ /**
371
+ * Re-acquire an existing sandbox by id. The returned handle is
372
+ * scope-bound but the finalizer is "detach" (no destroy) —
373
+ * appropriate for paused / detached / persistent sandboxes that
374
+ * outlive any single agent run.
375
+ */
376
+ readonly attach: (
377
+ id: SandboxId,
378
+ ) => Effect.Effect<SandboxInstance, SandboxError.SandboxError, Scope.Scope>
379
+
380
+ /**
381
+ * Enumerate sandboxes visible to the configured account / project.
382
+ */
383
+ readonly list: Effect.Effect<ReadonlyArray<SandboxRef>, SandboxError.SandboxError>
384
+
385
+ /**
386
+ * Escape hatch for destroying a sandbox from outside its owning
387
+ * scope. Most callers should let the scope finalizer handle this.
388
+ */
389
+ readonly destroy: (id: SandboxId) => Effect.Effect<void, SandboxError.SandboxError>
390
+
391
+ /** Snapshot management. Capability-gated on {@link SandboxSnapshots}. */
392
+ readonly snapshots: SandboxSnapshotsApi
393
+
394
+ /** Volume management. Capability-gated on {@link SandboxVolumes}. */
395
+ readonly volumes: SandboxVolumesApi
396
+
397
+ /** Port exposure. Capability-gated on {@link SandboxPortExposure}. */
398
+ readonly ports: SandboxPortsApi
399
+ }
400
+
401
+ export class Sandbox extends Context.Service<Sandbox, SandboxService>()(
402
+ "@betalyra/effect-uai/Sandbox",
403
+ ) {}
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // Capability markers
407
+ // ---------------------------------------------------------------------------
408
+
409
+ /**
410
+ * Capability marker — provided by adapter Layers whose provider
411
+ * supports a hostname-level egress allowlist (matching against host
412
+ * names / wildcards, not just CIDRs). Vercel, Cloudflare, Deno,
413
+ * Microsandbox, Runloop, Anthropic srt ship this marker; Modal and
414
+ * Daytona (CIDR-only) do not.
415
+ *
416
+ * Phantom — the value is `void`; adapters register with
417
+ * `Layer.succeed(SandboxHostnameAllowlist, undefined)`.
418
+ */
419
+ export class SandboxHostnameAllowlist extends Context.Service<SandboxHostnameAllowlist, void>()(
420
+ "@betalyra/effect-uai/capability/SandboxHostnameAllowlist",
421
+ ) {}
422
+
423
+ /**
424
+ * Capability marker — adapter's provider supports proxy-layer
425
+ * secret rewriting (header injection on outbound HTTPS to bound
426
+ * hosts; the running code never sees the real value). Deno,
427
+ * Cloudflare, E2B, Vercel (firewall transform), Microsandbox,
428
+ * Runloop (Agent Gateway) ship this. Modal, Daytona, CodeSandbox
429
+ * do not.
430
+ */
431
+ export class SandboxSecretInjection extends Context.Service<SandboxSecretInjection, void>()(
432
+ "@betalyra/effect-uai/capability/SandboxSecretInjection",
433
+ ) {}
434
+
435
+ /**
436
+ * Capability marker — adapter supports snapshot-as-new-sandbox
437
+ * (capture state, restore as a derived sandbox with a fresh id).
438
+ * Distinct from {@link SandboxPauseResume}, which preserves the
439
+ * same id and resumes in place.
440
+ */
441
+ export class SandboxSnapshots extends Context.Service<SandboxSnapshots, void>()(
442
+ "@betalyra/effect-uai/capability/SandboxSnapshots",
443
+ ) {}
444
+
445
+ /**
446
+ * Capability marker — adapter supports in-place pause and resume on
447
+ * the same sandbox id (E2B, CodeSandbox). Memory + processes are
448
+ * preserved across the pause boundary. Most providers without this
449
+ * marker offer {@link SandboxSnapshots} instead.
450
+ */
451
+ export class SandboxPauseResume extends Context.Service<SandboxPauseResume, void>()(
452
+ "@betalyra/effect-uai/capability/SandboxPauseResume",
453
+ ) {}
454
+
455
+ /**
456
+ * Capability marker — adapter manages named persistent volumes
457
+ * outside any single sandbox lifecycle (Modal Volume, Daytona,
458
+ * Microsandbox, BoxLite). Providers without this marker leave
459
+ * `Sandbox.volumes.*` returning `SandboxUnsupported`; the free
460
+ * helpers ({@link createVolume}, {@link destroyVolume}) additionally
461
+ * require this marker on R so misuses become a type error.
462
+ */
463
+ export class SandboxVolumes extends Context.Service<SandboxVolumes, void>()(
464
+ "@betalyra/effect-uai/capability/SandboxVolumes",
465
+ ) {}
466
+
467
+ /**
468
+ * Capability marker — adapter supports a stateful kernel session
469
+ * (Jupyter-style: persistent Python / JS context that returns rich
470
+ * outputs across many `runCode` calls). Cloudflare Code Interpreter
471
+ * and E2B ship this; everyone else expects you to write your own
472
+ * script and `exec` it.
473
+ *
474
+ * The actual `runCode` helper lives separately (not on
475
+ * `SandboxInstance`) because the return shape is different — rich
476
+ * outputs with images/tables, not buffered stdout strings.
477
+ */
478
+ export class SandboxKernelSession extends Context.Service<SandboxKernelSession, void>()(
479
+ "@betalyra/effect-uai/capability/SandboxKernelSession",
480
+ ) {}
481
+
482
+ /**
483
+ * Capability marker — adapter exposes an interactive PTY session
484
+ * (Runloop, Cloudflare terminal). Distinct from `execStream` which
485
+ * is byte-oriented and one-shot.
486
+ */
487
+ export class SandboxPty extends Context.Service<SandboxPty, void>()(
488
+ "@betalyra/effect-uai/capability/SandboxPty",
489
+ ) {}
490
+
491
+ /**
492
+ * Capability marker — adapter accepts a user-supplied Dockerfile (or
493
+ * OCI image build) instead of just a registry reference. Vercel does
494
+ * not ship this (fixed AL2023). Deno does not (snapshots only).
495
+ * Most others do.
496
+ */
497
+ export class SandboxCustomImage extends Context.Service<SandboxCustomImage, void>()(
498
+ "@betalyra/effect-uai/capability/SandboxCustomImage",
499
+ ) {}
500
+
501
+ /**
502
+ * Capability marker — adapter can expose an internal port as a
503
+ * publicly reachable URL. Most cloud providers ship this; local
504
+ * providers (Microsandbox, BoxLite) typically only support
505
+ * host-side port forwarding (handled by ignoring this marker and
506
+ * documenting via the per-provider request type).
507
+ */
508
+ export class SandboxPortExposure extends Context.Service<SandboxPortExposure, void>()(
509
+ "@betalyra/effect-uai/capability/SandboxPortExposure",
510
+ ) {}
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Free helpers — accept Sandbox via Context, optionally require markers.
514
+ // ---------------------------------------------------------------------------
515
+
516
+ /**
517
+ * Provision a sandbox bound to the caller's scope. Drop the
518
+ * `Scope.Scope` requirement with `Effect.scoped`.
519
+ */
520
+ export const create = (
521
+ request: CommonCreateRequest,
522
+ ): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>
523
+ Effect.flatMap(Sandbox.asEffect(), (s) => s.create(request))
524
+
525
+ /**
526
+ * Re-acquire an existing sandbox by id. Same scope semantics as
527
+ * `create`, but the finalizer detaches rather than destroying.
528
+ */
529
+ export const attach = (
530
+ id: SandboxId,
531
+ ): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>
532
+ Effect.flatMap(Sandbox.asEffect(), (s) => s.attach(id))
533
+
534
+ /** Enumerate sandboxes for the configured account / project. */
535
+ export const list: Effect.Effect<
536
+ ReadonlyArray<SandboxRef>,
537
+ SandboxError.SandboxError,
538
+ Sandbox
539
+ > = Effect.flatMap(Sandbox.asEffect(), (s) => s.list)
540
+
541
+ /**
542
+ * Escape hatch for destroying a sandbox from outside its owning
543
+ * scope. Most callers should let the scope finalizer handle this
544
+ * automatically.
545
+ */
546
+ export const destroy = (id: SandboxId): Effect.Effect<void, SandboxError.SandboxError, Sandbox> =>
547
+ Effect.flatMap(Sandbox.asEffect(), (s) => s.destroy(id))
548
+
549
+ /**
550
+ * Create a snapshot of a live sandbox. Requires
551
+ * {@link SandboxSnapshots} on the R channel — adapters whose
552
+ * provider doesn't support snapshot-as-new-sandbox don't ship the
553
+ * marker and calls fail at `Effect.provide` with a type error.
554
+ */
555
+ export const snapshot = (
556
+ from: SandboxInstance,
557
+ name?: string,
558
+ ): Effect.Effect<SnapshotId, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>
559
+ Effect.gen(function* () {
560
+ const s = yield* Sandbox.asEffect()
561
+ yield* SandboxSnapshots.asEffect()
562
+ return yield* s.snapshots.create(from, name)
563
+ })
564
+
565
+ /**
566
+ * Create a named persistent volume. Requires {@link SandboxVolumes}.
567
+ */
568
+ export const createVolume = (
569
+ name: string,
570
+ options?: { readonly quotaBytes?: number },
571
+ ): Effect.Effect<VolumeId, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>
572
+ Effect.gen(function* () {
573
+ const s = yield* Sandbox.asEffect()
574
+ yield* SandboxVolumes.asEffect()
575
+ return yield* s.volumes.create(name, options)
576
+ })
577
+
578
+ /** Destroy a named volume. Requires {@link SandboxVolumes}. */
579
+ export const destroyVolume = (
580
+ id: VolumeId,
581
+ ): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>
582
+ Effect.gen(function* () {
583
+ const s = yield* Sandbox.asEffect()
584
+ yield* SandboxVolumes.asEffect()
585
+ return yield* s.volumes.destroy(id)
586
+ })
587
+
588
+ /** Enumerate persistent volumes. Requires {@link SandboxVolumes}. */
589
+ export const listVolumes: Effect.Effect<
590
+ ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>,
591
+ SandboxError.SandboxError,
592
+ Sandbox | SandboxVolumes
593
+ > = Effect.gen(function* () {
594
+ const s = yield* Sandbox.asEffect()
595
+ yield* SandboxVolumes.asEffect()
596
+ return yield* s.volumes.list
597
+ })
598
+
599
+ /** Destroy a snapshot. Requires {@link SandboxSnapshots}. */
600
+ export const destroySnapshot = (
601
+ id: SnapshotId,
602
+ ): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>
603
+ Effect.gen(function* () {
604
+ const s = yield* Sandbox.asEffect()
605
+ yield* SandboxSnapshots.asEffect()
606
+ return yield* s.snapshots.destroy(id)
607
+ })
608
+
609
+ /** Enumerate snapshots. Requires {@link SandboxSnapshots}. */
610
+ export const listSnapshots: Effect.Effect<
611
+ ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>,
612
+ SandboxError.SandboxError,
613
+ Sandbox | SandboxSnapshots
614
+ > = Effect.gen(function* () {
615
+ const s = yield* Sandbox.asEffect()
616
+ yield* SandboxSnapshots.asEffect()
617
+ return yield* s.snapshots.list
618
+ })
619
+
620
+ /**
621
+ * Expose an internal port of a live sandbox as a publicly reachable
622
+ * URL. Requires {@link SandboxPortExposure} — adapters whose provider
623
+ * doesn't support runtime port exposure (e.g. Microsandbox, which
624
+ * forwards at create time only) don't ship the marker, and calls
625
+ * fail at `Effect.provide` with a type error.
626
+ */
627
+ export const exposePort = (
628
+ instance: SandboxInstance,
629
+ port: number,
630
+ ): Effect.Effect<
631
+ { readonly url: string },
632
+ SandboxError.SandboxError,
633
+ Sandbox | SandboxPortExposure
634
+ > =>
635
+ Effect.gen(function* () {
636
+ const s = yield* Sandbox.asEffect()
637
+ yield* SandboxPortExposure.asEffect()
638
+ return yield* s.ports.expose(instance, port)
639
+ })