Execbox Runner Specification
This page defines the runner specification for transport-backed execbox runners.
Use it when you want to implement a non-TypeScript runner, such as a Go remote runner, without reverse-engineering the shipped TypeScript implementation. For the control-flow walkthrough, read execbox-remote-workflow.md. For the message catalog, read execbox-protocol-reference.md.
Table of Contents
- Status And Scope
- Core Model
- Session Model
- Inputs To The Runner
- Guest Namespace Contract
- Tool Call Contract
- Serialization Contract
- Logs
- Final Result Contract
- Cancellation And Transport Failure
- Execution Id And Message Correlation
- Minimal Success Transcript
- Minimal Cancellation Transcript
Status And Scope
This is the normative runner specification for the current execbox runner contract.
It defines:
- the execution lifecycle a conforming runner must implement
- what crosses the host/runner boundary
- how guest-visible tools behave
- how results, logs, cancellation, and failures must surface
It does not define:
- a built-in network transport such as HTTP or WebSocket
- authentication, tenancy, or deployment policy
- how a runner internally embeds JavaScript
A conforming runner may use a different language or engine internally, as long as its externally visible behavior matches this contract.
Core Model
Execbox splits responsibility between a trusted host and an untrusted or less-trusted runner:
- the host owns providers, tool closures, validation, upstream clients, API clients, and secrets
- the runner owns guest JavaScript execution, guest-visible tool proxies, console capture, and final result emission
The runner never receives host closures or secrets. It only receives code, runtime options, and provider metadata.
Session Model
A transport-backed runner covered by this specification is single-execution and single-session:
- one transport session carries exactly one active execution
- the transport is bidirectional and sustained for the lifetime of that execution
- the host may open a fresh transport per execution
- the runner must not require a second callback connection for tool calls
If the runner receives a second execute while one execution is active, it should reject that new request with a terminal done carrying internal_error.
State Machine
Specification rules:
- a successful session must end with exactly one
done startedshould be emitted once after the runner acceptsexecuteand before anytool_callordonetool_callandtool_resultmay repeat zero or more times beforedone- after
done, the session is complete and no further protocol messages are valid for that execution
Inputs To The Runner
The runner receives one execute message:
{
type: "execute";
id: string;
code: string;
options: ExecutorRuntimeOptions;
providers: ProviderManifest[];
}Requirements:
ididentifies the execution sessioncodeis the full guest JavaScript program to runoptionscarries timeout, memory, and log limitsprovidersis metadata only; it is not executable capability
The runner must not assume any provider manifest contains:
- host closures
- upstream MCP clients
- tenant maps
- API keys or other secrets
Guest Namespace Contract
Each ProviderManifest becomes one guest-visible global namespace whose property names are the manifest tool safeName values.
For a provider manifest like:
{
name: "firecrawl",
tools: {
scrape_url: {
safeName: "scrape_url",
originalName: "scrape-url"
}
},
types: "declare namespace firecrawl { ... }"
}the guest runtime must expose:
await firecrawl.scrape_url(input);Injected-tool requirements:
- each tool must be an async or Promise-returning function
- only the first guest argument is transported as tool input
- omitted input is treated as
undefined - the emitted tool input must be transport-safe
- the proxy must suspend normal async execution until a matching
tool_resultarrives
Pause/Resume Semantics
When guest code runs:
const page = await firecrawl.scrape_url({ url: "https://example.com" });
const title = page.title ?? null;the conforming runner must:
- create a pending Promise for that tool call
- emit
tool_call - let JavaScript pause at the
await - wait for a
tool_resultwith the samecallId - resolve or reject the pending Promise
- resume the same guest execution after the
await
This pause is normal JavaScript Promise suspension, not a source-to-source rewrite pass.
Tool Call Contract
Each guest tool invocation emits:
{
type: "tool_call";
callId: string;
providerName: string;
safeToolName: string;
input: unknown;
}Specification rules:
callIdmust uniquely identify one tool invocation within the executionproviderNameandsafeToolNamemust match the injected namespace/tool pair- the runner must not emit a second
tool_callwith the samecallId
The host responds with exactly one matching tool_result:
{
type: "tool_result";
callId: string;
ok: true;
result: unknown;
}or:
{
type: "tool_result";
callId: string;
ok: false;
error: {
code: ExecuteErrorCode;
message: string;
}
}Specification rules:
tool_result.callIdmust correlate exactly one pending tool Promise- a successful
tool_resultresolves the Promise toresult - a failing
tool_resultrejects the Promise with an Error-like value whose.messageis the trusted host message and whose.codeis the trusted host error code - trusted host-originated tool failures must remain distinguishable from guest-created errors so the final uncaught result can preserve the trusted host error code
Serialization Contract
Every value that crosses the host/runner boundary must be transport-safe.
The current runner specification allows:
undefinedfor omitted tool input and successful expressions that evaluate toundefinednull- strings
- booleans
- finite numbers
- arrays of serializable values
- plain objects with serializable values
The runner specification rejects:
bigint- functions
- symbols
- non-finite numbers
- cyclic values
- non-plain objects as transported results
Requirements:
- non-serializable tool inputs must not be surfaced to the host as successful structured values
- non-serializable tool results must surface as
serialization_error - non-serializable final guest results must surface as
serialization_error
JSON Transport Note
The protocol types are defined as JavaScript values, but many real transports will serialize them as JSON.
Guidance for JSON-backed transports:
- omitted tool input may be represented by a missing
inputfield and interpreted asundefined - a successful final result of
undefinedmay be represented by an omittedresultfield whenok: true - a failed result still requires an explicit
error
Logs
Logs are captured runner-side and returned in the terminal done message as string[].
Requirements:
- expose
console.log,console.info,console.warn, andconsole.error - each call appends one log line
- one log line is the space-joined formatting of the console arguments
undefinedformats as the literal stringundefined- non-string values should be JSON-stringified when possible, with fallback string conversion when needed
Truncation
Log truncation is externally visible behavior and is part of this specification:
- first apply
maxLogLinesby keeping only the earliest lines up to the limit - then apply
maxLogCharscumulatively across those remaining lines - if the character limit is reached in the middle of a line, that final line is clipped
- truncated logs are returned from
done
Final Result Contract
The runner must terminate with one done message:
{
type: "done";
id: string;
ok: boolean;
durationMs: number;
logs: string[];
result?: unknown;
error?: {
code: ExecuteErrorCode;
message: string;
};
}Requirements:
idmust match the active execution id- exactly one of
resultorerrormust be present according took logsmust be astring[]durationMsmust measure execution wall time for that runresultmust be transport-safe, with omittedresultinterpreted asundefinedon successful JSON-serialized responses
Stable Error Codes
A conforming runner must use the current public error code set:
timeoutmemory_limitvalidation_errortool_errorruntime_errorserialization_errorinternal_error
Error Mapping Rules
Terminal-failure requirements:
- trusted host tool failures may surface as their trusted host code when uncaught by guest code
- guest-thrown values must not be upgraded into trusted host errors solely because their text mentions timeout or memory
- timeout must be reserved for real timeout or cancellation behavior
- memory-limit classification must be reserved for real runtime memory failures
- unknown or unexpected runner failures should surface as
internal_errororruntime_error, not as a forged trusted host error
Cancellation And Transport Failure
The host may send:
{
type: "cancel";
id: string;
}Requirements:
- if
idmatches the active execution, the runner must promptly abort execution - pending guest tool awaits should reject promptly so the execution can unwind
- cancellation should result in a terminal timeout-shaped failure for the caller
Host-session semantics that a conforming runner must tolerate:
cancelmay arrive before anytool_callcancelmay arrive while the runner is waiting on atool_result- the host may force-terminate the transport shortly after
cancelif the runner does not finish - unexpected transport close or transport error before
doneis terminal for the session
Execution Id And Message Correlation
Specification rules:
ididentifies the execution sessioncallIdidentifies one tool invocation inside that session- the host may ignore runner messages whose
iddoes not match the active execution - the runner must match
tool_resultbycallId - after a tool Promise settles, the associated
callIdis no longer active
Minimal Success Transcript
{"type":"execute","id":"exec-1","code":"const value = await tools.echo({\"ok\":true}); value.ok","options":{"timeoutMs":1000,"memoryLimitBytes":67108864,"maxLogLines":100,"maxLogChars":64000},"providers":[{"name":"tools","tools":{"echo":{"safeName":"echo","originalName":"echo","description":"Echo input"}},"types":"declare namespace tools { ... }"}]}
{"type":"started","id":"exec-1"}
{"type":"tool_call","callId":"call-1","providerName":"tools","safeToolName":"echo","input":{"ok":true}}
{"type":"tool_result","callId":"call-1","ok":true,"result":{"ok":true}}
{"type":"done","id":"exec-1","ok":true,"durationMs":3,"logs":[],"result":true}Minimal Cancellation Transcript
{"type":"execute","id":"exec-2","code":"await tools.hang({})","options":{"timeoutMs":1000,"memoryLimitBytes":67108864,"maxLogLines":100,"maxLogChars":64000},"providers":[{"name":"tools","tools":{"hang":{"safeName":"hang","originalName":"hang"}},"types":"declare namespace tools { ... }"}]}
{"type":"started","id":"exec-2"}
{"type":"tool_call","callId":"call-9","providerName":"tools","safeToolName":"hang","input":{}}
{"type":"cancel","id":"exec-2"}
{"type":"done","id":"exec-2","ok":false,"durationMs":1005,"logs":[],"error":{"code":"timeout","message":"Execution timed out"}}