terok_agent
terok_agent
¶
terok-agent: single-agent task runner for hardened Podman containers.
Builds agent images, launches instrumented containers, and manages the
lifecycle of one AI coding agent at a time. Designed for standalone use
(terok-agent run claude .) and as a library for terok orchestration.
Public API::
# Provider registry
from terok_agent import HEADLESS_PROVIDERS, HeadlessProvider, get_provider
from terok_agent import PROVIDER_NAMES, CLIOverrides
from terok_agent import apply_provider_config, build_headless_command
from terok_agent import collect_opencode_provider_env, collect_all_auto_approve_env
# Agent config preparation
from terok_agent import AgentConfigSpec, prepare_agent_config_dir, parse_md_agent
# Auth
from terok_agent import AUTH_PROVIDERS, AuthProvider, authenticate
# Instructions
from terok_agent import resolve_instructions, bundled_default_instructions
# Credential proxy
from terok_agent import ensure_proxy_routes
# Config stack
from terok_agent import ConfigStack, ConfigScope, resolve_provider_value
Internal symbols (available via submodule import for white-box tests)::
from terok_agent.headless_providers import generate_agent_wrapper, generate_all_wrappers
from terok_agent.headless_providers import OpenCodeProviderConfig, ProviderConfig, WrapperConfig
from terok_agent.config_stack import deep_merge, load_yaml_scope, load_json_scope
from terok_agent.instructions import has_custom_instructions
from terok_agent._util import podman_userns_args
AUTH_PROVIDERS = {}
module-attribute
¶
All known auth providers (agents + tools), keyed by name. Loaded from resources/agents/*.yaml.
DEFAULT_BASE_IMAGE = 'ubuntu:24.04'
module-attribute
¶
Default base OS image when none is specified.
HEADLESS_PROVIDERS = {}
module-attribute
¶
All headless agent providers, keyed by name. Loaded from resources/agents/*.yaml.
AgentConfigSpec(tasks_root, task_id, subagents, selected_agents=None, prompt=None, provider='claude', instructions=None, default_agent=None, mounts_base=None)
dataclass
¶
Groups parameters for preparing an agent-config directory.
__post_init__()
¶
Coerce mutable sequences to tuples for true immutability.
Source code in src/terok_agent/agents.py
AuthProvider(name, label, host_dir_name, container_mount, command, banner_hint, extra_run_args=tuple(), modes=('api_key',), api_key_hint='')
dataclass
¶
Describes how to authenticate one tool/agent.
name
instance-attribute
¶
Short key used in CLI and TUI dispatch (e.g. "codex").
label
instance-attribute
¶
Human-readable display name (e.g. "Codex").
host_dir_name
instance-attribute
¶
Directory name under mounts_dir() (e.g. "_codex-config").
container_mount
instance-attribute
¶
Mount point inside the container (e.g. "/home/dev/.codex").
command
instance-attribute
¶
Command to execute inside the container (OAuth mode only).
banner_hint
instance-attribute
¶
Provider-specific help text shown before the container runs.
extra_run_args = field(default_factory=tuple)
class-attribute
instance-attribute
¶
Additional podman run arguments (e.g. port forwarding).
modes = ('api_key',)
class-attribute
instance-attribute
¶
Supported auth modes: "oauth" (container), "api_key" (fast path).
api_key_hint = ''
class-attribute
instance-attribute
¶
Hint shown when prompting for an API key (URL to get one).
supports_oauth
property
¶
Whether this provider supports OAuth (container-based) auth.
supports_api_key
property
¶
Whether this provider supports direct API key entry.
BuildError
¶
Bases: RuntimeError
Raised when base-image construction cannot complete.
The CLI maps this to a user-facing error message; library callers
can catch it without being terminated by SystemExit.
ImageSet(l0, l1, l1_sidecar=None)
dataclass
¶
L0 + L1 image tags produced by a build.
CommandDef(name, help='', handler=None, args=(), group='')
dataclass
¶
Definition of a terok-agent subcommand.
ConfigScope(level, source, data)
dataclass
¶
A single layer in the config stack.
ConfigStack()
¶
Ordered collection of config scopes, lowest-priority first.
Usage::
stack = ConfigStack()
stack.push(ConfigScope("global", global_path, global_data))
stack.push(ConfigScope("project", proj_path, proj_data))
resolved = stack.resolve()
Initialise an empty config stack.
Source code in src/terok_agent/config_stack.py
CLIOverrides(model=None, max_turns=None, timeout=None, instructions=None)
dataclass
¶
CLI flag overrides for a headless agent run.
model = None
class-attribute
instance-attribute
¶
Explicit --model from CLI (takes precedence over config).
max_turns = None
class-attribute
instance-attribute
¶
Explicit --max-turns from CLI.
timeout = None
class-attribute
instance-attribute
¶
Explicit --timeout from CLI.
instructions = None
class-attribute
instance-attribute
¶
Resolved instructions text. Delivery is provider-aware.
HeadlessProvider(name, label, binary, git_author_name, git_author_email, headless_subcommand, prompt_flag, auto_approve_env, auto_approve_flags, output_format_flags, model_flag, max_turns_flag, verbose_flag, supports_session_resume, resume_flag, continue_flag, session_file, supports_agents_json, supports_session_hook, supports_add_dir, log_format, opencode_config=None)
dataclass
¶
Describes how to run one AI agent in headless (autopilot) mode.
name
instance-attribute
¶
Short key used in CLI dispatch (e.g. "claude", "codex").
label
instance-attribute
¶
Human-readable display name (e.g. "Claude", "Codex").
binary
instance-attribute
¶
CLI binary name (e.g. "claude", "codex", "opencode").
git_author_name
instance-attribute
¶
AI identity name for Git author/committer policy application.
git_author_email
instance-attribute
¶
AI identity email for Git author/committer policy application.
headless_subcommand
instance-attribute
¶
Subcommand for headless mode (e.g. "exec" for codex, "run" for opencode).
None means the binary uses flags only (e.g. claude -p).
prompt_flag
instance-attribute
¶
Flag for passing the prompt.
"-p" for flag-based, "" for positional (after subcommand).
auto_approve_env
instance-attribute
¶
Environment variables for fully autonomous execution.
Injected into the container env by _apply_unrestricted_env() when
TEROK_UNRESTRICTED=1. Read by agents regardless of launch path.
Claude uses /etc/claude-code/managed-settings.json instead.
auto_approve_flags
instance-attribute
¶
CLI flags injected by the shell wrapper when TEROK_UNRESTRICTED=1.
Only for agents that lack an env var or managed config mechanism
(currently Codex only). Empty for all other agents — their env vars
and /etc/ config files handle permissions across all launch paths.
output_format_flags
instance-attribute
¶
Flags for structured output (e.g. ("--output-format", "stream-json")).
model_flag
instance-attribute
¶
Flag for model override ("--model", "--agent", or None).
max_turns_flag
instance-attribute
¶
Flag for maximum turns ("--max-turns" or None).
verbose_flag
instance-attribute
¶
Flag for verbose output ("--verbose" or None).
supports_session_resume
instance-attribute
¶
Whether the provider supports resuming a previous session.
resume_flag
instance-attribute
¶
Flag to resume a session (e.g. "--resume", "--session").
continue_flag
instance-attribute
¶
Flag to continue a session (e.g. "--continue").
session_file
instance-attribute
¶
Filename in /home/dev/.terok/ for stored session ID.
Providers that capture session IDs via plugin or post-run parsing set this
to a filename (e.g. "opencode-session.txt"). Providers with their own
hook mechanism (Claude) or no session support set this to None.
supports_agents_json
instance-attribute
¶
Whether the provider supports --agents JSON (Claude only).
supports_session_hook
instance-attribute
¶
Whether the provider supports SessionStart hooks (Claude only).
supports_add_dir
instance-attribute
¶
Whether the provider supports --add-dir "/" (Claude only).
log_format
instance-attribute
¶
Log format identifier: "claude-stream-json" or "plain".
opencode_config = None
class-attribute
instance-attribute
¶
Configuration for OpenCode-based providers (Blablador, KISSKI, etc.).
When set, this provider uses OpenCode with a custom OpenAI-compatible API. The configuration includes API endpoints, model preferences, and provider-specific settings that are injected into the container environment.
uses_opencode_instructions
property
¶
Whether the provider uses OpenCode's instruction system.
CredentialProxyRoute(provider, route_prefix, upstream, auth_header='Authorization', auth_prefix='Bearer ', credential_type='api_key', credential_file='', phantom_env=dict(), oauth_phantom_env=dict(), base_url_env='', socket_path='', socket_env='', shared_config_patch=None, oauth_refresh=None)
dataclass
¶
Proxy route config parsed from a credential_proxy: YAML section.
Used to generate the routes.json that the credential proxy server reads.
provider
instance-attribute
¶
Agent/tool name (e.g. "claude").
route_prefix
instance-attribute
¶
Path prefix in the proxy (e.g. "claude" → /claude/v1/...).
upstream
instance-attribute
¶
Upstream API base URL (e.g. "https://api.anthropic.com").
auth_header = 'Authorization'
class-attribute
instance-attribute
¶
HTTP header name for the real credential.
auth_prefix = 'Bearer '
class-attribute
instance-attribute
¶
Prefix before the token value in the auth header.
credential_type = 'api_key'
class-attribute
instance-attribute
¶
Type of credential: "oauth", "api_key", "oauth_token", "pat".
credential_file = ''
class-attribute
instance-attribute
¶
Credential file path relative to the auth mount.
phantom_env = field(default_factory=dict)
class-attribute
instance-attribute
¶
Phantom env vars for API-key credentials (e.g. {"ANTHROPIC_API_KEY": true}).
oauth_phantom_env = field(default_factory=dict)
class-attribute
instance-attribute
¶
Phantom env vars for OAuth credentials (e.g. {"CLAUDE_CODE_OAUTH_TOKEN": true}).
When the stored credential type is "oauth" and this is non-empty, these
env vars are injected instead of :attr:phantom_env.
base_url_env = ''
class-attribute
instance-attribute
¶
Env var to override with proxy URL (e.g. "ANTHROPIC_BASE_URL").
socket_path = ''
class-attribute
instance-attribute
¶
Unix socket path for socat bridge (e.g. "/tmp/terok-claude-proxy.sock").
socket_env = ''
class-attribute
instance-attribute
¶
Env var that receives :attr:socket_path (e.g. "ANTHROPIC_UNIX_SOCKET").
shared_config_patch = None
class-attribute
instance-attribute
¶
Optional shared config patch applied after auth (e.g. Vibe's config.toml).
oauth_refresh = None
class-attribute
instance-attribute
¶
OAuth refresh config: {token_url, client_id, scope}.
SidecarSpec(tool_name, env_map=dict())
dataclass
¶
Sidecar container configuration parsed from a sidecar: YAML section.
Tools with sidecar specs run in a separate lightweight L1 image (no agent CLIs) and receive the real API key instead of phantom tokens.
tool_name
instance-attribute
¶
Tool identifier used to select the Jinja2 install block in the template.
env_map = field(default_factory=dict)
class-attribute
instance-attribute
¶
Maps container env var names to credential dict keys.
Example: {"CODERABBIT_API_KEY": "key"} reads cred["key"] and
injects it as CODERABBIT_API_KEY.
AgentRunner(*, sandbox=None, roster=None, base_image='ubuntu:24.04')
¶
Composes sandbox + agent config into a single container launch.
All three run methods follow the same flow:
- Ensure L0+L1 images exist (build if missing)
- Prepare agent-config directory (wrapper, instructions, prompt)
- Assemble environment variables and volume mounts
- Optionally set up gate (mirror repo, create token)
- Launch container via podman
Source code in src/terok_agent/runner.py
sandbox
property
¶
Lazy-init sandbox facade.
roster
property
¶
Lazy-init agent roster.
run_headless(provider, repo, *, prompt, branch=None, model=None, max_turns=None, timeout=1800, gate=True, name=None, follow=False, unrestricted=True, gpu=False, hooks=None)
¶
Launch a headless agent run. Returns container name.
The agent executes the prompt against repo (local path or git URL) and exits when done or when timeout is reached. Set follow=True to block until the agent finishes (the CLI does this by default).
Source code in src/terok_agent/runner.py
run_interactive(provider, repo, *, branch=None, gate=True, name=None, unrestricted=True, gpu=False, hooks=None)
¶
Launch an interactive container. Returns container name.
The container stays up after init; user logs in via podman exec.
Source code in src/terok_agent/runner.py
run_web(repo, *, port=None, branch=None, gate=True, name=None, public_url=None, unrestricted=True, gpu=False, hooks=None)
¶
Launch a toad web container. Returns container name.
If port is None, an available port is auto-allocated.
Source code in src/terok_agent/runner.py
run_tool(tool, repo, *, tool_args=(), branch=None, gate=True, name=None, follow=True, timeout=600)
¶
Launch a sidecar tool container. Returns container name.
Runs the named tool in a lightweight sidecar L1 image (no agent CLIs). The tool receives the real API key from the credential store — not a phantom token.
Source code in src/terok_agent/runner.py
resolve_provider_value(key, config, provider_name)
¶
Extract a provider-aware config value.
Supports two forms:
- Flat value —
model: opus→ same for all providers. - Per-provider dict —
model: {claude: opus, codex: o3, _default: fast}→ looks up provider_name, falls back to_default, thenNone.
Returns None when the key is absent or has no match for the provider.
Null override behaviour: when a per-provider dict maps a provider to
null (Python None), that None is treated as "no value" and the
resolver falls back to _default. This is intentional — it allows a
lower-priority config layer to set a provider-specific value that a
higher-priority layer can effectively unset by mapping it to null,
letting the _default (or None) bubble up instead.
Source code in src/terok_agent/agent_config.py
parse_md_agent(file_path)
¶
Parse a .md file with YAML frontmatter into an agent dict.
Expected format
name: agent-name description: ... tools: [Read, Grep] model: sonnet
System prompt body...
Source code in src/terok_agent/agents.py
prepare_agent_config_dir(spec)
¶
Create and populate the agent-config directory for a task.
Writes:
- terok-agent.sh (always) — wrapper functions with git env vars
- agents.json (only when provider supports it and sub-agents are non-empty)
- prompt.txt (if prompt given, headless only)
- instructions.md (always) — custom instructions or a neutral default
- instructions path injected into shared
OpenCode and Blablador configs
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
AgentConfigSpec
|
All agent-config parameters bundled in an :class: |
required |
Returns the agent_config_dir path.
Source code in src/terok_agent/agents.py
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 | |
authenticate(project_id, provider, *, mounts_dir, image)
¶
Run the auth flow for provider against project_id.
Dispatches based on the provider's modes field:
- api_key only: prompt for key, store directly (no container)
- oauth only: launch container with vendor CLI
- both: ask user to choose, then dispatch accordingly
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str
|
Project identifier (for container naming). |
required |
provider
|
str
|
Auth provider name (e.g. |
required |
mounts_dir
|
Path
|
Base directory for shared config bind-mounts. |
required |
image
|
str
|
Container image to use for the auth container. |
required |
Raises SystemExit if the provider name is unknown.
Source code in src/terok_agent/auth.py
store_api_key(provider, api_key, credential_set='default')
¶
Store an API key directly in the credential DB (no container needed).
This is the non-interactive fast path for automated workflows and CI.
The key is stored as {"type": "api_key", "key": "<value>"}.
Source code in src/terok_agent/auth.py
build_base_images(base_image=DEFAULT_BASE_IMAGE, *, rebuild=False, full_rebuild=False, build_dir=None)
¶
Build L0 + L1 container images and return their tags.
Skips building if images already exist locally (unless rebuild or full_rebuild is set). Uses a temporary directory for the build context by default; pass build_dir to use a specific (empty or non-existent) directory instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_image
|
str
|
Base OS image (e.g. |
DEFAULT_BASE_IMAGE
|
rebuild
|
bool
|
Force rebuild with cache bust (refreshes agent installs). |
False
|
full_rebuild
|
bool
|
Force rebuild with |
False
|
build_dir
|
Path | None
|
Build context directory (must be empty or absent). |
None
|
Returns:
| Type | Description |
|---|---|
ImageSet
|
class: |
Raises:
| Type | Description |
|---|---|
BuildError
|
If podman is missing or a build step fails. |
ValueError
|
If build_dir exists and is non-empty. |
Source code in src/terok_agent/build.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | |
build_sidecar_image(base_image=DEFAULT_BASE_IMAGE, *, tool_name='coderabbit', rebuild=False, full_rebuild=False, build_dir=None)
¶
Build the L1 sidecar image for a specific tool. Returns the image tag.
Ensures L0 exists first (builds it if missing), then builds the sidecar image FROM L0. The sidecar contains only the named tool — no agent CLIs, no LLMs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_image
|
str
|
Base OS image (passed through to L0 build). |
DEFAULT_BASE_IMAGE
|
tool_name
|
str
|
Tool to install (selects Jinja2 conditional in template). |
'coderabbit'
|
rebuild
|
bool
|
Force rebuild with cache bust. |
False
|
full_rebuild
|
bool
|
Force rebuild with |
False
|
build_dir
|
Path | None
|
Build context directory (must be empty or absent). |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The sidecar image tag (e.g. |
Raises:
| Type | Description |
|---|---|
BuildError
|
If podman is missing or a build step fails. |
Source code in src/terok_agent/build.py
l0_image_tag(base_image)
¶
l1_image_tag(base_image)
¶
l1_sidecar_image_tag(base_image)
¶
render_l1_sidecar(l0_image, *, tool_name='coderabbit', cache_bust='0')
¶
Render the L1 sidecar (tool-only) Dockerfile.
The sidecar image is built FROM L0 (not L1) and installs a single tool binary — no agent CLIs, no LLMs. The tool_name selects which tool install block to activate via Jinja2 conditional.
Source code in src/terok_agent/build.py
stage_scripts(dest)
¶
Stage container helper scripts into dest.
Copies all files from terok_agent/resources/scripts/ into the given
directory, replacing any existing contents. Python bytecode caches and
__init__.py markers are excluded.
Source code in src/terok_agent/build.py
stage_tmux_config(dest)
¶
Stage the container tmux configuration into dest.
Copies container-tmux.conf — the green-status-bar config that
distinguishes container tmux sessions from host tmux.
Source code in src/terok_agent/build.py
stage_toad_agents(dest)
¶
Stage Toad ACP agent TOML definitions into dest.
These describe OpenCode-based agents (Blablador, KISSKI, etc.) that are injected into Toad's bundled agent directory at container build time.
Source code in src/terok_agent/build.py
extract_credential(provider, base_dir)
¶
Run the appropriate extractor for provider against base_dir.
Raises ValueError if no extractor is registered or extraction fails.
Source code in src/terok_agent/credential_extractors.py
agent_doctor_checks(roster, *, proxy_port=None)
¶
Return agent-level health checks for in-container diagnostics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
roster
|
AgentRoster
|
The loaded agent roster. |
required |
proxy_port
|
int | None
|
Credential proxy TCP port. Required for base URL checks;
if |
None
|
Returns:
| Type | Description |
|---|---|
list[DoctorCheck]
|
List of :class: |
Source code in src/terok_agent/doctor.py
apply_provider_config(provider, config, overrides=None)
¶
Resolve config values for a provider with best-effort feature mapping.
CLI flag overrides take precedence over config values. When the provider lacks a feature, an analogue is used where possible (e.g. injecting max-turns guidance into the prompt), and a warning is emitted for features that have no analogue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
dict
|
Merged agent config dict (from :func: |
required |
overrides
|
CLIOverrides | None
|
CLI flag overrides (model, max_turns, timeout, instructions). |
None
|
Source code in src/terok_agent/headless_providers.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
build_headless_command(provider, *, timeout, model=None, max_turns=None)
¶
Assemble the bash command string for a headless agent run.
The command assumes:
- init-ssh-and-repo.sh has already set up the workspace
- The prompt is in /home/dev/.terok/prompt.txt
- For Claude, the claude() wrapper function is sourced via bash -l
Returns a bash command string suitable for ["bash", "-lc", cmd].
Source code in src/terok_agent/headless_providers.py
collect_all_auto_approve_env()
¶
Collect auto_approve_env from all providers into one dict.
Used by task runners to inject these env vars at the container level (not just inside shell wrappers) so that ACP-spawned agents also inherit unrestricted permissions.
Source code in src/terok_agent/headless_providers.py
collect_opencode_provider_env()
¶
Collect environment variables for all OpenCode-based providers.
Returns a dictionary of environment variables that will be injected into containers to configure OpenCode-based providers. Each provider with opencode_config set contributes variables prefixed with TEROK_OC_{PROVIDER_NAME}_*.
Source code in src/terok_agent/headless_providers.py
get_provider(name, *, default_agent=None)
¶
Resolve a provider name to a HeadlessProvider.
Resolution order
- Explicit name if given
- default_agent (from project config)
"claude"(ultimate fallback)
Raises SystemExit if the resolved name is not in the registry.
Source code in src/terok_agent/headless_providers.py
bundled_default_instructions()
¶
Read and return the bundled default instructions from package resources.
Source code in src/terok_agent/instructions.py
resolve_instructions(config, provider_name, project_root=None)
¶
Resolve instructions from a merged config dict.
Supports:
- Flat string: returned as-is
- Per-provider dict: uses :func:resolve_provider_value, falls back to _default
- List (with _inherit): splices bundled default at each _inherit sentinel
- Absent/None: returns bundled default
After resolving the YAML value, appends the contents of
project_root/instructions.md (if it exists and is non-empty).
Returns the final instructions text.
Source code in src/terok_agent/instructions.py
mounts_dir()
¶
Base directory for agent config bind-mounts.
Each agent/tool gets a subdirectory (e.g. _claude-config/) that is
bind-mounted read-write into task containers. These directories are
intentionally separated from the credentials store since they are
container-exposed and subject to potential poisoning.
Source code in src/terok_agent/paths.py
scan_leaked_credentials(mounts_base)
¶
Return (provider, host_path) for credential files found in shared mounts.
When the credential proxy is active, real secrets should only live in the proxy's sqlite DB — not in the shared config directories that get mounted into containers. This function checks each routed provider's mount for credential files that would leak real tokens alongside phantom ones.
Files injected by :func:~terok_agent.auth._write_claude_credentials_file
are recognised by their dummy accessToken marker and skipped.
Source code in src/terok_agent/proxy_commands.py
ensure_proxy_routes(cfg=None)
¶
Generate routes.json from the YAML roster and write it to disk.
The routes file is written to the path configured in
:class:~terok_sandbox.SandboxConfig (typically
~/.local/share/terok/proxy/routes.json).
When cfg is None, falls back to standalone defaults.
Returns the path to the written file.