executor

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: Apache-2.0 Imports: 62 Imported by: 0

Documentation

Overview

Package executor is the hands' consumer: it pulls tool_exec work from the queue, runs the built-in toolset inside the session's sandbox, and appends the agent.tool_result events the brain resumes on. Platform-managed cloud and customer BYOC are the same pull protocol at two deployment points; this is the platform-managed one, embedding the Docker sandbox provider.

tool_exec is the loop this package is named for but not the whole of it. The same process claims three more kinds, each with its own driver file, and two of them run in the executor's own process with no sandbox at all: web_exec (webwork.go — web_fetch and web_search, for BOTH environment kinds, since the reference's worker implements only the six sandbox tools) and mcp_exec (mcpwork.go for the discovery that fills mcp_catalogs, mcpexec.go for the call itself, mcpspill.go for an answer too large to inline, mcpcred.go for the credential — likewise both kinds, since only this platform's driver answers an MCP call). The fourth, outputs_harvest (harvest.go), is a cloud session's alone: it snapshots the deliverables out of the sandbox to open an outcome-grading cycle, and a self_hosted sandbox has no file lane to read. What goes INTO a sandbox lives beside them — skills.go, files.go, repos.go and repoclone.go (go-git, so no git binary is a runtime dependency).

This package also owns the sandbox's lifecycle, which no work item triggers. reaper.go is the single owner of sandbox destruction on four tiers (a session deleted, archived or terminated, plus an idle tier past a configured TTL), and checkpoint.go captures a session's durable state to object storage before the idle tier destroys it, restoring it into a fresh sandbox on the next provision. Both are here because this is the only process holding the sandbox provider, and both are per-endpoint rather than coordinated: an executor sees only its own daemon or namespace, and reaping is idempotent.

The loop mirrors the brain's: Claim the oldest tool_exec item (reclaiming an expired lease), do the work, hand the item back. The brain, when a turn stops for a built-in tool, commits the agent.tool_use intents and enqueues one tool_exec item; this executor answers every unanswered agent.tool_use for the session, then — once the set is complete — enqueues the model_turn that wakes the brain to continue. The result append, the resume enqueue, and the item's completion are one transaction under the session row lock, so a concurrent trigger never sees a gap.

At-most-once is the queue's lease, not a marker in the sandbox (which is agent-writable and disposable — see internal/sandbox/shell). A lease keeper holds the claim while tools run so two executors never run one session's tools at once; a crash mid-run lets the lease lapse, and the reclaiming executor re-runs only the still-unanswered tools — a committed result is never re-run, so a tool's result is exactly-once even though a non-idempotent command can run more than once across a crash. That residue is inherent to a disposable sandbox and is documented, not solved here.

Index

Constants

View Source
const (
	// MetricCheckpoints counts capture attempts by outcome; the duration
	// histogram shares its name with a ".duration" suffix.
	MetricCheckpoints = "sandbox.checkpoint"
	// MetricRestores counts restore attempts by outcome.
	MetricRestores = "sandbox.restore"
)
View Source
const (
	// MetricSkillsMaterialized counts per-skill materialization outcomes.
	MetricSkillsMaterialized = "skills.materialized"
	// MetricSkillsMaterializeDuration is one whole materialization pass.
	MetricSkillsMaterializeDuration = "skills.materialize.duration"
	// MetricFilesMaterialized counts per-file mount materialization outcomes.
	MetricFilesMaterialized = "files.materialized"
	// MetricFilesMaterializeDuration is one whole file-materialization pass.
	MetricFilesMaterializeDuration = "files.materialize.duration"
	// MetricReposMaterialized counts per-repository materialization outcomes.
	MetricReposMaterialized = "repos.materialized"
	// MetricReposMaterializeDuration is one whole repo-materialization pass.
	MetricReposMaterializeDuration = "repos.materialize.duration"
	// MetricReposMaterializeBytes is one landed repository's shipped size.
	MetricReposMaterializeBytes = "repos.materialize.bytes"
)
View Source
const DefaultStallTimeout = toolset.DefaultStallBudget

DefaultStallTimeout is the budget an operator who sets none gets, shared with the BYOC worker rather than written out twice (toolset.DefaultStallBudget).

Exported because cmd/executor must check the budget an operator will actually get. A floor that only guards the value they typed is no floor at all: raising EXECUTOR_REPO_CLONE_TIMEOUT past this default rebuilds the reclaim loop while EXECUTOR_STALL_TIMEOUT is left unset (#383).

View Source
const MetricSessionsReaped = "sandbox.sessions.reaped"

MetricSessionsReaped counts reaped sessions by tier.

Variables

View Source
var ErrCheckpointTooLarge = errors.New("executor: checkpoint exceeds the configured size budget")

ErrCheckpointTooLarge reports a capture whose members exceed the configured budget. The TTL tier treats it as "reap without checkpoint" — an agent must not pin its sandbox immortal by filling the disk (plan 24 D8).

Functions

func GateTokenRevoker added in v0.2.0

func GateTokenRevoker(pool *pgxpool.Pool) sandbox.GateTokenRevoker

GateTokenRevoker is the pool-backed sandbox.GateTokenRevoker — the same implementation the executor injects into every Spec — exported for the provider construction in cmd glue, where the sandbox backend is built with a revoker so Reap can revoke a session's gate token before removing its containers (plan 24).

func ValidateWorkdir added in v0.2.0

func ValidateWorkdir(workdir string) error

ValidateWorkdir refuses a configured workdir that would alias the checkpoint's other roots or its own machinery: a workdir under (or over) the shell-state root or /mnt/session double-captures and double-charges the shared subtree, and one under /tmp would put the restore staging file — and state the checkpoint deliberately drops — inside the archive. cmd/executor calls this at startup; a violation is configuration, not data.

Types

type Config

type Config struct {
	Image        string
	Workdir      string
	LeaseTTL     time.Duration
	PollInterval time.Duration
	// StallTimeout bounds how long a claimed item may make no progress before
	// the lease keeper gives up on it — cancelling the work and leaving the
	// lease to lapse, so another executor reclaims it (#383). Progress is a
	// step finishing, not a byte moving, so it must clear the longest single
	// step a healthy run takes: toolset.MaxTimeout for one `bash`, a 500 MB
	// mount, a wait on the session lock behind another goroutine's checkpoint
	// capture, a cold image pull, or a CheckpointMaxBytes restore — provisioning
	// reports between those, so they are separate intervals rather than one sum.
	// The check rides the keeper's renewal tick, so detection lands somewhere in
	// [stall, stall+LeaseTTL/3]. Set it under the longest step and the item does
	// not merely retry: every reclaim re-runs that step and cancels at the same
	// point, so the session waits on a loop nothing breaks — which is why the
	// binary floors the configured value above toolset.MaxTimeout (a tool at its
	// cap is killed and answers *after* it) and a deployment whose object store
	// is far away should raise it well past that. It bounds
	// *silence*, never duration — an item that keeps finishing steps runs as
	// long as it likes. Not an off switch: a wedged sandbox call is what left
	// an executor stuck with no recovery at all, so 0 takes the default
	// (EXECUTOR_STALL_TIMEOUT).
	StallTimeout time.Duration
	// ReapInterval paces the sandbox reaper (reaper.go): one sweep of this
	// endpoint's owned sessions per interval (EXECUTOR_REAP_INTERVAL; 0 takes
	// the 60s default). Teardown latency is bounded by it, and nothing else
	// destroys sandboxes, so there is no off switch — a deployment that wants
	// slower reaping sets it longer.
	ReapInterval time.Duration
	// CheckpointMaxBytes budgets a workspace checkpoint (checkpoint.go): ONE
	// measure on both sides — the framed, uncompressed tar stream, metered as
	// capture writes it and again as restore decompresses it — so a capture
	// that fits is arithmetically guaranteed to restore
	// (EXECUTOR_CHECKPOINT_MAX_BYTES; 0 takes the 2 GiB default). Over
	// budget, the TTL tier reaps without a checkpoint — an agent must not
	// pin its sandbox immortal by filling the disk (plan 24 D8).
	CheckpointMaxBytes int64
	// SandboxIdleTTL arms the reaper's idle tier (plan 24 slice 5): an idle
	// cloud session whose last activity is older than this is checkpointed and
	// its sandbox reaped — unless it still owes work (a queued/starting/active
	// work item) or an unanswered confirmation ask (HITL-idle is mid-turn).
	// Zero disables the tier — deliberately, so a hand-built test Config never
	// reaps by surprise; cmd/executor resolves the unset env to the 24h
	// default (EXECUTOR_SANDBOX_IDLE_TTL; 0 there disables too). A blob-less
	// executor disables the tier at startup regardless: reaping without the
	// means to checkpoint would silently discard workspaces.
	SandboxIdleTTL time.Duration
	// Hardening is the containment every session's sandbox is created with —
	// cgroup limits, capability drops, optionally a uid and a read-only root
	// (#65). The zero value hardens nothing, which is what a test that builds a
	// Config by hand wants; cmd/executor resolves the platform's defaults from
	// the environment (sandbox.HardeningFromEnv), so every deployment gets them.
	Hardening sandbox.Hardening
	// ControlplaneURL and GateImage opt the deployment into the per-session egress
	// gate. A session wants a gate when its networking is `limited` or it has
	// vaults attached; its gate container (GateImage) fetches that session's egress
	// config from ControlplaneURL. When both are set, a gate-wanting session gets a
	// gate; when either is empty, no gate is requested and a gate-wanting session
	// keeps the backend's own fail-closed networking (Docker `limited` → no egress,
	// K8s → its init-container isolation, vault-attached → inert placeholders) — the
	// pre-gate behavior, so an un-opted-in deployment is unchanged rather than
	// faulted. An unrestricted, vault-less session never wanted a gate and networks
	// directly regardless. See gateSpec.
	ControlplaneURL string
	GateImage       string
	// OTelEndpoint and OTelInsecure are the deployment's OTLP collector config,
	// threaded into each session's gate container so its egress_request spans
	// export to the same collector as the executor (the gate is a separate process
	// that does not inherit this executor's environment). Empty OTelEndpoint =
	// no collector; the gate runs without an exporter.
	OTelEndpoint string
	OTelInsecure bool
	// The web tools' backends (docs/plan/15_web-tools.md). An unconfigured
	// tool answers with an is_error result naming what is missing, so the
	// misconfiguration surfaces to an operator instead of masking the tool.
	// web_search needs TavilyAPIKey; web_fetch needs JinaAPIKey OR an explicit
	// WebFetchBaseURL (the Reader protocol works keyless — a keyless proxy, or
	// the public free tier named deliberately — but with neither set, a bare
	// install must not silently egress model-chosen URLs to a public third
	// party). The base URLs point at Tavily-protocol / Jina-Reader-protocol
	// endpoints; empty resolves to the adapters' public defaults.
	TavilyAPIKey     string
	JinaAPIKey       string
	WebSearchBaseURL string
	WebFetchBaseURL  string
	// WebAllowedDomains, when non-empty, is the operator-side allowlist for
	// the web tools (#225): web_fetch may reach only these hosts, and a
	// search hit whose source is outside them is dropped. Entries use the
	// same grammar as the wire's allowed_hosts (a bare hostname, an IPv4
	// literal, or a "*."-prefixed wildcard that never matches the apex —
	// egress.HostSet, the one matcher). Empty means unrestricted: the
	// reference has per-tool allowed domains, but no wire field carries
	// them, so this knob is platform-native (docs/DIVERGENCES.md).
	WebAllowedDomains []string
	// The clone budgets for github_repository mounts (plan 25 decision 1).
	// The spool a clone lands in sits on executor-local disk, outside the
	// sandbox's own storage hardening, so one unbounded repository could
	// exhaust the executor and disrupt unrelated sessions. RepoCloneMaxBytes
	// is metered as bytes land — over the tree and its tar together — and
	// RepoCloneTimeout bounds one repository's clone; both surface as
	// tolerated clone failures (too_large / timeout), never as a failed run.
	RepoCloneTimeout  time.Duration
	RepoCloneMaxBytes int64
	// MCPPassTimeout bounds one mcp_exec pass — the discovery half across all of
	// the session's MCP servers, and the execution half across all of a turn's
	// outstanding calls — for the reason the clone budgets exist: both walk
	// third-party endpoints serially, so an unbounded pass would hold this
	// process's single work goroutine and disrupt unrelated sessions. Neither
	// half fails the run when it runs out: a server discovery does not reach is
	// a tolerated failed row, and a call execution does not make stays
	// outstanding and keeps the item, which comes back to finish it.
	MCPPassTimeout time.Duration
}

Config tunes the loop. Image is the sandbox base image (a deployment choice — the wire's environment config has no image field). LeaseTTL must comfortably exceed toolset.MaxTimeout: the lease keeper renews at TTL/3 while a tool runs, but the TTL is also the window a crashed executor's work waits before another reclaims it.

type Executor

type Executor struct {
	// contains filtered or unexported fields
}

Executor consumes tool_exec work over one Postgres pool and one sandbox provider.

func New

func New(pool *pgxpool.Pool, log *events.Log, q *queue.Queue, provider sandbox.Provider, blobs blob.Store, cipher secrets.Cipher, cfg Config) *Executor

func (*Executor) Run

func (e *Executor) Run(ctx context.Context) error

Run polls until the context is cancelled. It claims one tool_exec item at a time; an error processing one item is logged by returning it up to the caller only for a fatal claim failure — a per-item fault is swallowed so the loop keeps serving other sessions, and the faulted item is reclaimed after its lease lapses.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL