On this page

All code in ADF — whether run by sys_code, sys_lambda, trigger lambdas, timer lambdas, or API route handlers — executes inside a sandboxed environment built on Node.js Worker Threads and V8 VM Contexts. sys_code and sys_lambda are themselves ordinary tools[] entries, toggled like any other tool (tools.sys_code.enabled).

Execution Contexts

ContextEntry PointState PersistenceAuthorizationReceives
sys_codeLLM calls the toolYes — same worker per agent, variables carry overAlways unauthorizedRaw code string
sys_lambdaLLM calls the toolNo — fresh VM context per callHIL if target is authorized; otherwise unauthorizedargs object via destructuring
Trigger lambdaSystem scope trigger firesNo — fresh VM context per call (unless warm: true)Based on file’s authorized flagevent object (details)
Timer lambdaon_timer system scope firesNo — fresh VM context per call (unless warm: true)Based on file’s authorized flagevent object (details)
API route handlerHTTP request matches a routeNo — fresh VM context per call (unless warm: true)Based on file’s authorized flagrequest object (details)
Middleware lambdaPipeline integration point firesNo — fresh VM context per callBased on file’s authorized flaginput object (details)

All contexts have access to the adf proxy object for calling tools, invoking the model (including multimodal content blocks — image_url, input_audio, video_url — for capable models; see model_invoke), and running lambdas.

The Authorization column indicates whether the execution context can call restricted tools and methods. sys_code always runs unauthorized — inline code has no provenance. When the LLM calls sys_lambda targeting an authorized file, the runtime triggers a HIL approval prompt; if approved, the lambda runs with authorization. Unauthorized targets run without authorization, no prompt needed. System-initiated contexts (triggers, timers, middleware) inherit authorization from the source file’s authorized flag. See Authorized Code Execution for the full security model.

Security Model

The sandbox is configured with:

codeGeneration: { strings: false, wasm: true }

This disables:

  • eval() and new Function() — no dynamic code generation from strings

WebAssembly is enabled to support standard library packages that use WASM (e.g., sql.js, mupdf).

Native fetch and related globals (Request, Response, Headers) are deleted from the worker scope on startup. All network access must go through adf.sys_fetch(), which routes through the agent’s security middleware pipeline. The code_execution.network flag (default false) is what changes this — when enabled, the sandbox keeps direct network access.

Available Globals

The sandbox exposes a curated set of standard JavaScript globals:

Core types: Array, Object, Map, Set, WeakMap, WeakSet, String, Number, Boolean, Symbol, BigInt, RegExp

Error types: Error, TypeError, RangeError, SyntaxError, URIError, ReferenceError, EvalError

Numbers and encoding: parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined, encodeURIComponent, decodeURIComponent, encodeURI, decodeURI, atob, btoa

Binary data: ArrayBuffer, SharedArrayBuffer, DataView, Uint8Array, Uint16Array, Uint32Array, Uint8ClampedArray, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array, Buffer

Async and timing: Promise, setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotask

Utilities: Math, Date, JSON, structuredClone, TextEncoder, TextDecoder, URL, URLSearchParams

ADF-specific: adf (proxy object), __require (for allowed Node.js modules and standard library packages), __stdlibPath (base path for standard library packages on disk)

Not Available

GlobalReasonAlternative
fetchDeleted on worker startup — no direct network accessadf.sys_fetch()
eval / FunctionDisabled by codeGeneration policyWrite code directly
require / importNo arbitrary module loadingimport from allowed modules and standard library (see below)
processNo access to host processN/A
__dirname / __filenameNo filesystem path contextN/A

Note: console is available in every execution context. Lambda contexts (sys_lambda, triggers, timers, API routes) have console.log, console.warn, console.error, and console.info — output is captured and logged to adf_logs. The sys_code context also gets a console injected per execution; its output is returned as stdout in the tool result.

Allowed Node.js Modules

You can use standard import syntax to load these built-in Node.js modules:

ModuleUse Case
cryptoHashing, HMAC, random bytes, encryption
bufferBinary data manipulation
urlURL parsing and formatting
querystringQuery string parsing
pathFile path manipulation
utilUtility functions (inspect, format, promisify)
string_decoderBuffer-to-string decoding
punycodeUnicode/ASCII domain encoding
assertAssertions for validation
eventsEventEmitter pattern
streamStream processing
zlibCompression (gzip, deflate, brotli)
osOS information (platform, arch, cpus)
import { createHash } from 'crypto'
import { join } from 'path'

const hash = createHash('sha256').update('hello').digest('hex')
const filePath = join('lib', 'utils.ts')

Importing any module not in this list (and not in the standard library or custom packages) throws an error:

Module "fs" is not available in the sandbox. Available modules: crypto, buffer, url, ..., xlsx, pdf-lib, ...

Standard Library Packages

In addition to Node.js built-in modules, the sandbox provides a curated set of npm packages for document processing, data manipulation, and image handling. These are always available — no configuration needed.

PackageVersionUse Case
xlsx0.18.5Read/write Excel spreadsheets (.xlsx, .xls, .csv)
pdf-lib1.17.1Create and modify PDF documents
mupdf0.3.0Parse and extract content from existing PDFs
docx9.0.2Generate Word documents (.docx)
jszip3.10.1Create and extract ZIP archives
sql.js1.11.0In-memory SQLite database (WebAssembly)
cheerio1.0.0Parse and manipulate HTML (jQuery-like API)
yaml2.6.0Parse and stringify YAML
date-fns4.1.0Date/time manipulation and formatting
jimp1.6.0Image processing (resize, crop, rotate, filters)

All packages are pure JavaScript or WebAssembly — no native addons.

Usage Examples

// Parse an Excel file from the VFS
import { read, utils } from 'xlsx'
const file = await adf.fs_read({ path: 'data/report.xlsx' })
const buf = Buffer.from(file.content, 'base64')
const workbook = read(buf)
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const rows = utils.sheet_to_json(sheet)

// Create a PDF
import { PDFDocument } from 'pdf-lib'
const doc = await PDFDocument.create()
const page = doc.addPage()
page.drawText('Hello from ADF')
const bytes = await doc.save()
await adf.fs_write({ mode: 'write', path: 'output.pdf', content: Buffer.from(bytes), mime_type: 'application/pdf' })

// Parse HTML
import * as cheerio from 'cheerio'
const resp = await adf.sys_fetch({ url: 'https://example.com' })
const $ = cheerio.load(resp.body)
const title = $('title').text()

// In-memory SQLite
import initSqlJs from 'sql.js'
const SQL = await initSqlJs()
const db = new SQL.Database()
db.run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)')
db.run("INSERT INTO test VALUES (1, 'hello')")
const results = db.exec('SELECT * FROM test')

// Process images
import { Jimp } from 'jimp'
const imgFile = await adf.fs_read({ path: 'photo.png' })
const image = await Jimp.read(Buffer.from(imgFile.content, 'base64'))
image.resize({ w: 200, h: 200 })
const output = await image.getBuffer('image/png')
await adf.fs_write({ mode: 'write', path: 'thumb.png', content: output, mime_type: 'image/png' })

// Parse/stringify YAML
import YAML from 'yaml'
const config = YAML.parse('key: value\nlist:\n  - a\n  - b')
const yamlStr = YAML.stringify({ hello: 'world' })

// Date manipulation
import { format, addDays } from 'date-fns'
const tomorrow = format(addDays(new Date(), 1), 'yyyy-MM-dd')

// Read a PDF with mupdf
import mupdf from 'mupdf'
const pdfFile = await adf.fs_read({ path: 'document.pdf' })
const pdfDoc = mupdf.Document.openDocument(Buffer.from(pdfFile.content, 'base64'), 'application/pdf')
const pageCount = pdfDoc.countPages()

First-Launch Install

Standard library packages are installed automatically on first launch to ~/.adf-studio/sandbox-stdlib/. During the initial install (typically 1-2 minutes), attempting to import a stdlib package throws:

Module "xlsx" is not available. Standard library is still installing — try again shortly.

A progress banner appears in the UI during installation. Subsequent launches use the cached packages.

Custom Packages

Beyond the standard library, agents can install additional npm packages using the npm_install tool. Packages must be pure JavaScript or WebAssembly — native addons are detected and blocked.

Installing from Code

// Agent installs a package (persisted to its config)
await adf.npm_install({ name: 'vega-lite', version: '^5.21.0' })
await adf.npm_install({ name: 'vega' })
await adf.npm_install({ name: '@resvg/resvg-wasm' })

Installed packages become importable on the next turn:

import * as vl from 'vega-lite'
import * as vega from 'vega'
import { Resvg } from '@resvg/resvg-wasm'

const spec = { /* vega-lite spec */ }
const compiled = vl.compile(spec)
const view = new vega.View(vega.parse(compiled.spec), { renderer: 'none' })
const svg = await view.toSVG()

// SVG → PNG via WASM (auto-initialized, no initWasm() call needed)
const png = new Resvg(svg, { fitTo: { mode: 'width', value: 800 } }).render().asPng()
await adf.fs_write({ mode: 'write', path: 'chart.png', content: Buffer.from(png).toString('base64'), encoding: 'base64', mime_type: 'image/png' })

Three Package Tiers

TierScopeManaged by
Standard libraryAll agents, alwaysBundled with Studio
Runtime packagesAll agents on this instanceUser via Settings > Packages
Agent packagesSingle agentAgent via npm_install / agent config UI

Module resolution follows this order: Node built-ins → stdlib → runtime packages → agent packages. If a package isn’t in the agent’s config or the runtime config, the import throws MODULE_NOT_FOUND even if the package is installed on disk.

Runtime Packages

Runtime packages are configured in Settings > Packages and are available to every agent. Use this for packages you want globally available (e.g., charting libraries, data processing tools). Agents can also promote their own packages to runtime via the Make Runtime button in Settings. Both surfaces are owner-only — instance-scoped, not agent config; ask your principal.

Limits

LimitValue
Per-package install size50 MB
Total user packages200 MB
Max packages per agent50

WASM Auto-Initialization

Packages that use WebAssembly and export an initWasm() function (common for wasm-bindgen packages like @resvg/resvg-wasm) are auto-initialized during import. The sandbox detects the .wasm file in the package directory, reads it, and calls initWasm(buffer) before returning the module. No manual initialization is needed.

First-Open Install Prompt

When opening an agent that declares packages in code_execution.packages, Studio checks if those packages are installed. Missing packages trigger a modal prompting the user to install them or skip.

Import/Export Transforms

Before execution, the sandbox transforms modern JavaScript syntax:

Imports are converted to await __require() calls:

// Written as:
import { createHash } from 'crypto'
import path from 'path'
import * as util from 'util'

// Transformed to:
const { createHash } = await __require('crypto')
const path = await __require('path')
const util = await __require('util')

The await is required for standard library packages that use ESM with top-level await (e.g., mupdf). For Node.js built-in modules, __require() resolves synchronously but the await is harmless.

Exports are stripped so functions/constants become context-accessible:

// Written as:
export function process(data) { ... }
export const VERSION = '1.0'

// Transformed to:
function process(data) { ... }
const VERSION = '1.0'

This means you can write standard TypeScript/JavaScript modules and they work in the sandbox.

The adf Object

Every execution context has access to the global adf proxy object. It provides an async RPC bridge to all enabled agent tools, the LLM model, the lambda execution engine, and the identity store. In addition to regular tools, the following special methods are available only from code execution (controlled via the Code Execution config): model_invoke, sys_lambda, task_resolve, loop_inject, identity_status, get_identity, set_identity, emit_event, attestation_list, attestation_add, and attestation_issue. identity_status reports only envelope state, never a secret or key. attestation_issue (signing certs about other DIDs with this agent’s key) is restricted to authorized code by default via code_execution.restricted_methods. Additional methods are available exclusively from authorized code: set_meta_protection, set_file_protection (and sys_set_meta/sys_delete_meta bypass protection checks when authorized).

Each special method is gated by a boolean at code_execution.<method> (e.g. code_execution.set_identity), agent-writable via sys_update_config (HIL-gated: your principal approves). The exception is code_execution.restricted_methods, which is owner-only — agent writes are hard-denied.

Discovering Tool Schemas

Never guess the input shape of an adf.<tool>({...}) call — fetch the exact schema first:

const tools = await adf.sys_get_config({ section: 'tools' })

This returns every tool — including hidden, absorbed, and disabled ones — with its state and schema. From the shell, the equivalent is config tools (list all) or config tools <name|substring> (full JSON schemas for matches).

Bypassing Output Limits (_full)

Tools like db_query truncate their output by default to protect the LLM context window. Since code execution results go to your code (not the model), you can add _full: true to get the complete, untruncated result:

const allRows = await adf.db_query({ sql: 'SELECT * FROM local_events', _full: true })

Note: fs_read always returns full content from code execution — no _full needed:

const result = await adf.fs_read({ path: 'data/export.csv' })
const lines = result.content.split('\n')

This parameter is only honored in code execution contexts — the runtime strips it from direct LLM tool calls. See the adf object reference for details.

See the adf Proxy Object Reference for the complete API.

Console and Logging

Console behavior varies by context:

Contextconsole AvailableOutput Destination
sys_codeYes (injected per execution)Returned as stdout in tool result
sys_lambdaYesReturned as stdout in tool result, logged to adf_logs
Trigger lambdasYesLogged to adf_logs
Timer lambdasYesLogged to adf_logs
API route handlersYesLogged to adf_logs with the api_response entry

All console methods (log, warn, error, info) are captured. warn and error prefix output with [warn] and [error] respectively.

Timeouts

Code execution timeout is governed by the agent’s limits.execution_timeout_ms:

SettingValue
Effective defaultlimits.execution_timeout_ms (default 60000 ms / 60 s)
Ceilingmin(limits.execution_timeout_ms, 300000) — a hard 300 s (5 minutes) sandbox cap

The sys_code tool accepts an optional timeout parameter (in milliseconds). When omitted it defaults to limits.execution_timeout_ms; any value is clamped to the 300-second sandbox ceiling. If execution exceeds the timeout, the operation fails with a TIMEOUT error code.

The worker itself has an additional 2-second buffer beyond the configured timeout to allow pending RPC round-trips to complete before the worker is forcibly terminated.

State Persistence

sys_code uses a persistent worker per agent. Variables, functions, and state defined in one sys_code call carry over to the next. This makes it suitable for building up state incrementally:

// First call
let counter = 0
function increment() { return ++counter }

// Second call — counter and increment() still exist
const val = increment() // returns 1

sys_lambda, trigger lambdas, timer lambdas, and API route handlers use fresh VM contexts by default. Each invocation starts clean with no leftover state.

Warm mode: Trigger targets, timers, and API routes can set warm: true to keep the sandbox worker alive between invocations. This trades isolation for performance — useful for frequently-firing triggers or high-traffic API endpoints. The sandbox IDs are:

  • Trigger/timer lambdas: {agentId}:lambda
  • API routes: {agentId}:api

Error Handling

All adf.* calls can throw errors. Use try/catch to handle them:

try {
  const data = await adf.fs_read({ path: 'config.json' })
  const config = JSON.parse(data)
} catch (err) {
  // err.code contains the error code (e.g., 'NOT_FOUND', 'TOOL_ERROR')
  // err.message contains a human-readable description
  await adf.fs_write({ path: 'errors.log', content: `Error: ${err.message}\n` })
}

The sandbox also fast-fails for certain conditions without making an RPC round-trip:

  • Disabled/unknown tools — If the tool isn’t in the agent’s enabled set (and not restricted), throws immediately with code NOT_FOUND
  • Restricted tools — Tools with restricted: true cannot be called from unauthorized code, throws with code REQUIRES_AUTHORIZED_CODE. Authorized code can call restricted tools directly (bypassing HIL).

See the adf object error codes for the complete list.

Circular Call Detection

When sys_lambda calls another sys_lambda (via the adf proxy), the runtime tracks the call stack. If a circular call is detected (A calls B which calls A), execution fails immediately with a CIRCULAR_CALL error:

Circular sys_lambda detected: lib/a.ts:process → lib/b.ts:transform → lib/a.ts:process

This prevents infinite recursion between lambda functions.