Documentation
¶
Overview ¶
Package agent runs one headless coding-agent session. Callers supply execution profiles, environment, MCP servers and outcome policy.
Index ¶
- Constants
- Variables
- func CheckDaggerEngine(engine string) error
- func CheckDaggerVersion(version string) error
- func CountTurns(path string) int
- func MCPEntries(servers map[string]MCPEntry) map[string]MCPEntry
- func MarkInterrupted(sessionDir, note string) error
- func RateLimitedText(msg string) bool
- func ReadWork(dir string) (work.Ref, error)
- func ValidateOutcome(role, status string, valid []string) error
- func WriteMCPConfig(path string, entries map[string]MCPEntry) error
- func WriteOutcome(dir string, o Outcome) error
- func WriteWork(dir string, ref work.Ref) error
- type Access
- type Backend
- type Bind
- type Boundary
- type Confinement
- type Confiner
- type ContainerBoundary
- type Dagger
- type Enforcer
- type Grants
- type HostBoundary
- type HostMCP
- type HostServer
- type Interrupted
- type MCPEntry
- type Mount
- type Outcome
- type Placement
- type Policy
- type Profile
- type RateLimit
- type Request
- type RestrictedCapabilities
- type RestrictedResult
- type Result
- type Runner
- type SandboxBoundary
- type Session
- type SkillPreparer
- type ToolSupport
- type Turn
Constants ¶
const ( // EnvOpenCodeConfigContent is opencode's highest-precedence inline // configuration. A held turn (opencodeHold) owns it even when the // caller inherited or supplied another value. EnvOpenCodeConfigContent = "OPENCODE_CONFIG_CONTENT" // EnvOpenCodePermission is applied after every file-backed permission // source. The held agent repeats the same permissions because its // rules take precedence over the global ones. EnvOpenCodePermission = "OPENCODE_PERMISSION" )
const ( AgentClaude = "claude" AgentCodex = "codex" AgentOpenCode = "opencode" AgentPi = "pi" SandboxNone = "none" SandboxClaude = "claude" SandboxContainer = "container" // SandboxSbx runs the session inside a Docker Sandbox, a microVM the // sbx CLI manages (https://docs.docker.com/ai/sandboxes/). SandboxSbx = "sbx" ContainerEngine = "docker" // SandboxCLI is the command SandboxSbx sessions are run with. SandboxCLI = "sbx" DefaultTimeout = 45 * time.Minute )
const ( // DefaultRestrictedTimeout bounds a restricted turn whose profile does // not choose one. DefaultRestrictedTimeout = 15 * time.Minute // DefaultRestrictedMaxTurns bounds a restricted turn whose profile does // not choose one. DefaultRestrictedMaxTurns = 40 )
const DaggerInstallScript = "https://dl.dagger.io/dagger/install.sh"
DaggerInstallScript is the script that installs the Dagger CLI in the sandbox. The sandbox's network policy must allow its host.
const EnvDaggerRunnerHost = "_EXPERIMENTAL_DAGGER_RUNNER_HOST"
EnvDaggerRunnerHost is the variable the Dagger CLI reads the address of an engine it does not start itself from.
const EnvOpenCodeConfig = "OPENCODE_CONFIG"
EnvOpenCodeConfig is the variable opencode reads a configuration file's path from, set for every opencode session to the file opencodeConfig wrote.
const EnvPiMCPConfigMode = "PI_MCP_CONFIG_MODE"
EnvPiMCPConfigMode is the variable the adapter reads to decide whether the --mcp-config file is the only configuration it reads; ordinary pi sessions set it to "exclusive". Restricted sessions load no adapter.
const InterruptedFile = "interrupted"
InterruptedFile marks a session directory whose session was stopped on purpose — orphan cleanup, the live view's kill key and the scheduler's hard stop write it — so the next session can be told it was stopped rather than left to guess that the machine crashed. It holds one line of prose, which is what MarkInterrupted was given.
const OpenCodeConfigFile = "opencode.json"
OpenCodeConfigFile is the name of that file in the session directory.
const OutcomeFile = "outcome.json"
OutcomeFile is the file a session's outcome is written to inside the session directory (by the caller outcome reporter or the `done` MCP tool).
const PiMCPAdapter = "npm:pi-mcp-adapter"
PiMCPAdapter is the pi package that gives ordinary pi sessions MCP support, loaded ahead of Profile.PiPackages. Restricted sessions load neither.
const PiMCPConfigFile = "pi-mcp.json"
PiMCPConfigFile is the adapter's configuration file in the session directory, passed as --mcp-config.
const ( // ReadServerName is the MCP server name a restricted turn reaches the // read server by. An inherited server of the same name is replaced by // it rather than disabled. ReadServerName = "restricted_read" )
The read server gives a restricted turn whose backend has no read-only file tool of its own (RestrictedCapabilities.ReadServer: Codex, whose only way to read a file is its shell, which RunRestricted switches off) a way to read the workspace and nothing else. It is an MCP server served from the runner's own process for the length of one turn, on a fresh loopback port behind a fresh bearer token, with three tools: read_file, list_directory and search_files. Every path they take is opened through an os.Root of the workspace, so neither `..` nor a symbolic link reaches a file outside it, and none of them writes, runs or fetches anything.
const ResultFile = "result.json"
ResultFile is the file Run writes when a session ends, whatever it ended with. Its absence is what says a session never finished.
const SubtypeCostCap = "cost_cap"
SubtypeCostCap is the ErrorSubtype of a session whose known cost reached Request.CostCapUSD, whether the runner stopped it in flight or it ended on its own with a final cost at or over the cap.
const ToolsAll = "*"
ToolsAll grants every built-in tool of the agent. MCP servers are never covered by it: each one is granted as "mcp__<server>".
const TranscriptFile = "transcript.jsonl"
TranscriptFile is the session's stream-json output, written line by line while the session runs. An interrupted session leaves an unfinished one.
const WorkFile = "work.json"
WorkFile records the caller's work reference independently of session names.
Variables ¶
var AgentCredentials = credentialsByAgent()
AgentCredentials names credentials forwarded into an isolated container, per agent: the backends' Credentials indexed by name. opencode and pi are authenticated through whichever provider they are configured for, so their entries list one key per provider they read: any one of them is a credential, and every one of them that is set is forwarded.
var Agents = procs.AgentExecutables
Agents lists the backends the runner builds a command line for. It is procs' list of agent executables: procs recognizes a running session in the process table by the command it runs and cannot import this package, so the two are one list and an agent added to it is known to both. The backend descriptors (Backends) describe the same agents and are held to this list by test rather than derived from it.
var Backends = []Backend{ { Name: AgentClaude, Credentials: []string{"ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"}, ProviderEnv: []string{ "ANTHROPIC_*", "CLAUDE_*", "AWS_*", "GOOGLE_*", "CLOUD_ML_REGION", "VERTEX_*", "DISABLE_*", "MAX_THINKING_TOKENS", "MCP_*", }, ArgvMarker: true, Restricted: &RestrictedCapabilities{Supported: true, FollowUp: true}, WritableTools: supportEverywhere(), // contains filtered or unexported fields }, { Name: AgentCodex, Credentials: []string{"OPENAI_API_KEY", "CODEX_API_KEY"}, ProviderEnv: []string{"OPENAI_*", "CODEX_*"}, ArgvMarker: true, Restricted: &RestrictedCapabilities{Supported: true, FollowUp: true, ReadServer: true}, WritableTools: codexWritableTools(), // contains filtered or unexported fields }, { Name: AgentOpenCode, Credentials: []string{ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "DEEPSEEK_API_KEY", }, ProviderEnv: []string{ "OPENCODE_*", "ANTHROPIC_*", "OPENAI_*", "GEMINI_*", "GOOGLE_*", "AWS_*", "OPENROUTER_*", "GROQ_*", "MISTRAL_*", "XAI_*", "DEEPSEEK_*", "AZURE_*", }, Restricted: &RestrictedCapabilities{Supported: true, FollowUp: true}, WritableTools: openCodeWritableTools(), // contains filtered or unexported fields }, { Name: AgentPi, Credentials: []string{ "ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "XAI_API_KEY", "MISTRAL_API_KEY", "DEEPSEEK_API_KEY", "CEREBRAS_API_KEY", }, ProviderEnv: []string{ "PI_*", "ANTHROPIC_*", "OPENAI_*", "GEMINI_*", "GOOGLE_*", "AWS_*", "OPENROUTER_*", "GROQ_*", "MISTRAL_*", "XAI_*", "DEEPSEEK_*", "AZURE_*", "CEREBRAS_*", "MCP_*", }, Restricted: &RestrictedCapabilities{Supported: true, FollowUp: true}, WritableTools: supportNowhere(fmt.Sprintf("grant %q", ToolsAll)), // contains filtered or unexported fields }, }
Backends are the backends the runner builds a command line for, one descriptor each, in the order Agents lists them. Adding a backend here is what gives it credentials, provider environment and executable selection everywhere they are read; procs.AgentExecutables still has to name its executable, and doctor still has to carry its check, either of which a test reports missing.
var ErrNoGrants = errors.New("request has no grants")
ErrNoGrants is returned for a request that carries no grants.
var ErrNotGranted = errors.New("not granted")
ErrNotGranted wraps a request that asks for more than its grants.
var ErrPolicyChanged = errors.New("turn differs from the session's policy")
ErrPolicyChanged wraps the refusal of a turn that is not the one the session's policy describes.
var ErrReleased = errors.New("session was released")
ErrReleased is returned by Run on a session that was released.
var ErrUnsupported = errors.New("grant cannot be enforced")
ErrUnsupported wraps a grant the selected boundary cannot enforce.
var Placements = []Placement{ {Sandbox: SandboxNone}, {Sandbox: SandboxNone, Confine: true}, {Sandbox: SandboxClaude}, {Sandbox: SandboxClaude, Confine: true}, {Sandbox: SandboxContainer}, {Sandbox: SandboxSbx}, }
Placements are every placement a valid profile selects.
var Sandboxes = []string{SandboxNone, SandboxClaude, SandboxContainer, SandboxSbx}
Sandboxes lists the sandbox kinds the runner implements, weakest first.
var SbxTemplates = map[string]string{ AgentClaude: "docker/sandbox-templates:claude-code", AgentCodex: "docker/sandbox-templates:codex", AgentOpenCode: "docker/sandbox-templates:opencode", }
SbxTemplates are the sbx CLI's template images, per agent: what a template of the operator's is built on (sbx's own default for `sbx create <agent>` is the image's "-docker" variant). An agent without one cannot run in SandboxSbx.
var VCSExecutables = []string{"gh", "git", "hg", "jj", "svn"}
VCSExecutables are the executables denied to a session without VCS.
Functions ¶
func CheckDaggerEngine ¶
CheckDaggerEngine reports whether an engine address is one a sandbox can be given: a socket by its absolute path, a TCP address with a port, or a container by a name Docker accepts.
func CheckDaggerVersion ¶
CheckDaggerVersion reports whether a Dagger CLI release is one the install script takes.
func CountTurns ¶
CountTurns counts the turns of a transcript that no final event has closed: claude's assistant messages, codex's "item/completed" notifications, opencode's finished steps or pi's ended turns, whichever the transcript carries. A session takes its turn count from the end of its stream, and three kinds of session never reached one: an interrupted one, a running one the live view is watching, and one whose agent process died from a signal. The turns are counted instead: close enough to say how far the session had got, and not the same number the agent would have reported. A missing or unreadable file is 0.
func MCPEntries ¶
MCPEntries converts configured servers into file entries, expanding $VAR references in their environment and headers. Call this once before handing prepared entries to the runner.
func MarkInterrupted ¶
MarkInterrupted records in a session directory that the session was stopped on purpose. A directory that is already gone is not an error: marking is bookkeeping for the next session, never a reason to fail a kill.
func RateLimitedText ¶
RateLimitedText recognizes capacity and transient service-limit messages. It lives with agent execution so restricted fallback chains and callers in ops make the same decision.
func ValidateOutcome ¶
ValidateOutcome checks a caller-supplied status set. Nil accepts any status.
func WriteMCPConfig ¶
WriteMCPConfig writes the file claude is given as --mcp-config.
func WriteOutcome ¶
WriteOutcome stores an outcome in dir without validating it.
Types ¶
type Backend ¶
type Backend struct {
// Name is the backend's identity everywhere: the value of the agent
// setting that selects it, and the basename of the executable a
// session of it runs. The two are one fact because Agents is procs'
// list of executable basenames and an agent setting is one of its
// entries.
Name string
// Credentials are the variables the CLI reads its provider
// credential from. opencode and pi are authenticated through
// whichever provider they are configured for, so their lists hold
// one key per provider they read: any one of them is a credential,
// and every one of them that is set is forwarded.
Credentials []string
// ProviderEnv are the variables the CLI is configured and
// authenticated through, beyond the host environment every session
// inherits: its providers' credentials and its own settings. A
// trailing "*" is a prefix.
ProviderEnv []string
// ArgvMarker says whether the command line the backend's
// implementation builds carries a marker procs finds the session by
// in the process table. A backend without one is found through its
// pid file alone; procs' package comment says why.
ArgvMarker bool
// Restricted declares whether the backend can establish
// RunRestricted's read-only floor, and whether a restricted turn can
// continue a session it owns. The pointer is deliberate: nil means a
// descriptor forgot to make the declaration, distinct from an explicit
// unsupported declaration.
Restricted *RestrictedCapabilities
// WritableTools declares, for every placement (Placements), whether an
// ordinary turn, one that may write, can be held to the built-in tools
// its grants name when they do not grant ToolsAll. A placement the map
// does not name is unsupported: the turn is refused with
// ErrUnsupported before anything starts, and so is one declared
// unsupported, whose Remedy the error carries. A test holds every
// descriptor to a declaration for every placement.
WritableTools map[Placement]ToolSupport
// contains filtered or unexported fields
}
A Backend is one agent CLI a session can run as, described once: the per-agent facts every consumer reads — the executable a session runs, the variables that hold its credential, the environment that configures it, whether the command line carries a marker the process-table scan finds the session by — together with the implementation that builds that command line and reads what the CLI printed. The lists these facts appear in derive from the descriptors: AgentCredentials below, the provider environment a caller derives from them, the executable every backend defaults to, and the backend backendFor selects. procs' executable list is the one that cannot derive, because procs cannot import this package: it stays the independent source, and a test holds the two together.
type Boundary ¶
Boundary is where a session's process runs and what enforces its grants. Verify inspects a request without starting anything and returns the turn the boundary would run, or an error when the request asks for more than its grants or for a grant the boundary cannot enforce.
type Confinement ¶
type Confinement struct {
// Sandbox is the profile's host sandbox, SandboxNone or SandboxClaude.
Sandbox string
// Mounts are the granted mounts. Where one lies inside another the
// inner one decides, so a read-only mount inside a read-write one stays
// read-only.
Mounts []Mount
// System are the system paths and, once the runner has resolved it, the
// agent's executable. They add to what the mounts allow.
System []Mount
// Denied are the files and directories that stay unreadable and
// unexecutable inside Mounts and System: the VCS executables of a turn
// without VCS. A denied file is denied at its own path, whatever shell
// or symbolic link leads there, and under a hard link in a directory
// that holds a denied path. A hard link to it anywhere else in Mounts or
// System is not found.
Denied []string
}
Confinement is what the operating system enforces for a confined host turn. Every path has its symbolic links resolved. Nothing outside Mounts and System can be read, written or executed.
type Confiner ¶
type Confiner interface {
// Check reports whether this platform can enforce c, without starting
// anything. Its error wraps ErrUnsupported.
Check(c Confinement) error
// Start starts cmd under c. It never starts cmd with less than c.
Start(cmd *exec.Cmd, c Confinement) error
}
Confiner enforces a Confinement with the operating system.
type ContainerBoundary ¶
type ContainerBoundary struct {
// Environ is the host environment agent credentials are read from; nil
// reads os.Environ.
Environ func() []string
// Home is the container's private HOME.
Home string
// SessionsDir is where a session directory is created for a request
// that names none.
SessionsDir string
// MountDirs must be granted read-write.
MountDirs []string
// SkillMountDirs must be granted when the profile has skills.
SkillMountDirs []string
// Masks are read-only binds the runner owns, laid over paths of the
// image for a turn without VCS: a stand-in over each VCS executable the
// image has (see NewContainer). Their sources need no grant: they show
// the container nothing of the host but the stand-ins themselves.
Masks []Bind
}
ContainerBoundary runs a session in a container. The container is given the granted mounts, with their access, and nothing else of the host; an environment built from the allowlist alone; and, without VCS, stand-ins for the VCS executables in front of the image's PATH, which deny them by name, and Masks over the paths the caller found them at. Verify refuses a request whose own paths the grants do not cover.
func (ContainerBoundary) Verify ¶
func (b ContainerBoundary) Verify(req Request) (*Turn, error)
Verify checks the request and builds the container turn. It fails closed: a path the container needs that no grant covers, a mount the engine cannot be given exactly, or a variable outside the allowlist is refused.
type Dagger ¶
type Dagger struct {
// Engine is where the engine listens on the host: "unix://<path>" for
// its socket, "tcp://<host>:<port>", or "docker-container://<name>"
// for an engine in a container of the host's container engine
// (Runner.DockerBin) that publishes neither.
Engine string
// Version is the Dagger CLI release installed in the sandbox, the
// engine's own: "v0.20.5" or "0.20.5".
Version string
}
Dagger is the Dagger engine a SandboxSbx session runs the Dagger CLI against, and the CLI release installed in the sandbox for it.
type Enforcer ¶
type Enforcer interface {
// Prepare checks the grants on their own and asks the platform whether
// it can enforce them, without starting an agent. A kind this platform
// cannot enforce is refused with an error that wraps ErrUnsupported,
// never prepared with less.
Prepare(ctx context.Context, grants Grants) (Session, error)
}
Enforcer prepares turns of one sandbox kind that are held to their grants by something other than the agent: the operating system for a host kind (the confined HostBoundary), the engine's binds for a container, the virtual machine's workspaces for a Docker Sandbox. There is one constructor per kind: NewHostNone, NewHostClaude, NewContainer and NewSbx.
session, err := enforcer.Prepare(ctx, grants) // inspect session.Policy(), and refuse to go on when it is not what was meant result, err := session.Run(ctx, request) err = session.Release(ctx)
func NewContainer ¶
NewContainer returns the enforcer of turns in a container of image. The container is given the granted mounts and nothing else of the host. For grants without VCS, Prepare runs the image once, with no network and no mounts, to find its VCS executables: the files the names in VCSExecutables resolve to in the image's PATH and the usual system directories, and git's directory of subcommand programs. Each is bound over, read-only, by a stand-in that exits 126, so it is denied by absolute path and from a shell too. A VCS executable anywhere else in the image, a hard link to one, or one the turn fetches is not found. The stand-ins are files of the runner's under the temporary directory, which the engine must be able to bind; Release removes them.
func NewHostClaude ¶
NewHostClaude returns the enforcer of host turns under Claude Code's sandbox, confined the same way. Seatbelt holds it on macOS. Landlock cannot hold Claude's box, which is built with mount(2), so on Linux Prepare refuses unless r.Confiner is one that can.
func NewHostNone ¶
NewHostNone returns the enforcer of host turns with no sandbox of the agent's own. The turn is a confined one (Profile.Confine): r.Confiner, or the platform's, holds it to its mounts and r.SystemPaths.
func NewSbx ¶
NewSbx returns the enforcer of turns of agent in a Docker Sandbox (SandboxSbx), each created from template, or from sbx's own template for the agent when template is empty (SbxTemplates). An empty agent is AgentClaude. A turn of another agent, or one whose profile names another template, is refused. Every turn gets a sandbox of its own, given the granted mounts as its workspaces and nothing else of the host, a network policy rule for each host port it is granted and reaches, and removed when the turn ends.
Prepare checks the grants and that the sbx CLI answers (`sbx version`). Without VCS, the VCS executables are shadowed on the turn's PATH and denied by name alone: sbx binds directories and not files, so nothing is laid over an executable of the template, one reached by its path runs, and the sandbox's user can undo anything done inside the virtual machine with sudo. What keeps VCS from such a turn is what it is not given: VCS credentials and configuration in its environment and writable VCS metadata. Credentials the operator stores with `sbx secret set` are the sandbox proxy's, which injects them into the requests it forwards; grants cannot withhold them.
type Grants ¶
type Grants struct {
// Env is the complete allowlist of environment variable names. A name
// ending in "*" grants every name with that prefix. Host variables are
// inherited only when listed; a variable the profile or request sets
// that is not listed is refused.
Env []string
// Tools are the tools the agent may use: built-in tool names (or
// ToolsAll) and "mcp__<server>" for each MCP server. The profile's
// allowed tools and MCP servers may narrow them, never widen them.
Tools []string
// Mounts are the paths the session may read or write. The working
// directory must lie inside one of them.
Mounts []Mount
// Within, when set, confines every mount: a mount that resolves outside
// it, through ".." or a symbolic link, is refused.
Within string
// VCS grants version control: VCS credentials and configuration in the
// environment, VCS executables, and writable VCS metadata. Without it
// all three are denied.
VCS bool
// DaggerEngine grants the Dagger engine at this address (Dagger.Engine)
// to a session whose profile asks for it, and only in SandboxSbx. A
// profile that asks for another engine, or none, is refused.
DaggerEngine string
// HostServers grants a SandboxSbx session MCP servers the caller runs
// on the host's loopback itself (HostServer). Every other mode refuses
// them.
HostServers []HostServer
}
Grants are the complete capabilities of one session. Whatever they do not name, the session does not get: the runner verifies a request against them before any process starts and refuses what it cannot enforce.
type HostBoundary ¶
type HostBoundary struct {
// Environ is the host environment; nil reads os.Environ.
Environ func() []string
// StripPrefix removes host variables with this prefix even when granted.
StripPrefix string
// AddDirs are directories the agent is told it may write.
AddDirs []string
// Confiner enforces a confined turn; nil selects this platform's, which
// is Landlock on Linux, Seatbelt on macOS and none anywhere else.
Confiner Confiner
// SystemPaths are what a confined turn reaches beyond its mounts; nil
// selects DefaultSystemPaths.
SystemPaths []Mount
// SessionsDir is where a session directory is created for a request
// that names none. A confined turn must be granted it.
SessionsDir string
// SkillDirs hold the prepared skills; a confined turn with skills must
// be granted them.
SkillDirs []string
}
HostBoundary runs a session as a process of this host. By default the host enforces nothing of the mounts, so Verify asks for the grants that say so: "/" read-write and VCS without a sandbox, "/" read-only and a writable working directory under SandboxClaude. RunRestricted uses its private verification mode for a read-only root held by the backend's fixed tool and sandbox configuration. A profile with Confine set is held to its grants by the operating system instead: it reads the granted mounts and SystemPaths and nothing else, its working directory may be read-only, and without VCS the VCS executables cannot be read or run under any path. Neither host sandbox needs "/" for it, and where nothing can enforce it the request is refused with ErrUnsupported.
type HostMCP ¶
type HostMCP struct {
Name string
Entry MCPEntry
// Env overrides the session environment for the host server (secrets stay in memory).
Env map[string]string
ListenArgs []string
TokenEnv string
ListeningPrefix string
Path string
}
HostMCP describes a caller-owned server that can serve HTTP from the host. The runner appends ListenArgs and the listen address to Entry.Args, puts a fresh bearer token in TokenEnv, and reads ListeningPrefix + address on stdout. Entry is also supplied in Profile.MCP for ordinary host sessions.
type HostServer ¶
type HostServer struct {
// Name is the server's name, the key of its Profile.MCP entry.
Name string
// Port is the port it listens on, on the host's loopback.
Port int
}
HostServer grants a SandboxSbx session one MCP server the caller runs on the host's loopback: the sandbox, and no other, is allowed to reach Port while it exists (`sbx policy allow network --sandbox <name> localhost:<port>`). The profile's MCP entry of that name must be an HTTP entry whose URL names the host and Port, or the request is refused. The server must also be granted as a tool ("mcp__<name>"). A grant widens nothing else: a granted server the profile has no entry for gets no rule, and an entry that is not granted is passed as it is and gets none either, so it reaches the host only if the machine's own policy lets every sandbox do so.
type Interrupted ¶
type Interrupted struct {
// Role is who ran the session, and Name the session directory's name.
Role string
Name string
Dir string
// Transcript is the path of the unfinished transcript, empty when the
// session was killed before it wrote one.
Transcript string
// Turns is how many assistant messages the transcript holds. It is an
// approximation of the turn count a finished session reports: that
// number comes from the event that ends a stream, which an interrupted
// session never emitted.
Turns int
// Killed is true when the session was stopped on purpose (orphan cleanup)
// rather than lost with the process that ran it, and Note is what the
// marker said.
Killed bool
Note string
}
Interrupted describes a session that started and never finished: no result file was written and no process is running for it any more.
func CheckInterrupted ¶
func CheckInterrupted(role, dir string, alive func(int) bool) (*Interrupted, bool)
CheckInterrupted reports what a session directory says about the session that ran in it. role names who ran it — the directory cannot say — and alive answers whether a pid is still running (nil means procs.Alive; it is a seam so tests need no real process). It returns:
- nil, true while the session is still running: its pid file names a live process. A session that has not written its result file yet is the ordinary state of a running session and must never be reported as interrupted;
- nil, false when the session finished — the result file exists — or when the directory is gone;
- an Interrupted, false when it started and never finished.
type MCPEntry ¶
type MCPEntry struct {
Type string `json:"type,omitempty"`
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
// EnvVars names variables Codex must inherit from its own environment.
// Claude, OpenCode and pi inherit it already, so this is not a file entry.
EnvVars []string `json:"-"`
// BearerTokenEnv names the process variable holding an HTTP bearer token.
// Backend writers render an environment reference, never the secret value.
BearerTokenEnv string `json:"-"`
}
MCPEntry is one MCP server as a session is given it: an entry of claude's --mcp-config file, the source of codex's mcp_servers overrides, or a server of opencode's or pi-mcp-adapter's configuration file.
type Mount ¶
Mount grants one path of the host, and everything below it, to a session.
func DefaultSystemPaths ¶
func DefaultSystemPaths() []Mount
DefaultSystemPaths are what a confined turn reaches beyond its mounts when the boundary names nothing else: the programs and libraries of the system, the configuration a program needs to resolve a name, verify a certificate and find its libraries, the process and CPU information a runtime reads, and the devices every program expects. No home directory, no temporary directory and nothing of /var or /run: a turn that needs one is granted it. A path this machine does not have is skipped.
type Outcome ¶
type Outcome struct {
Work work.Ref `json:"work"`
Status string `json:"status"`
Note string `json:"note,omitempty"`
}
Outcome is the structured result a session reports when it finishes.
func ReadOutcome ¶
ReadOutcome loads the outcome from dir. ok is false when none was written.
type Placement ¶
A Placement is where a turn runs: its sandbox (one of Sandboxes, never empty), and for a host sandbox whether the operating system confines it (Profile.Confine).
func PlacementOf ¶
PlacementOf is where a profile's turn runs.
type Policy ¶
type Policy struct {
// Sandbox is the kind: SandboxNone, SandboxClaude, SandboxContainer or
// SandboxSbx.
Sandbox string
// Env is the allowlist of variable names the turn's environment is
// built from.
Env []string
// Tools are the built-in tools the agent is started with; nil is all
// of them. MCPServers are the servers it may be given.
Tools []string
MCPServers []string
// Mounts are the granted paths. Where one lies inside another the inner
// one decides.
Mounts []Mount
// System is what a host turn reaches beyond its mounts: the system
// paths, and the executables of the agents the runner names, of which a
// turn runs one. Nil for a container or a sandbox, which reaches its
// image and nothing else of the host.
System []Mount
// VCS is whether version control is granted.
VCS bool
// DeniedExecutables are the names shadowed on the turn's PATH, and
// Denied the paths that can be neither read nor executed: on the host
// for a host turn, inside the image for a container. A sandbox has no
// denied paths.
DeniedExecutables []string
Denied []string
// Image is what a container turn runs, or the template a sandbox is
// created from (empty: sbx's own for Agent). Binds are everything a
// container or a sandbox is given of the host: the mounts, and a
// container's stand-ins over Denied. A turn adds to them only a granted
// mount again, under the path a symbolic link names a directory of the
// request by.
Image string
Binds []Bind
// Agent is the agent a sandbox is created for; empty for any other
// kind.
Agent string
// DaggerEngine is the Dagger engine a sandbox turn whose profile asks
// for it reaches, and HostServers the caller's servers on the host's
// loopback it may be allowed to reach.
DaggerEngine string
HostServers []HostServer
}
Policy is what a session enforces. Every path of the host has its symbolic links resolved, a bind's destination apart, which is also the path a mount was granted by. It carries names and paths and no values: the environment's values are the turn's.
func NewPolicy ¶
NewPolicy checks grants on their own, the way every Enforcer does before it asks its platform anything, and returns the policy they describe for a sandbox kind: no system paths and no denied paths, which a platform adds, for a container or a sandbox the binds of its mounts and no image, and for a sandbox no agent.
func (Policy) Allows ¶
Allows reports whether the turn may be given a tool: a built-in one the agent is started with, or a tool of an MCP server ("mcp__<server>" or "mcp__<server>__<tool>") the turn may have.
func (Policy) Reads ¶
Reads reports whether the turn can read the host's file or directory at path, and Writes whether it can change or create it. Both follow the contract every confiner and the engine's binds are held to: the innermost mount that covers the path decides, a host turn's system paths add to it, and nothing is reached under a denied path, or on the host through a hard link to a denied file in a directory that holds a denied path. A path that is not absolute, or whose links cannot be resolved, is neither read nor written.
A turn is never given more than they say. It can be given less: Landlock lets nothing be created, removed or renamed at the level of a directory that holds a denied path or a read-only mount inside a read-write one. They do not judge a file's metadata, which a host confiner leaves readable, or the entries of "/", which Seatbelt does.
func (Policy) Runs ¶
Runs reports whether the turn can execute the file at path, a path as the turn sees it. A host turn executes what it can read. A container turn sees its image, whose links the host knows nothing of, so its path is judged as it is written, cleaned and with no link followed on the host or in the image: it runs unless it lies under Denied, whether it is a path of the image or one a mount is bound at. A name the image links to a denied path ("/bin/git" where "/bin" is a link to "usr/bin") is therefore said to run, and the stand-in over the path it leads to still refuses it. A sandbox turn is judged the same way and has no denied paths: every absolute path runs, a VCS executable reached by its path included. A path that is not absolute does not run.
type Profile ¶
type Profile struct {
Name string
Agent string
Model string
// Fallback is the profile a caller runs the session on instead when
// this one has no capacity (ops.SelectProfile), agent included, and it
// may have a fallback of its own. The claude backend also passes a
// claude fallback's model as --fallback-model, so claude switches to it
// within the session; a fallback on another agent is the caller's to
// run. Nil is no fallback.
Fallback *Profile
Effort string
MaxTurns int
Timeout time.Duration
AllowedTools []string
DisallowedTools []string
// MCP contains prepared entries; MCPEntries expands configured references once.
MCP map[string]MCPEntry
// Sandbox is one of Sandboxes; empty is SandboxNone.
Sandbox string
// Confine has the operating system hold a host session (SandboxNone or
// SandboxClaude) to its granted mounts: see HostBoundary. A platform
// that cannot refuses the session with ErrUnsupported.
Confine bool
// SandboxImage is the image a SandboxContainer session runs in, and the
// template a SandboxSbx session is created from (empty selects the
// sbx CLI's own template for the agent, SbxTemplates).
SandboxImage string
// Dagger gives a SandboxSbx session the Dagger CLI and a host's Dagger
// engine to run it against; nil is neither. The session must also be
// granted the engine (Grants.DaggerEngine).
Dagger *Dagger
SandboxDomains []string
ContainerUseEnvironment string
// VCSAccess permits workspace VCS mounts and caller-supplied VCS environment.
// It is not a security boundary on an unsandboxed host that is not confined.
VCSAccess bool
Shell string
Env map[string]string
Skills []string
// PiPackages are pi package sources (npm:, git:, a URL or a local path)
// an ordinary pi session loads after PiMCPAdapter. RunRestricted loads
// neither packages nor the adapter.
PiPackages []string
}
Profile contains only agent execution settings. Workflow and identity policy belong to the caller. Skills are generic plugin sources, prepared by Skills.
type RateLimit ¶
type RateLimit struct {
// Status is rate_limit_info.status. Only "allowed" and
// "allowed_warning" are known to mean the session may keep going;
// anything else is treated as blocking, because the blocked value has
// never been observed here and must not be guessed at.
Status string `json:"status,omitempty"`
// Type is rate_limit_info.rateLimitType ("five_hour", "seven_day").
Type string `json:"type,omitempty"`
// ResetsAt is when the window rolls, from rate_limit_info.resetsAt.
// Zero when the event carried none.
ResetsAt time.Time `json:"resets_at,omitempty"`
}
RateLimit is what claude last said about the account's capacity: the rate_limit_info of the final "rate_limit_event" of a session's stream.
type Request ¶
type Request struct {
// Name identifies the session in logs, e.g. "builder-task-12-r1".
Name string
Profile Profile
// ValidOutcomes is nil to accept any status; an empty non-nil set accepts none.
ValidOutcomes []string
// HostMCP optionally starts a caller-owned stdio server on the host for
// container and sandbox sessions.
HostMCP *HostMCP
// ContainerEnv overrides Env only inside a container or a sandbox.
ContainerEnv map[string]string
// Workspace supplies the working directory and optional VCS resources.
Workspace vcs.Workspace
// VCSEnv and VCSContainerEnv are caller-owned identity, credentials and
// configuration, injected only when the profile allows VCS access.
VCSEnv map[string]string
VCSContainerEnv map[string]string
// VCSContainerPath are caller-owned directories put in front of the
// image's PATH inside a container or a sandbox, only when the profile
// allows VCS access: wrappers such as a gh that fetches its own
// credentials. Each must be inside a directory the box mounts. On the
// host the caller sets PATH in Env itself.
VCSContainerPath []string
// SystemPrompt is appended to claude's default system prompt.
SystemPrompt string
// Prompt is the task given to the session.
Prompt string
// Env are extra environment variables.
Env map[string]string
// SessionDir is the per-session directory. When empty one is created;
// callers that want to reference it in prompts create it first with
// Runner.NewSessionDir.
SessionDir string
// Grants are the session's complete capabilities. Run verifies the
// request against them before it starts anything and refuses a request
// without them.
Grants *Grants
// ResumeID, when set, is the agent's own id of an earlier session
// (Result.ClaudeID) whose conversation this one continues, so a later
// round of the same role starts with the previous round's context
// instead of relearning the codebase. Claude, opencode, pi and codex
// (through thread/resume) honour it. The caller owns the id's
// lifetime, and an id the agent no longer knows ends two ways: claude,
// opencode and codex fail the launch before saying anything — for
// codex, an error response to thread/resume, including JSON-RPC
// -32600 ("no rollout found for thread id <id>"), ends the session as
// an error with no fallback to thread/start — which the caller's
// retry runs fresh, while pi starts a new session under that id with
// no earlier context.
ResumeID string
// CostCapUSD, when above zero, is the most the session may cost: the
// runner stops it once the known cost its stream reports reaches this
// many dollars, and a session that ends with a known cost at or over
// it is reported as capped too (Result.CostCapped). Zero is no cap. A
// cost the backend does not report never reaches the cap: codex's app
// server records token counts in the transcript
// (thread/tokenUsage/updated) but never prices them, so this cap stays
// inert and no codex session is ever stopped by it.
CostCapUSD float64
}
Request describes one session to run.
func Admit ¶
Admit returns req as a session with policy p, prepared with g, runs it, or the reason it does not: the grants are the session's, and a request that carries others is refused; the profile's sandbox and image are the session's, a host session's image being none, and a profile that names another, or a container-use environment to build one from, is refused whatever the kind; a sandbox's agent is the session's, and a profile that names another is refused; a host turn is confined; granted VCS is given to the turn, and a profile that asks for VCS that was not granted is refused. The request is then checked against the grants the way every boundary checks it.
type RestrictedCapabilities ¶
type RestrictedCapabilities struct {
Supported bool
FollowUp bool
// ReadServer says the backend has no read-only file tool of its own
// once its command execution is switched off, so a restricted turn is
// given the runner's read server (readserver.go) to read the
// workspace with instead.
ReadServer bool
}
RestrictedCapabilities are the parts of restricted execution that differ by backend. Supported says the command builder can establish the complete floor. FollowUp says ResumeID is meaningful for that backend; a resume id is never handed to a different backend during fallback.
type RestrictedResult ¶
RestrictedResult is a successful restricted turn. Result is the ordinary runner result; Model is the selected profile's model, including the model of a fallback that answered.
type Result ¶
type Result struct {
Name string `json:"name"`
Role string `json:"role"`
SessionDir string `json:"session_dir"`
Transcript string `json:"transcript"`
StartedAt time.Time `json:"started_at"`
Duration time.Duration `json:"duration"`
ExitCode int `json:"exit_code"`
// Signal is the signal that terminated the agent, or 0 when it exited
// of its own accord. Go reports an ExitCode of -1 for a signalled
// process and the signal is the only part that says why, so both are
// recorded: the number here, its name in ErrorSubtype ("signal_killed").
Signal int `json:"signal,omitempty"`
// ClaudeID is the id the agent gave the session: claude's, opencode's
// or pi's session id, or codex's thread id. The JSON name stays
// claude_session_id for older readers of result.json. Agent is the
// backend that gave it (AgentClaude when the profile named none), the
// only one that can resume it.
ClaudeID string `json:"claude_session_id,omitempty"`
Agent string `json:"agent,omitempty"`
ResultText string `json:"result_text,omitempty"`
IsError bool `json:"is_error"`
ErrorSubtype string `json:"error_subtype,omitempty"`
NumTurns int `json:"num_turns"`
CostUSD float64 `json:"cost_usd"`
// CostKnown says whether the stream reported any cost at all: claude
// reports the cost in the result event of its stream alone, so a
// session killed before it emitted one has no cost at all rather than
// a cost of zero, and codex reports tokens but never a cost. Nothing
// derives one. Opencode and pi report each step's cost as it ends, so
// a session stopped part way has the cost of the steps it finished.
CostKnown bool `json:"cost_known"`
// CostCapped says the known cost reached Request.CostCapUSD: the
// runner stopped the session for it, or it ended with a final cost at
// or over the cap. ErrorSubtype is SubtypeCostCap, and CostUSD is the
// cost observed until then. A caller's cancellation is never this.
CostCapped bool `json:"cost_capped,omitempty"`
TimedOut bool `json:"timed_out"`
Outcome Outcome `json:"outcome"`
HasOutcome bool `json:"has_outcome"`
// RateLimit is the last rate-limit event of the session's stream, or
// nil when it carried none.
RateLimit *RateLimit `json:"rate_limit,omitempty"`
// Diagnostics are failures the agent reported that ResultText may not
// mention, one line each: an MCP server that did not start, or an MCP
// tool call that failed. Codex reports them; other backends leave it
// empty.
Diagnostics []string `json:"diagnostics,omitempty"`
}
Result is what a finished session produced.
func (*Result) SessionLimited ¶
SessionLimited answers the only question the scheduler asks of a finished session's capacity report: did it die on the account-wide claude limit, and when does that limit reset? It says yes when the last rate-limit event was blocking, or when a session that failed without reporting an outcome has a result text naming a session or usage limit. The reset time is the one the last event carried and is zero when there was none — the human-readable sentence is never scraped for it.
type Runner ¶
type Runner struct {
// ClaudeBin is the claude executable. Default "claude".
ClaudeBin string
// CodexBin is the codex executable, run for a role whose agent is
// codex. Default "codex".
CodexBin string
// OpenCodeBin is the opencode executable, run for a role whose agent
// is opencode. Default "opencode".
OpenCodeBin string
// PiBin is the pi executable, run for a role whose agent is pi.
// Default "pi".
PiBin string
// DockerBin is the container engine a container session is run with,
// and the one a SandboxSbx session's docker-container Dagger engine is
// reached through (Dagger.Engine). Default ContainerEngine.
DockerBin string
// SbxBin is the Docker Sandboxes CLI a SandboxSbx session is run with.
// Default SandboxCLI.
SbxBin string
// ContainerListen is the address the caller-supplied MCP server listens on
// for a container or sandbox session. Empty picks the address the box
// reaches the host by (see containerListen and sandboxListen).
ContainerListen string
SessionsDir string
// EnvironmentPrefix is removed from the inherited host environment.
EnvironmentPrefix string
// NamePrefix prefixes backend titles and container names.
NamePrefix string
// ContainerLabel identifies containers during orphan inspection.
ContainerLabel string
ContainerHome string
ContainerUseRepository string
// MountDirs are directories a container or sandbox session writes; each must be
// granted read-write.
MountDirs []string
// Skills prepares generic skill plugin directories.
Skills SkillPreparer
// SkillMountDirs hold the prepared skills; a container or sandbox session or a
// confined host session with skills must be granted them.
SkillMountDirs []string
// AddDirs are extra directories claude may write (the state dir). Each
// must be granted read-write.
// Codex, which runs without a sandbox, needs no such list, and neither
// does opencode, whose --auto approves writing outside the worktree,
// nor pi, which has no approvals to ask.
AddDirs []string
// Confiner enforces a confined host session (Profile.Confine); nil
// selects this platform's. SystemPaths are what such a session reaches
// beyond its mounts; nil selects DefaultSystemPaths.
Confiner Confiner
SystemPaths []Mount
// Stream, when set, receives every stream-json line (debug output).
Stream io.Writer
Logger *slog.Logger
// contains filtered or unexported fields
}
Runner executes sessions.
func (*Runner) NewSessionDir ¶
NewSessionDir creates a fresh per-session directory under SessionsDir.
func (*Runner) Run ¶
Run executes the session and returns its result. An error is returned only when the session could not be started, produced no usable result, or was stopped by its context being cancelled — that one also leaves no result file, so the session directory reads as an interrupted session; a session that ran but reported failure returns a Result with IsError set.
func (*Runner) RunRestricted ¶
RunRestricted runs a read-only agent turn through the same backend and process lifecycle as Run. It is intended for reviews and other analysis that needs no caller-owned tools: the turn receives no caller's or inherited MCP server, writable mount, VCS access, skills, hooks or local project configuration. A backend whose descriptor declares RestrictedCapabilities.ReadServer is given the runner's own read-only server over its workspace instead, and nothing else. A backend descriptor must explicitly declare that it implements the restriction; unsupported backends are refused before any process starts.
The request may set Name, Profile's execution fields (Name, Agent, Model, Fallback, Effort, MaxTurns, Timeout and Env), Workspace, Prompt, Env, ResumeID and CostCapUSD, which holds each attempt to the cap. Other fields describe capabilities a restricted turn cannot have and are rejected rather than ignored.
type SandboxBoundary ¶
type SandboxBoundary struct {
// SessionsDir is where a session directory is created for a request
// that names none.
SessionsDir string
// MountDirs must be granted read-write.
MountDirs []string
// SkillMountDirs must be granted when the profile has skills.
SkillMountDirs []string
}
SandboxBoundary runs a session in a Docker Sandbox. The sandbox is given the granted mounts as its workspaces, with their access, and nothing else of the host; an environment built from the allowlist alone, with no HOME of its own (the sandbox has one; a HOME the session sets is passed) and no agent credential (the sandbox's proxy supplies it); and, without VCS, stand-ins for the VCS executables in front of the sandbox's PATH, which deny them by name and by nothing else: a VCS executable of the sandbox's image reached by its path is not masked. Verify refuses a request whose own paths the grants do not cover, the way ContainerBoundary does, and a workspace sbx cannot be handed: the host's root, or a path holding a colon. The Dagger engine is given only to a profile that asks for it and is granted it (verifyDagger), and an MCP entry on the host only when its name and port are granted (HostServer).
type Session ¶
type Session interface {
// Policy is what the session enforces on every turn it runs.
Policy() Policy
// Run runs one turn under the session's grants: see Admit for what it
// does to the request. A turn the policy does not describe, because
// something the policy was read from has changed since Prepare, is
// refused with ErrPolicyChanged before anything starts. Otherwise it is
// Runner.Run.
Run(ctx context.Context, req Request) (*Result, error)
// Release frees what Prepare made. Run is refused with ErrReleased
// afterwards. Call it once no turn of the session is running; calling it
// again does nothing.
Release(ctx context.Context) error
}
Session is one prepared set of grants.
type SkillPreparer ¶
SkillPreparer supplies prepared plugin directories; acquisition and cache policy belong to the caller.
type ToolSupport ¶
ToolSupport is one backend's declaration for one placement: whether a writable turn there is held to its granted built-in tools, and for one that is not, what the caller can change instead.
type Turn ¶
type Turn struct {
// Env is the complete process environment, as NAME=value pairs, before
// the backend's own configuration variables are added.
Env []string
// Tools are the built-in tools passed to the agent; nil means all.
Tools []string
// Mounts are the granted paths with their symbolic links resolved.
Mounts []Mount
// WriteDirs are the writable directories beyond the working directory
// the agent is told about.
WriteDirs []string
// VCS is whether version control is granted and requested.
VCS bool
// DeniedExecutables are shadowed on PATH for the session.
DeniedExecutables []string
// Binds are everything a container or Docker Sandbox session sees of
// the host; nil for a host session.
Binds []Bind
// Confinement is what the operating system enforces for a confined host
// session; nil for any other.
Confinement *Confinement
// DaggerEngine is the Dagger engine the session reaches; empty for
// none.
DaggerEngine string
// HostServers are the granted host servers the profile's MCP entries
// reach, in name order: the ports a SandboxSbx session is allowed on
// the host's loopback.
HostServers []HostServer
}
Turn is a verified request: the effective capabilities its process gets.
Source Files
¶
- backend.go
- backends.go
- codex_rpc.go
- codex_tools.go
- confine.go
- confine_linux.go
- confine_seatbelt.go
- container.go
- containermask.go
- containeruse.go
- costcap.go
- enforce.go
- grants.go
- interrupted.go
- opencode_customtools.go
- outcome.go
- pi.go
- probe.go
- profile.go
- readserver.go
- restricted.go
- sandbox.go
- sbx.go
- sbxdagger.go
- sbxhost.go
- session.go
- work.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agentbin resolves the executable of a coding agent — claude, codex, opencode or pi — before a session runs it, and keeps a test binary from ever running a real one.
|
Package agentbin resolves the executable of a coding agent — claude, codex, opencode or pi — before a session runs it, and keeps a test binary from ever running a real one. |
|
Package agenttest supplies offline executables for runner and caller contract tests: agents, the container engine, the Docker Sandboxes CLI and a host MCP server.
|
Package agenttest supplies offline executables for runner and caller contract tests: agents, the container engine, the Docker Sandboxes CLI and a host MCP server. |
|
enforcertest
Package enforcertest is an agent.Enforcer that starts no process: a caller tests what it does around Prepare, Run and Release, and what its grants let a turn do, with a Go function playing the agent.
|
Package enforcertest is an agent.Enforcer that starts no process: a caller tests what it does around Prepare, Run and Release, and what its grants let a turn do, with a Go function playing the agent. |
|
Package procs finds and stops agent sessions started by a caller, for orphan cleanup after a crash.
|
Package procs finds and stops agent sessions started by a caller, for orphan cleanup after a crash. |