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.
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 ¶
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" )
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" )
const MetricSessionsReaped = "sandbox.sessions.reaped"
MetricSessionsReaped counts reaped sessions by tier.
Variables ¶
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
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
// 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
}
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 (*Executor) Run ¶
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.