Documentation
¶
Overview ¶
Package sandbox is agentloop's JS executor — a goja sandbox the model writes run(args) → return turns against, one capability per registered Pack.
Index ¶
- func ReservedModuleNames() []string
- func ValidateModuleCode(name, code string) error
- func ValidateModuleName(name string) error
- type DefaultPolicy
- type Event
- type EventKind
- type HelpEntry
- type InputRequestData
- type InputRequester
- type LogEntry
- type MultiPolicy
- type OnEvent
- type Pack
- func AIPack(ctx context.Context, client llm.Client, model string) Pack
- func FetchPack(ctx context.Context, requester InputRequester) Pack
- func HelpPack() Pack
- func HttpModulePack() Pack
- func MarkdownModulePack() Pack
- func RequirePack(ctx context.Context) Pack
- func SkillDiscoveryPack(skills []SkillInfo) Pack
- func SkillPack(name, code, description string) Pack
- type PolicyCheckResult
- type PolicyChecker
- type Sandbox
- func (s *Sandbox) AddHelpEntry(name, description string)
- func (s *Sandbox) AddSkillCode(name, code string)
- func (s *Sandbox) CheckPolicy(ctx context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
- func (s *Sandbox) CheckPolicyOrPanic(rt *goja.Runtime, ctx context.Context, toolName string, args map[string]any) PolicyCheckResult
- func (s *Sandbox) Emit(evt Event)
- func (s *Sandbox) Execute(code string) (result string, err error)
- func (s *Sandbox) ExecuteTurn(code string, argsJSON json.RawMessage) (logs string, ret json.RawMessage, err error)
- func (s *Sandbox) HelpEntries() []HelpEntry
- func (s *Sandbox) HelpEntry(name string) (string, bool)
- func (s *Sandbox) Logs() []LogEntry
- func (s *Sandbox) Packs() []Pack
- func (s *Sandbox) RegisterModule(name, code, description string) error
- func (s *Sandbox) RequireCacheGet(name string) (goja.Value, bool)
- func (s *Sandbox) RequireCacheSet(name string, v goja.Value)
- func (s *Sandbox) Runtime() *goja.Runtime
- func (s *Sandbox) SetExecTimeout(d time.Duration)
- func (s *Sandbox) SetOnEvent(fn OnEvent)
- func (s *Sandbox) SetPolicy(p PolicyChecker)
- func (s *Sandbox) SkillCode(name string) (string, bool)
- func (s *Sandbox) SystemPrompt() string
- func (s *Sandbox) TakeAnswer() (value string, ok bool)
- type SkillInfo
- type URLAllowlistPolicy
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ReservedModuleNames ¶
func ReservedModuleNames() []string
ReservedModuleNames returns a sorted snapshot of the reserved-name list. Surfaces externally (skill authoring guide, validation error detail) read from this rather than poking at private state.
func ValidateModuleCode ¶
ValidateModuleCode checks JS code for syntax errors without executing it. The function-wrap mirrors the runtime exec wrap so "compiles" here means "will compile at registration time too."
func ValidateModuleName ¶
ValidateModuleName reports whether name is a valid JS identifier and not in the reserved set. Used by product UIs to gate skill creation before persisting.
Types ¶
type DefaultPolicy ¶
type DefaultPolicy struct {
// AllowTools grants side-effecting tool names ("sendEmail",
// "secret"). Empty means all of them are denied.
AllowTools []string
// URLAllowPrefixes is the outbound allowlist for fetch/http,
// matched by prefix (same semantics as URLAllowlistPolicy). Empty
// means public URLs are allowed (the fetch pack's per-domain user
// approval still applies); non-empty means only matching URLs are.
// An explicit prefix also overrides the private-network denial —
// listing an internal URL is a conscious decision.
URLAllowPrefixes []string
// AllowPrivateNetworks disables the deny-by-default for loopback,
// RFC-1918/ULA, link-local, and *.localhost/*.local/*.internal
// targets. Leave false outside local development.
AllowPrivateNetworks bool
}
DefaultPolicy is the conservative PolicyChecker agentloop's Loop installs when Config.Policy is nil: side-effecting primitives are denied unless explicitly granted, and fetch/http may not reach private address ranges. The zero value is the safe default; a caller states its grants:
sandbox.DefaultPolicy{
AllowTools: []string{"sendEmail"},
URLAllowPrefixes: []string{"https://api.example.com/"},
}
A caller that truly wants fail-open passes AllowAll.
func (DefaultPolicy) Check ¶
func (p DefaultPolicy) Check(_ context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
Check implements PolicyChecker.
type Event ¶
type Event struct {
Kind EventKind `json:"kind"`
Summary string `json:"summary"`
Detail string `json:"detail,omitempty"`
Result string `json:"result,omitempty"`
// Payload carries a structured value when the kind implies one (e.g. a
// render block's spec). Optional; string-only consumers ignore it.
Payload map[string]any `json:"payload,omitempty"`
}
Event is one observability emission from inside the sandbox. Summary is the one-line description a trace / drawer renders; Detail carries the long-form payload (code, args, response body); Result carries the output when the kind implies one (a fetch response, an ai() reply).
type EventKind ¶
type EventKind string
EventKind identifies the type of sandbox event. The vocabulary is the observability surface every cross-cutting consumer (trace, audit, analytics, frontend drawer) reads.
New kinds get added here when a new pack lands; existing values are stable.
const ( // EventScript fires when Sandbox.Execute begins a JS run. EventScript EventKind = "script" // EventScriptResult fires when a JS run finishes (success or error). EventScriptResult EventKind = "script_result" // EventLog is a captured log() / print() / console.log() call. EventLog EventKind = "log" // EventDocumentSearch is a document-search primitive call. EventDocumentSearch EventKind = "document_search" // EventWebSearch is a web-search primitive call. EventWebSearch EventKind = "web_search" // EventFetch is an outbound HTTP request via fetch() / require('http'). EventFetch EventKind = "fetch" // EventMemory historically fired on memory ops; today it's reused // for secret reads. Kept named "memory" so existing trace consumers // don't break. EventMemory EventKind = "memory" // EventAsk fires when an input request is dispatched to a human. EventAsk EventKind = "ask" // EventAskResult fires when the user's response arrives. EventAskResult EventKind = "ask_result" // EventSkillCall fires when a user-authored skill module is invoked. EventSkillCall EventKind = "skill_call" // EventAI fires when ai() / aiJSON() / aiSearch() runs. EventAI EventKind = "ai" // EventPlan fires when plan() emits a reasoning step. EventPlan EventKind = "plan" // EventEmail fires when sendEmail() attempts a send. EventEmail EventKind = "email" // EventCard fires when a card() primitive emits a structured // artifact (deep-link, session-link, …). EventCard EventKind = "card" // EventCollInsert / Find / Update / Remove track per-store // collection ops. Application-supplied stores emit these. EventCollInsert EventKind = "collection_insert" EventCollFind EventKind = "collection_find" EventCollUpdate EventKind = "collection_update" EventCollRemove EventKind = "collection_remove" // EventAssetWrite fires when asset.write() persists a file. EventAssetWrite EventKind = "asset_created" // EventBlock carries a structured render block (chart/table/process-map/ // sql/…) emitted by a pack for a rich UI to render. Summary names the block // type; the structured spec rides in Event.Payload. EventBlock EventKind = "block" // EventError fires when a primitive errors. EventError EventKind = "error" // EventWarning fires for non-fatal degradation the trace should // surface — e.g. a capability whose Build failed and was skipped, // so the run proceeds with a smaller tool surface than configured. EventWarning EventKind = "warning" // EventAnswer fires when answer() delivers the run's final result. // Result carries the value (a string, or a JSON-encoded object/array). // The loop treats a called answer() as terminal. EventAnswer EventKind = "answer" )
type InputRequestData ¶
type InputRequestData struct {
// RequestID is a caller-allocated identifier. Optional.
RequestID string
// InputType is one of: "confirm", "choice", "text".
InputType string
// Message is the question shown to the user.
Message string
// Options is required for InputType == "choice".
Options []string
// Placeholder is hint text for InputType == "text" (optional).
Placeholder string
}
InputRequestData is the payload InputRequester.Request receives.
type InputRequester ¶
type InputRequester interface {
// Request dispatches the prompt and blocks until the user replies
// (or the underlying transport errors out). The returned value's
// concrete type depends on InputType:
// - "confirm" → bool
// - "choice" → string (the selected option)
// - "text" → string
Request(req InputRequestData) (any, error)
}
InputRequester is the sandbox-side seam for human-in-the-loop. The fetch pack's allowUrls auto-prompt calls Request — implementations close over the run's HITL transport and block until the user replies.
A nil InputRequester is the documented "no HITL for this run" case: FetchPack still requires the type to exist, but an unapproved-domain fetch simply fails with a policy-denial error instead of prompting.
type LogEntry ¶
LogEntry is a captured log() call retained for the current Execute call so we can surface the joined log lines as the return value when the script didn't produce one.
type MultiPolicy ¶
type MultiPolicy struct {
Checkers []PolicyChecker
}
MultiPolicy chains multiple checkers; the first denial wins. Each checker's ConfigJSON is preserved through the chain when every checker allows — later allow-with-config calls overwrite earlier ones, so put the canonical config provider (e.g. URLAllowlistPolicy) last in the chain.
func (MultiPolicy) Check ¶
func (m MultiPolicy) Check(ctx context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
Check implements PolicyChecker. Returns the first denial or the most-recent allow-with-config.
type OnEvent ¶
type OnEvent func(Event)
OnEvent is the callback shape Sandbox uses to publish events. A caller can wrap this into a Go channel so multiple consumers can subscribe without the sandbox itself fanning out.
type Pack ¶
type Pack struct {
// Name identifies the pack (e.g. "search", "fetch", "markdown").
Name string
// Description is a one-line summary shown in the skill-listing
// system primitive.
Description string
// Prompt is the detailed documentation inlined into the agent's
// system prompt — TypeScript-style `declare` lines work well
// because the model recognises the shape.
Prompt string
// HelpEntries maps JS function names to their help text. Surfaced
// via help(name).
HelpEntries map[string]string
// Register installs the pack's functions into the runtime. The
// Sandbox pointer is provided for state access (event emission,
// help-entry merging, skill-code registration).
Register func(rt *goja.Runtime, s *Sandbox)
}
Pack is a self-contained unit of sandbox functionality.
Each pack declares its name (must be unique per Sandbox), one-line description, a documentation block inlined into the agent's system prompt (this is how the LLM learns what functions are available and how to use them), and a Register function that installs JS functions into the Goja runtime.
HelpEntries map JS function names to their detailed help text — the in-sandbox help(name) primitive reads from here, so the LLM can inspect a function it doesn't recognise without a round trip.
func AIPack ¶
AIPack exposes synchronous LLM sub-calls inside the sandbox:
- ai(prompt, system?) — free-text completion. Fences stripped.
- aiJSON(prompt, schema?) — JSON-shaped completion. The pack augments the prompt with a "respond with JSON" instruction and parses the response. Schema is best-effort: the pack inlines it in the prompt rather than passing it to a native JSON-mode API (llm.Client doesn't yet expose JSON mode).
Both functions emit EventAI for the trace and gate through the installed PolicyChecker (under tool name "ai") so quota / tier limits can block them.
The pack also wires require('ai') as a module — a script can stick to either style.
func FetchPack ¶
func FetchPack(ctx context.Context, requester InputRequester) Pack
FetchPack creates a pack with fetch(), htmlToMarkdown(), allowUrls(), and fetchWhitelist() primitives. The pack runs every URL through the installed PolicyChecker (under tool name "fetch") and surfaces the allowlist through fetchWhitelist() so agents can pre-check before looping over fetches.
requester is the InputRequester used for the runtime allowUrls() auto-prompt — when fetch() is called against an unapproved host, the pack asks the user. Pass nil to skip the prompt path; fetches against unapproved hosts then fail with a policy-denial error.
func HelpPack ¶
func HelpPack() Pack
HelpPack provides the in-sandbox help() primitive. Should be registered LAST in the pack list so it sees every other pack's HelpEntries — the order is the caller's responsibility (e.g. DefaultSandboxBuilder), not this pack's.
func HttpModulePack ¶
func HttpModulePack() Pack
HttpModulePack registers a built-in `http` module reachable via require('http'). Wraps the internal _http() primitive (registered by FetchPack) so the script has named verbs instead of an options bag.
HttpModulePack depends on FetchPack being registered — _http needs to exist at require time. Register both as a pair.
func MarkdownModulePack ¶
func MarkdownModulePack() Pack
MarkdownModulePack creates a built-in module available via require('markdown'). It provides Go-backed markdown parsing functions for extracting structured data from markdown text without LLM calls.
func RequirePack ¶
RequirePack registers the require(name) primitive for CommonJS- style module loading inside the sandbox. Modules become loadable once a Pack calls Sandbox.AddSkillCode(name, body).
Each require(name) runs a PolicyChecker.Check with toolName=name so an application can gate individual modules without touching sandbox internals. Cached results are returned without re-checking: the policy decision on the initial load is the load decision for the rest of the session.
The ctx parameter is the per-session context the require() call shares with PolicyChecker — the loop's context, threaded through so a policy check can honour the run's cancellation.
func SkillDiscoveryPack ¶
SkillDiscoveryPack returns the pack that exposes skillList() and skillGet(name). The skills slice is the union of every other pack in the sandbox — assemblers (e.g. DefaultSandboxBuilder) build the list AFTER the other packs are constructed, then pass it here.
skillList() returns the full slice as a JS array. skillGet(name) returns the detailed help text registered for `name`, falling back to the SkillInfo.Description when no detailed help was registered.
func SkillPack ¶
SkillPack returns a Pack that wraps a user-authored JS skill module. It contributes the skill's docs to the system prompt and registers the JS function in the sandbox.
A skill name "foo" gets:
- AddSkillCode("foo", code) so require("foo") can load it.
- A function _skill_foo(args) defined in the runtime (the user's code wrapped).
- A Go-backed wrapper bound to globalThis.foo that emits an EventSkillCall event before delegating to _skill_foo.
The wrapper indirection is what gives observability — every skill invocation shows up in the trace without the user having to add any instrumentation.
type PolicyCheckResult ¶
type PolicyCheckResult struct {
Allowed bool
ConfigJSON json.RawMessage
Reason string
}
PolicyCheckResult is the sandbox-side view of a policy decision. When Allowed is false, Reason carries the human-readable explanation that's shown to the agent. When Allowed is true, ConfigJSON may carry tool-specific configuration the pack should consult (e.g. fetch's URL allowlist).
type PolicyChecker ¶
type PolicyChecker interface {
// Check is called by every pack with side effects before the
// effect runs. toolName is the primitive's name (e.g. "fetch",
// "sendEmail"); args carries the call's user-visible inputs.
//
// Implementations return PolicyCheckResult{Allowed: false, Reason: ...}
// to deny, or PolicyCheckResult{Allowed: true, ConfigJSON: ...}
// to allow optionally with tool-specific config (e.g. fetch's
// allowlist comes back here).
Check(ctx context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
}
PolicyChecker gates tool calls. Implementations typically close over the (workspace / session) tuple so the sandbox only has to pass tool-level inputs.
A nil PolicyChecker on the bare Sandbox fails open — useful for direct sandbox tests. agentloop's Loop installs DefaultPolicy when Config.Policy is nil.
var AllowAll PolicyChecker = allowAllPolicy{}
AllowAll is the explicit fail-open PolicyChecker. Every check passes with no config. Use it in tests and in the rare deployment that consciously wants ungated side effects — passing it is greppable and reviewable, unlike a nil-means-open default.
type Sandbox ¶
type Sandbox struct {
// contains filtered or unexported fields
}
Sandbox wraps a Goja JavaScript runtime with registered Packs.
Construction order matters: New() registers core primitives first, then each Pack in order, then snapshots the globalThis key set as baseKeys. Every Execute()/ExecuteTurn() rolls globalThis back to baseKeys before running the next script — cross-turn working memory lives in the threaded `args` (ExecuteTurn) or chat history, never in stray globals.
func New ¶
New creates a fresh Sandbox with core primitives and the given Packs.
Core primitives (log / print / console / text / plan / step / parseUrl) are always registered first. Each Pack's Register function is then called in order to install its functions, and its HelpEntries are merged into the help system.
func (*Sandbox) AddHelpEntry ¶
AddHelpEntry registers a help entry. Pack Register functions can call this to grow the help() listing after registration without having to mutate the Pack struct.
func (*Sandbox) AddSkillCode ¶
AddSkillCode registers a JS module body that the require(name) primitive can load. Packs that ship companion JS modules call this from their Register function.
func (*Sandbox) CheckPolicy ¶
func (s *Sandbox) CheckPolicy(ctx context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
CheckPolicy runs the installed checker and returns its result. A nil checker fails open with Allowed=true and no config.
func (*Sandbox) CheckPolicyOrPanic ¶
func (s *Sandbox) CheckPolicyOrPanic(rt *goja.Runtime, ctx context.Context, toolName string, args map[string]any) PolicyCheckResult
CheckPolicyOrPanic runs CheckPolicy and panics with a Goja runtime error when the call is denied or when the check itself errors — matching the panic(rt.NewGoError(...)) pattern packs already use for their other invariant violations. Returns the PolicyCheckResult on allow so callers can consume ConfigJSON for tool-specific sub-checks (fetch's URL allowlist is the canonical example).
func (*Sandbox) Execute ¶
Execute runs JavaScript in the sandbox and returns the auto-return value (the last expression). Code is wrapped in an IIFE so each call gets its own scope. Logs from this call are surfaced as the return value when the script didn't produce one.
Panics from host pack handlers are recovered into a sandbox error so a malformed exposure doesn't crash the agent goroutine.
func (*Sandbox) ExecuteTurn ¶
func (s *Sandbox) ExecuteTurn(code string, argsJSON json.RawMessage) (logs string, ret json.RawMessage, err error)
ExecuteTurn runs one turn under the run(args)→return contract used by the return-threading loop. argsJSON (the previous turn's return value) is injected as the global `args`; if the code defines `function run(args) { ... }` it is called with args and its return value is captured. The captured value is marshalled to JSON and returned as ret for the loop to thread into the next turn — it is NOT serialised into the LLM-visible result. logs holds the joined log() output, which (plus a structural digest the loop derives from ret) is the only thing the model sees back. A run that defines no run() function returns empty ret: loose statements carry nothing forward, only their side effects (log/answer/emit).
func (*Sandbox) HelpEntries ¶
HelpEntries returns a sorted slice of (name, description) pairs suitable for rendering the help() overview. The map is internal; the slice is a copy callers can iterate safely.
func (*Sandbox) HelpEntry ¶
HelpEntry returns the help text registered under name and a found-flag. Used by the help pack to format help() output and by the skill discovery pack to format skillGet().
func (*Sandbox) RegisterModule ¶
RegisterModule registers a JS module skill as a callable function in the sandbox. Used when a skill needs to be added after sandbox construction (e.g. mid-session); steady-state skill loading goes through SkillPack at construction time.
func (*Sandbox) RequireCacheGet ¶
RequireCacheGet returns a previously-cached require() result, or nil when none. Used by the require pack to avoid re-running a module body within a single session.
func (*Sandbox) RequireCacheSet ¶
RequireCacheSet caches a require() result for the rest of the session. The require pack calls this after a successful first load; subsequent require(name) calls return the cached value.
func (*Sandbox) Runtime ¶
Runtime exposes the underlying Goja runtime. Used by packs that need to call rt methods directly (the markdown / require packs are the primary callers).
func (*Sandbox) SetExecTimeout ¶
SetExecTimeout overrides the default 120s per-Execute wall-clock cap. Useful for tests (smaller) and for trusted operator scripts (larger). Zero disables the timeout entirely — fine for fakes.
func (*Sandbox) SetOnEvent ¶
SetOnEvent registers a callback that receives every sandbox event. Nil clears the callback. The loop uses this to relay events into its public RunEvent stream.
func (*Sandbox) SetPolicy ¶
func (s *Sandbox) SetPolicy(p PolicyChecker)
SetPolicy installs a PolicyChecker on the sandbox. Nil clears it (fails open).
func (*Sandbox) SkillCode ¶
SkillCode returns the JS body registered under name, or "" when no such skill is registered. Used by the require pack.
func (*Sandbox) SystemPrompt ¶
SystemPrompt builds the primitives documentation from all registered packs. Appended to the agent loop's base instructions so the LLM knows what's available. Pack prompts use TypeScript .d.ts format wrapped in a single code fence; the "context" pack (dynamic session data) is appended as prose after the fence.
func (*Sandbox) TakeAnswer ¶
TakeAnswer reports the value passed to answer() during the most recent Execute and whether answer() was called. The loop uses it as a terminal signal: a called answer() ends the run with this value as the final response. State is reset at the start of each Execute.
type SkillInfo ¶
type SkillInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
}
SkillInfo describes one entry returned by the skillList() primitive. Type is one of "pack" | "module" | "prompt" | "webhook" depending on what backs the skill — packs are agentloop's built-ins, module/prompt/webhook come from application-supplied skill rows.
type URLAllowlistPolicy ¶
type URLAllowlistPolicy struct {
AllowedPrefixes []string
}
URLAllowlistPolicy is a stateless, in-memory checker that allows fetch() / require('http') to call exactly the URL prefixes in AllowedPrefixes. Other tool names always allow (the canonical use is to compose it under MultiPolicy alongside a stricter product-side checker).
The decision is encoded so the fetch pack can read the allowlist from ConfigJSON instead of re-querying somewhere. Field name matches what the existing fetch pack expects: `whitelist`.
func (URLAllowlistPolicy) Check ¶
func (p URLAllowlistPolicy) Check(_ context.Context, toolName string, args map[string]any) (PolicyCheckResult, error)
Check implements PolicyChecker. fetch and the http module honour the allowlist; everything else passes through.