Documentation
¶
Overview ¶
Package ext holds optional sandbox.Pack implementations that are generically useful but don't belong in the core sandbox package.
sandbox's built-in packs (require, http, fetch, markdown, ai, help, skills) are the minimal set every agentloop deployment needs. The packs here — email, secret, document search, a document store — are common but optional: most of the applications this was extracted from use two or three of them, never all four, and a new deployment starts with none.
Each pack follows the same decoupling the core packs use: it takes a callback or a small interface (EmailPack's send func, SecretPack's get func, SearchPack's search func, StoresPack's StoresBackend) rather than a concrete dependency, so this package stays free of any mail/secrets/search-backend import. An application wires its own backend in when it builds the agentloop.Capability that installs the pack — see the package examples and DefaultCapabilities in the agentloop package for the pattern.
Index ¶
- func EmailPack(ctx context.Context, send func(to, subject, body string) error) sandbox.Pack
- func ExecPack(ctx context.Context, root *os.Root, opts ExecOptions) sandbox.Pack
- func OpenAPIPack(spec *openapi3.T, cfg OpenAPIConfig) (sandbox.Pack, error)
- func SearchPack(search func(query string, topK int) ([]SearchHit, error)) sandbox.Pack
- func SecretPack(ctx context.Context, get func(name string) (string, error)) sandbox.Pack
- func StoresPack(backend StoresBackend) sandbox.Pack
- func WorkspacePack(ctx context.Context, root *os.Root, opts WorkspaceOptions) sandbox.Pack
- type ExecOptions
- type OpenAPIConfig
- type SearchHit
- type StoreDoc
- type StoresBackend
- type WorkspaceOptions
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EmailPack ¶
EmailPack exposes a synchronous `sendEmail({to, subject, body})` primitive. It takes a send callback rather than a concrete mailer so this package stays transport-agnostic — the application's Capability closes the callback over its configured sender + from address.
Every call is policy-gated with toolName "sendEmail"; sandbox.DefaultPolicy denies it unless granted via DefaultPolicy.AllowTools. ctx is the per-session context the check shares with the run.
The argument is an object so the call site is self-documenting and resilient to added optional fields. Returns true on success; a send error surfaces as a catchable JS throw.
func ExecPack ¶ added in v0.4.0
ExecPack exposes exec(): run a command and get back its output and exit status.
root gives cwd a default and a meaning — a relative cwd is resolved inside it. That is an ergonomic boundary, NOT a security one, and the distinction matters: a command runs with the full privileges of the user running the agent, and nothing here stops it reading or writing anywhere that user can. The confinement WorkspacePack gets from os.Root has no equivalent once another program is executing. What bounds this primitive is the policy check on every call, the approval prompt behind it, and the caller's judgement about which commands to allow.
func OpenAPIPack ¶
OpenAPIPack turns an OpenAPI 3 document into a require()-able skill: one JS function per operation (path + method), named after the operation's operationId (sanitized) or derived from the method and path when it has none.
Each generated function calls the sandbox's own internal _http primitive — the same one require('http') wraps (see pack_fetch.go in the sandbox package) — rather than a new HTTP client. That's the whole design: OpenAPIPack only generates JavaScript source (a string), so every call a generated function makes is still policy-gated exactly like fetch()/require('http') already are, with no new Go-side networking code to audit. _http is used directly rather than require('http')'s get/post/put/del because it uniformly supports every method via its `method` option, including PATCH, which the http module has no wrapper for.
Following the skill-lazy-loading pattern the rest of agentloop's skill mechanism uses (see sandbox.SkillDiscoveryPack's doc comment), the returned Pack's Prompt is a single short line — the API's name, operation count, and a pointer to skillGet(name)/require(name) — not the full API surface; a spec can have far more operations than are worth inlining into every turn. The full per-operation documentation is TypeScript ambient declarations (`declare function ...`), in the same style the core packs' Prompt fields already use, registered as a HelpEntry under Name so skillGet(name) returns it on demand.
Parameters are passed as a single options object rather than positionally — `functionName(params)`, with params.<name> for each path/query parameter and params.body for the request body — since OpenAPI operations vary too much in parameter count and optionality for a positional signature to stay readable. Required-ness is documented but not enforced client-side; the server is still free to reject a missing required parameter.
Returns an error if the spec resolves to zero generated operations (no BaseURL and no servers, no paths, or every operation filtered out by Methods) — a skill with nothing callable is a config mistake, not something to silently install.
func SearchPack ¶
SearchPack exposes a synchronous `documentSearch(query, topK?)` primitive backed by a semantic-search / RAG retriever. It takes a search callback rather than a concrete retriever so this package stays free of that dependency — the application's Capability closes the callback over its own retriever and the per-session scope.
topK defaults to 5 when omitted or <= 0.
func SecretPack ¶
SecretPack exposes a synchronous `secret(name)` primitive that reads a decrypted secret by name. It takes a getter callback rather than a concrete secrets service so this package stays free of that dependency — the application's Capability closes the getter over its own secrets backend and the per-session scope.
Every read is policy-gated with toolName "secret"; sandbox.DefaultPolicy denies it unless granted via DefaultPolicy.AllowTools. ctx is the per-session context the check shares with the run.
Lookup failures (unknown name, decrypt error) surface as a thrown JS error so a skill can try/catch; they do not crash the run.
func StoresPack ¶
func StoresPack(backend StoresBackend) sandbox.Pack
StoresPack exposes a `stores` object with `list()` and `read(id)` for inspecting the indexed document corpus from inside the sandbox.
func WorkspacePack ¶ added in v0.4.0
WorkspacePack exposes a project directory to the agent: readFile, writeFile, editFile, listDir, glob, and grep.
Every path is resolved by root, an *os.Root, so ".." and symlinks cannot escape the directory the caller opened — the confinement is enforced by the runtime rather than by path arithmetic here, which is where this kind of thing is usually got wrong.
Reads are ungated. Mutations are policy-gated as "writeFile" and "editFile"; when the policy denies one and an Approver is configured, the user is asked to allow it, and both primitives ask the same question about a given path so approving an edit does not then prompt again for a write to the same file.
Types ¶
type ExecOptions ¶ added in v0.4.0
type ExecOptions struct {
// Timeout is the default wall-clock cap on one command. Zero uses
// 30s. A script may ask for less, and for more up to MaxTimeout.
Timeout time.Duration
// MaxTimeout is the ceiling a script cannot raise its own timeout
// past. Zero uses 2 minutes.
//
// This bound is load-bearing in a way the sandbox's own execution
// timeout is not: that one works by interrupting the JS runtime,
// and a runtime blocked inside a Go call is not running JS to
// interrupt. While a command runs, this is the only thing bounding
// the turn.
MaxTimeout time.Duration
// MaxOutputBytes caps how much of each of stdout and stderr is kept.
// Zero uses 64 KiB. Output past the cap is still READ and discarded
// — a command whose pipe fills up blocks forever, so the choice is
// between draining it and hanging.
MaxOutputBytes int
// PassEnv names environment variables the child inherits. Nil uses
// baseEnvPassthrough.
//
// An allowlist, never a denylist: the agent's own credentials are in
// this process's environment, and a list of things to strip is a
// list someone has to remember to update. Anything not named here
// simply is not there.
PassEnv []string
// Env is extra variables to set, applied after PassEnv.
Env map[string]string
// AllowShell permits the string form of exec(), which runs through
// `sh -c`. Off by default: a shell turns a model-authored string
// into an injection surface, and argv covers most of what an agent
// actually needs.
AllowShell bool
// Approver is asked to allow a command the policy denied. Nil means
// no human is available and a denied command simply fails.
Approver sandbox.InputRequester
}
ExecOptions tunes ExecPack. The zero value is usable.
type OpenAPIConfig ¶
type OpenAPIConfig struct {
// Name is the skill's name — the string passed to require(name) and
// shown in skillList(). Defaults to a slug of spec.Info.Title, or
// "api" if the spec has no title.
Name string
// Description is the skillList() one-liner. Defaults to
// spec.Info.Title, or a generic fallback if the spec has none.
Description string
// BaseURL overrides the spec's first `servers` entry. Required if
// the spec declares no servers.
BaseURL string
// Headers are applied to every request this skill's functions
// make — the place to put a static API key or bearer token.
// OpenAPI's securitySchemes are not interpreted; this is the whole
// auth story OpenAPIPack has.
Headers map[string]string
// Methods restricts which HTTP methods become functions. Nil
// generates GET, POST, PUT, PATCH, DELETE (the common REST verbs);
// HEAD/OPTIONS/TRACE/CONNECT are never generated regardless.
Methods []string
}
OpenAPIConfig configures OpenAPIPack.
type SearchHit ¶
type SearchHit struct {
DocumentID string `json:"documentId"`
DocumentTitle string `json:"documentTitle"`
ChunkIndex int `json:"chunkIndex"`
Content string `json:"content"`
Score float64 `json:"score"`
}
SearchHit is one semantic-search result surfaced to the sandbox. The JSON tags drive the JS-side key names (the pack converts hits to plain maps so the runtime sees camelCase keys).
type StoreDoc ¶
type StoreDoc struct {
ID string `json:"id"`
Title string `json:"title"`
ChunkCount int64 `json:"chunkCount"`
}
StoreDoc is one indexed document surfaced to the sandbox by the stores primitive.
type StoresBackend ¶
type StoresBackend interface {
// List returns every indexed document in the current scope.
List() ([]StoreDoc, error)
// Read returns the full text of one document (its chunks joined in
// source order). An unknown id yields an empty string, no error.
Read(documentID string) (string, error)
}
StoresBackend is the read surface the stores primitive needs. An application adapts its own document catalog to it (scoped to the session); this keeps this package free of that dependency.
type WorkspaceOptions ¶ added in v0.4.0
type WorkspaceOptions struct {
// MaxFileBytes caps a single readFile and skips larger files during
// grep. Zero uses 1 MiB. The cap exists because the loop threads a
// script's return value forward in full — an accidental read of a
// huge file is a context-window problem, not just a slow one.
MaxFileBytes int64
// MaxResults caps how many entries glob, grep, and listDir return.
// Zero uses 500.
MaxResults int
// MaxWalkFiles bounds how many entries a single glob or grep visits
// before giving up, so a pattern pointed at an enormous tree fails
// loudly instead of hanging the run. Zero uses 50,000.
MaxWalkFiles int
// SkipDirs are directory names never descended into. Nil uses
// {".git"} — not source, frequently enormous, and its config can
// hold credentials. Set explicitly to add vendor trees.
SkipDirs []string
// Approver is asked to allow a mutation the policy denied. Nil means
// no human is available, and a denied write simply fails — the same
// contract the fetch pack's InputRequester has.
Approver sandbox.InputRequester
}
WorkspaceOptions tunes WorkspacePack. The zero value is usable: every field falls back to a documented default.