Help us shape the future of Go! Take the 2026 Go Developer Survey

agentcore

module
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT

README

agentcore

github.com/kpenfound/agentcore runs headless coding-agent sessions and the pieces a factory built on them needs: the session runner and its execution boundary, an MCP host, a review pipeline, workspaces over git and jj, operational primitives and a client for Jev, TypeSafe's System One model. busybees is one consumer; its adapters supply the configuration, tracker mapping and policy this module leaves to the caller.

go get github.com/kpenfound/agentcore@latest

Build with go build ./.... Tests never run on the host: run them inside Dagger. CONTRIBUTING.md has the recipes.

Execution boundary

agent.Runner.Run takes an agent.Request with an execution-only Profile: backend, models, effort, tools, prepared MCP entries, sandbox, shell and environment. The caller selects profiles before running; Profile.Fallback is the profile the session runs on instead when it has no capacity, agent included, with its own fallback after it: ops.SelectProfile walks the chain for a retry, and the claude backend passes a claude fallback's model as --fallback-model so claude switches to it within the session.

The caller also supplies:

  • EnvironmentPrefix to strip stale inherited variables, even granted ones, and the request's Env for session context and credentials. An empty prefix strips nothing.

  • ValidOutcomes on each request and when calling agent.Report. A nil set accepts any status; an empty, non-nil set accepts none. Validation errors list the supplied statuses. Workflow-specific requirements belong to the caller.

  • Backend name prefixes, container labels, writable mounts, and optional HostMCP launch details for a server that runs on the host during a container session. A container session without HostMCP starts no host server.

  • SandboxDomains for Claude's network permissions. VCSAccess asks for the workspace's VCS mounts and the request's VCSEnv / VCSContainerEnv identity, credentials and configuration; the request's Grants.VCS must allow it. ContainerEnv and explicit caller mounts remain caller-controlled.

  • Grants, the session's complete capabilities (below). A request without them does not run.

  • An optional SkillPreparer and read-only skill cache mounts. Acquisition, caching and configuration policy stay with the caller.

  • An optional CostCapUSD on the request (zero is no cap). The runner stops the turn once the known cost its stream reports reaches the cap, and checks the final cost when the turn ends; either way the result has CostCapped set, ErrorSubtype cost_cap, the cost observed until then in CostUSD, and a result file. The caller's cancellation returns an error and no result. Each backend reports cost its own way:

    Backend Cost in the stream Stopped in flight
    claude running total in each result event by claude itself, given --max-budget-usd
    opencode each step_finish's part.cost, summed after the step that reaches the cap
    pi each assistant message_end's cost, summed after the response that reaches the cap
    codex none (tokens only) never: an unknown cost never reaches a cap

agent.Runner.RunRestricted is the smaller entry point for a read-only analysis turn. It accepts the same Request and uses the same backend command builders, stream parsers, timeout and process-group lifecycle as Run, but it owns the capability contract instead of accepting caller grants. The request may carry only a workspace, prompt, resume id, environment, cost cap and the profile's agent/model/effort/turn/timeout/fallback settings. It receives no MCP server, outcome contract, writable mount, VCS access, skill, plugin or hook. Variables with the runner's EnvironmentPrefix and VCS identity variables are removed. Claude is held to named read-only tools, empty settings sources and strict empty MCP configuration; Codex is held to its read-only sandbox, disabled command/network/plugin features and a fail-closed inventory that disables every inherited MCP server. Restricted Codex also sets agents.enabled=false: disabling multi-agent feature flags alone does not remove collaboration tools for models that advertise them. Its JavaScript functions.exec host remains available for configured read-only MCP tools. OpenCode runs in pure mode as a private agent whose effective configuration is probed before launch: only read, grep and glob are allowed and every inherited MCP server must resolve disabled. The turn is refused before that when OpenCode would load a custom tool (see the OpenCode tools bullet below). Pi loads no extension (including the ordinary MCP adapter), skill, prompt template or context file and receives only its read, grep, find and ls tools. Every backend descriptor declares whether it can establish this contract and whether it supports follow-up; Codex resumes its thread through thread/resume. A capacity failure walks Profile.Fallback, applying the same contract to every attempt and clearing a resume id before crossing to another backend. The returned RestrictedResult.Agent, RestrictedResult.Model and RestrictedResult.ClaudeID belong to the backend that answered.

MCP entries contain public context in Env and credential names in EnvVars. Pass credential values in the request environment. MCPEntries can expand configured environment references before entries are passed to the runner; prepared entries are not expanded again. BearerTokenEnv names the process variable holding a remote server's bearer token; each backend writes its native environment reference. HostMCP uses this field for its generated token.

Codex receives the session directory through -c shell_environment_policy.set.<prefix>SESSION_DIR=... even without MCP. Orphan scans use procs.CodexMarker(prefix) with the same environment prefix; LegacyCodex can also match a caller's older MCP-based marker. With the default empty prefix, procs.Find needs no marker overrides.

Discovery deletes the stale pid, container id and server pid files it reads, the cleanup a caller stopping sessions wants. A caller that only inspects asks through procs.Finder{ReadOnly: true}, which reports the same sessions and leaves every file in place.

An ordinary pi session (AgentPi) has no MCP support of its own: the runner loads PiMCPAdapter (npm:pi-mcp-adapter) and then Profile.PiPackages with -e, writes the MCP entries to pi-mcp.json in the session directory, and passes it as the adapter's --mcp-config with PI_MCP_CONFIG_MODE=exclusive. Restricted execution loads neither the adapter nor configured packages.

Adding a backend

A new agent CLI joins in one descriptor and one implementation, and every other list derives from or is held to them. The checklist:

  1. Metadata. Add the Backend to agent.Backends (backends.go): Name (also the value of the agent setting and the default executable's basename), Credentials (the variables the CLI reads its credential from), ProviderEnv (its configuration and credential variables, * for a prefix), ArgvMarker (whether the command line carries a marker the process-table scan finds), a bin selector for the runner's executable override, and the implementation. AgentCredentials and the provider environment a caller grants derive from the descriptors.
  2. Ordinary execution. Give the implementation its command (the executable comes from b.executable(r), never named directly) and its consume (the stream reduced to a streamEnd, every cost it carries reported to the costMeter as it is read), and cover both with a fixture test in fixture_test.go's style, backendNamed for the descriptor lookup.
  3. Restricted review execution. Extend command's paths.restricted branch to establish the whole read-only floor — no write, run or fetch tool, no MCP (inherited servers inventoried and disabled, a probe that cannot answer failing the launch), no writable VCS access, inherited configuration unable to widen any of it — and cover it in restricted_test.go: the command line, inherited-configuration attacks, the fail-closed probe, and the result fields. A restriction that cannot be established is refused before any process starts, never silently dropped.
  4. Capability declarations. Set Restricted (RestrictedCapabilities): Supported only when the command builder can establish the complete floor, and FollowUp when ResumeID is meaningful. The pointer is nil when a descriptor forgets, which TestBackendsAreDeclaredCompletely reports; TestBackendRestrictedCapabilities pins each declaration. Set WritableTools for every entry of agent.Placements (see Grants): Supported only where command holds a writable turn to the granted built-in tools, and a Remedy everywhere else. TestEveryBackendDeclaresWritableToolsForEveryPlacement pins them.
  5. Completeness. procs.AgentExecutables names the executable (TestBackendsMatchProcsExecutables holds the two together); a caller's doctor carries the toolchain check; SbxTemplates has a template or the sandbox is refused for it; profile.go's per-agent facts (PiPackages, SbxTemplates) say what applies; and the repository's review and role documentation names the agent where it lists the ones there are.

Grants

agent.Grants lists everything a session may have:

req.Grants = &agent.Grants{
	Env:    []string{"PATH", "HOME", "ANTHROPIC_API_KEY", "LC_*"},
	Tools:  []string{"Read", "Edit", "mcp__tools"},
	Mounts: []agent.Mount{{Path: "/", Access: agent.ReadOnly}, {Path: work, Access: agent.ReadWrite}},
	VCS:    false,
}
  • Env is the complete allowlist of variable names; NAME* is a prefix. A host variable is inherited only when listed, and a variable the profile or request sets must be listed. Without VCS, an entry that could match a gh, git or SSH-agent variable (GH_*, GITHUB_*, GIT_*, GCM_*, SSH_AUTH_SOCK, ...) is refused.
  • Tools are built-in tool names, or agent.ToolsAll, and mcp__<server> for each MCP server. The profile's AllowedTools, MCP and HostMCP may name less, never more; DisallowedTools only narrows. A grant without ToolsAll is held to the tools it names where the backend's WritableTools declares the placement supported, and refused with ErrUnsupported everywhere else (see below).
  • Mounts are absolute, clean, existing paths, ReadOnly or ReadWrite, judged after their symbolic links are resolved; Within confines them all. The working directory must lie inside one. Without VCS, a writable mount inside or holding .git, .hg, .jj or .svn is refused.
  • VCS grants version control. Without it, a host session finds gh, git, hg, jj and svn shadowed on PATH by stand-ins that exit 126, and Claude's sandbox settings deny them.
  • DaggerEngine grants a Dagger engine, unix://<socket> or tcp://<host>:<port>, to a SandboxSbx session whose Profile.Dagger asks for that engine. Every boundary refuses it otherwise: granted to a profile that does not ask (ErrUnsupported), asked for without the grant or with another engine granted (ErrNotGranted), or in any other mode (ErrUnsupported).
  • HostServers grants a SandboxSbx session MCP servers the caller already runs on the host's loopback, each by name and port. The server must also be granted as mcp__<name>. See the sandbox below; every other mode refuses the grant with ErrUnsupported.
Built-in tool grants

Each backend declares, for every agent.Placement (the sandbox, and for a host sandbox whether Profile.Confine is set), whether an ordinary turn, one that may write, can be held to the built-in tools its grants name. A placement declared unsupported is refused before anything starts, with an ErrUnsupported error that names the agent, the placement and the remedy:

builder: grant cannot be enforced: agent "opencode" cannot hold a writable turn to its granted built-in tools in sandbox "claude"; sandbox "claude" runs claude alone; run opencode in sandbox "none", "container" or "sbx"
Agent none none confined claude claude confined container sbx
claude yes yes yes yes yes yes
opencode yes yes no no yes yes
codex yes yes no no yes yes
pi no no no no no no
  • Claude is started with --tools.

  • Opencode tools carry opencode's own permission names: bash, codesearch, edit (every tool that changes a file), external_directory (the file tools outside the working directory), glob, grep, list, lsp, read, skill, todoread, todowrite, webfetch and websearch. Any other name is refused, and so is task: its subagents run under their own permissions, not the turn's. The turn runs with --pure as a generated primary agent, agentcore-granted, whose permissions deny *, allow each granted tool, and allow <server>_* for each of the session's MCP servers. A server whose <server>_* would also match one of opencode's own permissions is refused: a server named external, whose external_* matches external_directory. Before the model starts, opencode debug config runs where the turn will run: on the host, under the same confinement, in a container of the same image with the same binds, or in the same Docker Sandbox. Every inherited MCP server is disabled through OPENCODE_CONFIG_CONTENT, and a second inventory refuses the turn when the agent is missing, disabled, not primary, has any permission other than those or a tools map, when an inherited server is still enabled, or when one of the session's servers is missing, disabled or differs from what the runner wrote in any key (command, url, environment, headers, or a key added). A {env:NAME} reference matches as written or resolved against the turn's environment. Custom tools refuse the turn before any of that. OpenCode imports every *.js and *.ts file in the tool/ and tools/ directories of its configuration roots as a tool, even with --pure, and it does so before any permission applies. A file named after a granted tool, such as read.ts, takes that tool's permission. OpenCode's roots are $XDG_CONFIG_HOME/opencode (else ~/.config/opencode), every .opencode directory from the working directory up to the worktree, ~/.opencode and $OPENCODE_CONFIG_DIR. Where the turn will run, and with its environment, the runner starts the opencode executable as its Bun runtime (`BUN_BE_BUN=1 opencode --config=/dev/null --no-env-file -e

Directories

Path Synopsis
Package agent runs one headless coding-agent session.
Package agent runs one headless coding-agent session.
agentbin
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.
agenttest
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.
agenttest/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.
procs
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.
jev
Package jev asks Jev, TypeSafe's System One model, typed questions through a Provider, by default OpenRouter's TypeSafe-compatible API.
Package jev asks Jev, TypeSafe's System One model, typed questions through a Provider, by default OpenRouter's TypeSafe-compatible API.
jevtest
Package jevtest supplies a fake Jev provider for tests.
Package jevtest supplies a fake Jev provider for tests.
systemone
Package systemone is a client for TypeSafe's System One API, which answers typed questions about a state: a Choice selects one supplied option, a Score rates against supplied levels, and a Noul is the probability that a proposition holds.
Package systemone is a client for TypeSafe's System One API, which answers typed questions about a state: a Choice selects one supplied option, a Score rates against supplied levels, and a Noul is the probability that a proposition holds.
Package mcphost provides MCP registration and lifecycle without application roles or workflow policy.
Package mcphost provides MCP registration and lifecycle without application roles or workflow policy.
Package ops supplies operational primitives for caller-owned factory loops: retry decisions, accounting and budget signals, capacity and degraded episodes, bounded events, fair slots and level-triggered wakes.
Package ops supplies operational primitives for caller-owned factory loops: retry decisions, accounting and budget signals, capacity and degraded episodes, bounded events, fair slots and level-triggered wakes.
Package skills parses and prepares skill references for agent.SkillPreparer.
Package skills parses and prepares skill references for agent.SkillPreparer.
vcs
Package vcs defines workspaces without prescribing a version control system.
Package vcs defines workspaces without prescribing a version control system.
git
Package git replays revisions in a git working tree (vcs.Replayer) and creates signed commits in a git store (vcs.Signer).
Package git replays revisions in a git working tree (vcs.Replayer) and creates signed commits in a git store (vcs.Signer).
internal/vcstest
Package vcstest builds the local git repository the replay tests of every backend share: a dependent branch whose parent was integrated upstream as a squash.
Package vcstest builds the local git repository the replay tests of every backend share: a dependent branch whose parent was integrated upstream as a squash.
jj
Package jj replays revisions in a Jujutsu workspace (vcs.Replayer).
Package jj replays revisions in a Jujutsu workspace (vcs.Replayer).
Package work defines caller-owned work identity.
Package work defines caller-owned work identity.

Jump to

Keyboard shortcuts

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