Skip to content

Extensions

Sandbox execution

Run a trusted command while streaming its output, give generated JavaScript a bounded host API, or capture a rendered page. The sandbox package defines a separate contract for each job:

Need Use Result
Run a known command with arguments, working directory, and bounded output Sandbox A stream of process events
Run generated JavaScript that can call an allowlisted host API CodeExecutor through Code Mode One schema-bounded result
Render or extract one web page PageCapture One bounded capture result
Capture a rendered page as PNG PageScreenshot Bounded image bytes
Crawl one HTTPS host PageCrawl A scoped stream of Markdown records
Navigate, read, click, or fill a live page InteractiveBrowser A scoped browser handle

Sandbox is for command-shaped work. It keeps the executable, arguments, current directory, environment names, network request, and resource limits in a Schema-defined request. The host can call it directly or wrap it in an application Tool. Process execution has no durable recovery; a process that may have changed an external system has an uncertain outcome after a crash.

CodeExecutor is a different port for one generated JavaScript async function. The only host authority it receives is the scoped CodeExecutionHost callback. Code Mode adds the agent-facing Tool, validation, broker, and accounting. Do not use a command process when the work needs that narrow callback boundary.

Browser services also have their own contracts because rendering and navigation are egress operations. Use browser services for Cloudflare adapter setup, network policy, and browser session lifetime.

In your application, install the local adapter:

bun add @yielded/agent-sandbox-local@beta effect

Keep framework packages at the same release.

@yielded/agent-sandbox-local runs a child process on the current machine. It is useful for local development and trusted automation. Every event identifies it as unisolated.

The local adapter accepts only the unisolated-process runtime with the local-process identity. The request must name every environment variable the child may receive. It starts with an empty environment, then copies only those names. LocalSandbox.layer supplies the Node process services.

import {
import NodeRuntime
NodeRuntime
} from "@effect/platform-node";
import {
class NetworkDisabled

Explicitly denies all workload network access.

NetworkDisabled
,
class Sandbox

Scoped command/code execution capability. Implementations must identify their isolation posture in every emitted event; an unisolated implementation is never a security sandbox.

Sandbox
,
class SandboxRequest

Schema-first command execution request. Command and arguments are intentionally separate.

SandboxRequest
} from "@yielded/agent/sandbox";
import {
import LocalSandbox
LocalSandbox
} from "@yielded/agent-sandbox-local";
import {
import Console
Console
,
import Duration
Duration
,
import Effect
Effect
,
import Stream
Stream
} from "effect";
const
const request: SandboxRequest
request
=
class SandboxRequest

Schema-first command execution request. Command and arguments are intentionally separate.

SandboxRequest
.
BottomWithoutNew<unknown, unknown, unknown, unknown, Declaration, decodeTo<declareConstructor<SandboxRequest, { readonly runtime: { readonly kind: "container" | "microvm" | "wasm" | "unisolated-process"; readonly identity: string; }; ... 8 more ...; readonly artifactRules: readonly { ...; }[]; }, readonly [...], { ...; }>, Struct<...>, never, never>, ... 8 more ..., "required">.make(input: {
readonly runtime: SandboxRuntime;
readonly command: string;
readonly args: readonly string[];
readonly cwd: string;
readonly environment: SandboxEnvironment;
readonly mounts: readonly SandboxMount[];
readonly network: NetworkDisabled | NetworkAllowlist;
readonly limits: SandboxLimits;
readonly secretHandles: readonly SandboxSecretHandle[];
readonly artifactRules: readonly SandboxArtifactRule[];
}, options?: MakeOptions): SandboxRequest

Constructs a value from the make input representation synchronously.

When to use

Use when constructor input is trusted or when validation failure should abort with a thrown Error.

Details

Applies constructor defaults and type-side validation according to MakeOptions.

Gotchas

Throws an Error with the schema issue in its cause when validation fails. Schema validation failures use the generic message "Schema validation failed"; format the cause explicitly with SchemaIssue.makeFormatterDefault() when human-readable details are needed. Causes that contain defects, interruptions, or other non-schema reasons throw with the underlying Cause attached instead.

@see ― BottomWithoutNew.makeOption — construct synchronously and discard validation details

@see ― BottomWithoutNew.makeEffect — construct through Effect when validation failure should stay in the error channel

make
({
runtime: SandboxRuntime
runtime
: {
kind: "container" | "microvm" | "wasm" | "unisolated-process"
kind
: "unisolated-process",
identity: string
identity
: "local-process" },
command: string
command
:
var process: NodeJS.Process
process
.
NodeJS.Process.execPath: string

The process.execPath property returns the absolute pathname of the executable that started the Node.js process. Symbolic links, if any, are resolved.

'/usr/local/bin/node'

@since ― v0.1.100

execPath
,
args: readonly string[]
args
: ["-e", "console.log(process.env.LANG ?? 'LANG is not set')"],
cwd: string
cwd
:
var process: NodeJS.Process
process
.
NodeJS.Process.cwd(): string

The process.cwd() method returns the current working directory of the Node.js process.

import { cwd } from 'node:process';
console.log(`Current directory: ${cwd()}`);

@since ― v0.1.8

cwd
(),
environment: SandboxEnvironment
environment
: {
allow: readonly string[]
allow
: ["LANG"] },
mounts: readonly SandboxMount[]
mounts
: [],
network: NetworkDisabled | NetworkAllowlist
network
:
class NetworkDisabled

Explicitly denies all workload network access.

NetworkDisabled
.
BottomWithoutNew<unknown, unknown, unknown, unknown, Declaration, decodeTo<declareConstructor<NetworkDisabled, { readonly _tag: "NetworkDisabled"; }, readonly [TaggedStruct<"NetworkDisabled", {}>], { ...; }>, TaggedStruct<...>, never, never>, ... 8 more ..., "required">.make(input: void | {
readonly _tag?: "NetworkDisabled" | undefined;
}, options?: MakeOptions): NetworkDisabled

Constructs a value from the make input representation synchronously.

When to use

Use when constructor input is trusted or when validation failure should abort with a thrown Error.

Details

Applies constructor defaults and type-side validation according to MakeOptions.

Gotchas

Throws an Error with the schema issue in its cause when validation fails. Schema validation failures use the generic message "Schema validation failed"; format the cause explicitly with SchemaIssue.makeFormatterDefault() when human-readable details are needed. Causes that contain defects, interruptions, or other non-schema reasons throw with the underlying Cause attached instead.

@see ― BottomWithoutNew.makeOption — construct synchronously and discard validation details

@see ― BottomWithoutNew.makeEffect — construct through Effect when validation failure should stay in the error channel

make
({}),
limits: SandboxLimits
limits
: {
maxOutputBytes: number
maxOutputBytes
: 16 * 1024,
maxWallTime: Duration.Duration
maxWallTime
:
import Duration
Duration
.
const seconds: (seconds: number) => Duration.Duration

Creates a Duration from seconds.

Example (Creating durations from seconds)

import { Duration } from "effect"
Duration.toMillis(Duration.seconds(30)) // => 30_000

@category ― constructors

@since ― 2.0.0

seconds
(10),
},
secretHandles: readonly SandboxSecretHandle[]
secretHandles
: [],
artifactRules: readonly SandboxArtifactRule[]
artifactRules
: [],
});
const
const program: Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>
program
=
import Effect
Effect
.
const gen: <Effect.Effect<{
readonly execute: (request: SandboxRequest) => Stream.Stream<SandboxEvent, SandboxError>;
}, never, Sandbox> | Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>, void>(f: () => Generator<Effect.Effect<{
readonly execute: (request: SandboxRequest) => Stream.Stream<SandboxEvent, SandboxError>;
}, never, Sandbox> | Effect.Effect<...>, void, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
await Effect.runPromise(program) // => "Final amount to charge: 96"

@category ― constructors

@since ― 2.0.0

gen
(function* () {
const
const sandbox: {
readonly execute: (request: SandboxRequest) => Stream.Stream<SandboxEvent, SandboxError>;
}
sandbox
= yield*
class Sandbox

Scoped command/code execution capability. Implementations must identify their isolation posture in every emitted event; an unisolated implementation is never a security sandbox.

Sandbox
;
yield*
const sandbox: {
readonly execute: (request: SandboxRequest) => Stream.Stream<SandboxEvent, SandboxError>;
}
sandbox
.
execute: (request: SandboxRequest) => Stream.Stream<SandboxEvent, SandboxError>
execute
(
const request: SandboxRequest
request
).
Pipeable.pipe<Stream.Stream<SandboxStarted | SandboxOutput | SandboxExited, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>, Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>>(this: Stream.Stream<...>, ab: (_: Stream.Stream<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Stream
Stream
.
const runForEach: <SandboxStarted | SandboxOutput | SandboxExited, void, never, never>(f: (a: SandboxStarted | SandboxOutput | SandboxExited) => Effect.Effect<void, never, never>) => <E, R>(self: Stream.Stream<SandboxStarted | SandboxOutput | SandboxExited, E, R>) => Effect.Effect<void, E, R> (+1 overload)

Runs the provided effectful callback for each element of the stream.

Example (Running an effect for each value)

import { Effect, Stream } from "effect"
const stream = Stream.make(1, 2, 3)
const values: Array<string> = []
const program = Effect.gen(function*() {
yield* Stream.runForEach(stream, (n) => Effect.sync(() => values.push(`Processing: ${n}`)))
})
await Effect.runPromise(program)
values // => ["Processing: 1", "Processing: 2", "Processing: 3"]

@category ― destructors

@since ― 2.0.0

runForEach
((
event: SandboxStarted | SandboxOutput | SandboxExited
event
) => {
switch (
event: SandboxStarted | SandboxOutput | SandboxExited
event
.
_tag: "SandboxStarted" | "SandboxOutput" | "SandboxExited"
_tag
) {
case "SandboxStarted":
return
import Console
Console
.
const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>

Logs a general-purpose message to the console.

Example (Writing log messages)

import { Console, Effect } from "effect"
const messages: Array<ReadonlyArray<unknown>> = []
const testConsole: Console.Console = Object.assign(Object.create(console), {
log: (...args: ReadonlyArray<unknown>) => messages.push(args)
})
const program = Effect.gen(function*() {
yield* Console.log("Hello, world!")
yield* Console.log("User data:", { name: "John", age: 30 })
yield* Console.log("Processing", 42, "items")
})
Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
const expected = [
["Hello, world!"],
["User data:", { name: "John", age: 30 }],
["Processing", 42, "items"]
]
messages // => expected

@category ― accessors

@since ― 2.0.0

log
(`started ${
event: SandboxStarted
event
.
implementation: SandboxImplementation
implementation
.
identity: string
identity
}`);
case "SandboxOutput":
return
event: SandboxOutput
event
.
stream: "stdout" | "stderr"
stream
=== "stdout" ?
import Console
Console
.
const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>

Logs a general-purpose message to the console.

Example (Writing log messages)

import { Console, Effect } from "effect"
const messages: Array<ReadonlyArray<unknown>> = []
const testConsole: Console.Console = Object.assign(Object.create(console), {
log: (...args: ReadonlyArray<unknown>) => messages.push(args)
})
const program = Effect.gen(function*() {
yield* Console.log("Hello, world!")
yield* Console.log("User data:", { name: "John", age: 30 })
yield* Console.log("Processing", 42, "items")
})
Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
const expected = [
["Hello, world!"],
["User data:", { name: "John", age: 30 }],
["Processing", 42, "items"]
]
messages // => expected

@category ― accessors

@since ― 2.0.0

log
(
event: SandboxOutput
event
.
text: string
text
) :
import Console
Console
.
const error: (...args: ReadonlyArray<any>) => Effect.Effect<void>

Writes an error-level message to the console, typically displayed with error styling by the active console implementation.

Example (Writing error messages)

import { Console, Effect } from "effect"
const messages: Array<ReadonlyArray<unknown>> = []
const testConsole: Console.Console = Object.assign(Object.create(console), {
error: (...args: ReadonlyArray<unknown>) => messages.push(args)
})
const program = Effect.gen(function*() {
yield* Console.error("Something went wrong!")
yield* Console.error("Error details:", {
code: 500,
message: "Internal Server Error"
})
})
Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
const expected = [
["Something went wrong!"],
["Error details:", { code: 500, message: "Internal Server Error" }]
]
messages // => expected

@category ― accessors

@since ― 2.0.0

error
(
event: SandboxOutput
event
.
text: string
text
);
case "SandboxExited":
return
import Console
Console
.
const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>

Logs a general-purpose message to the console.

Example (Writing log messages)

import { Console, Effect } from "effect"
const messages: Array<ReadonlyArray<unknown>> = []
const testConsole: Console.Console = Object.assign(Object.create(console), {
log: (...args: ReadonlyArray<unknown>) => messages.push(args)
})
const program = Effect.gen(function*() {
yield* Console.log("Hello, world!")
yield* Console.log("User data:", { name: "John", age: 30 })
yield* Console.log("Processing", 42, "items")
})
Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
const expected = [
["Hello, world!"],
["User data:", { name: "John", age: 30 }],
["Processing", 42, "items"]
]
messages // => expected

@category ― accessors

@since ― 2.0.0

log
(`exited ${
event: SandboxExited
event
.
exitCode: number
exitCode
}`);
}
}),
);
}).
Pipeable.pipe<Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, Sandbox>, Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Sandbox, never, never>(layer: Layer<Sandbox, never, never>, options?: {
readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Sandbox>> (+5 overloads)

Provides dependencies to an effect using layers or a context. Use options.local to build the layer every time; by default, layers are shared between provide calls.

Example (Providing dependencies with a layer)

import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLayer = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLayer)
await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"

@category ― providing services

@since ― 2.0.0

provide
(
import LocalSandbox
LocalSandbox
.
const layer: Layer<Sandbox, never, never>

A scoped Node process implementation whose events are always labeled unisolated.

layer
),
import Effect
Effect
.
const scoped: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Scope>>

Runs an effect with a scope that closes when the effect completes.

When to use

Use to acquire scoped resources for the duration of a single workflow.

Details

Finalizers for resources acquired inside the workflow run as soon as the workflow completes, whether by success, failure, or interruption.

Example (Running a scoped acquisition)

import { Effect } from "effect"
const output: Array<unknown> = []
const resource = Effect.acquireRelease(
Effect.sync(() => { output.push("Acquiring resource") }).pipe(Effect.as("resource")),
() => Effect.sync(() => { output.push("Releasing resource") })
)
const program = Effect.scoped(
Effect.gen(function*() {
const res = yield* resource
yield* Effect.sync(() => { output.push(`Using ${res}`) })
return res
})
)
Effect.runSync(program)
output // => ["Acquiring resource", "Using resource", "Releasing resource"]

@category ― resource management

@since ― 2.0.0

scoped
);
import NodeRuntime
NodeRuntime
.
const runMain: <SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, void>(effect: Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>, options?: {
readonly disableErrorReporting?: boolean | undefined;
readonly teardown?: Teardown | undefined;
}) => void (+1 overload)

Helps you run a main effect with built-in error handling, logging, and signal management.

When to use

Use to run a Node.js application's main Effect with structured error handling, log management, interrupt support, or advanced teardown capabilities.

Details

This function launches an Effect as the main entry point, setting exit codes based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally logging errors. By default, it logs errors and uses a "pretty" format, but both behaviors can be turned off. You can also provide custom teardown logic to finalize resources or produce different exit codes.

The optional configuration object can include:

  • disableErrorReporting: Turn off automatic error logging.
  • teardown: Provide custom finalization logic.

@category ― running

@since ― 4.0.0

runMain
(
const program: Effect.Effect<void, SandboxSpawnError | SandboxExitError | SandboxTimeoutError | SandboxOutputLimitError | SandboxUnsupportedRequestError, never>
program
);

Save this as sandbox.ts and run it with Node.js:

node --experimental-transform-types sandbox.ts

Sandbox.execute emits SandboxStarted, zero or more SandboxOutput events, then SandboxExited when the process reaches an exit status. Stdout and stderr events may interleave. The local adapter counts their combined bytes against maxOutputBytes.

A zero exit code completes the stream. A nonzero exit still emits SandboxExited first, then fails with SandboxExitError. NodeRuntime.runMain reports the failure and exits unsuccessfully. A spawn failure has no started or exited event. SandboxOutputLimitError and SandboxTimeoutError also remain in the typed error channel. Use Effect.catchTag when your application has a recovery action for one of these failures.

Interrupting the consumer preserves Effect interruption. The adapter does not turn it into an exit record or a SandboxError. Keep the call in a Scope, as above, so stream finalization owns child process cleanup.

The local adapter enforces one wall-clock limit across environment loading, process startup, and stream consumption. Output activity does not reset this deadline. It also enforces the combined stdout and stderr byte limit. It does not enforce process isolation, mount access, CPU limits, memory limits, secret-handle resolution, or artifact collection. Requests that ask for those features fail as SandboxUnsupportedRequestError before the process starts.

NetworkDisabled is required by the local adapter because it rejects allowlists. It is only a request marker there. The adapter does not configure the operating system or a network namespace, so it does not prevent the child process from opening a connection. Do not run untrusted commands, model-generated commands, or sensitive credentials through this adapter.

The local adapter also rejects any runtime identity other than local-process. It passes no environment values unless they appear in environment.allow, and it never resolves secretHandles into environment variables. Put secrets behind a real isolated runtime or an application-owned host API.

CodeExecutor accepts only JavaScript source that evaluates to one async function. It has bounded source, logs, result, wall-clock time, CPU time when supported, host calls, and host-call payloads. An implementation must enforce each requested limit or return a typed unsupported error. Use Code Mode when an agent should write that program and call explicitly allowed application Tools.

PageCapture returns one rendered content, Markdown, links, structured extraction, or selector scrape result. Its output is untrusted input. PageCrawl returns a scoped stream of rendered Markdown from one exact HTTPS host. InteractiveBrowser returns a scoped handle that cannot be persisted or transferred. Its actions can have uncertain outcomes, especially after a click or fill request.

The Cloudflare adapters provide page capture, crawl, screenshots, and interactive browser passes. They do not make local command execution isolated. Keep browser and process capabilities behind the host policy that authenticates callers and authorizes the resources they may reach.