Skip to content

Providers

providers

Agent registry + per-agent behaviour.

Each supported AI coding agent is described by an Agent dataclass that owns both its capability shape (flags, environment, session handling) and the behaviour bound to that shape — config resolution (Agent.apply_config) and headless-command assembly (Agent.build_headless_command).

The AGENTS dict maps short names to descriptors and is populated at package load time from the YAML roster.

LAUNCHER_ON_PROVIDER_SELECT = 'on_provider_select' module-attribute

Launcher mode: run only when a provider override is selected (default = bare binary).

LAUNCHER_ALWAYS = 'always' module-attribute

Launcher mode: run on every invocation (the launcher does per-run prep).

AGENTS = {} module-attribute

All agents, keyed by name. Loaded from resources/agents/*.yaml.

AGENT_NAMES = () module-attribute

OPENCODE_PROVIDERS = {} module-attribute

OpenCode-driven provider name → config-dir, for the curated harness providers (Blablador, KISSKI, OpenRouter). Populated from the roster's providers at load time so this layer can drive per-provider config injection without importing the roster (which would cycle).

AgentRunConfig(model, max_turns, timeout, prompt_extra, warnings) dataclass

Resolved per-run config for a headless provider.

Produced by Agent.apply_config after best-effort feature mapping.

model instance-attribute

Model override for providers that support it, else None.

max_turns instance-attribute

Max turns for providers that support it, else None.

timeout instance-attribute

Effective timeout in seconds.

prompt_extra instance-attribute

Extra text to append to the prompt (best-effort feature analogues).

warnings instance-attribute

Warnings about unsupported features (for user display).

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.

ProviderBinding(default=None, token_env=dict(), base_url_env='', socket_env='', credential_file='', credential_type='api_key', config_patch=None) dataclass

How one agent consumes a provider — the delivery concern.

Lifted out of the agent vault: block: which provider this agent routes to by default, and the per-agent env-var / config-patch plumbing that carries the phantom token and vault base URL into the agent's own configuration. The endpoint concern (upstream, wire auth) lives on the Provider this binding names.

default = None class-attribute instance-attribute

Provider name this agent routes to by default.

Set for agents with a fixed provider (natives → their LLM provider; tools → their API provider; the OpenCode-shim agents → their endpoint). None for harnesses (opencode, pi), which take a provider at runtime.

token_env = field(default_factory=dict) class-attribute instance-attribute

Phantom-token env var name, keyed by stored credential type.

Keys are credential types ("oauth", "pat", …); "_default" is the fallback. Most agents read one var ({"_default": "MISTRAL_API_KEY"}); Claude swaps on OAuth ({"oauth": "CLAUDE_CODE_OAUTH_TOKEN", "_default": "ANTHROPIC_API_KEY"}).

base_url_env = '' class-attribute instance-attribute

Env var overridden with the vault's HTTP URL (e.g. "ANTHROPIC_BASE_URL").

socket_env = '' class-attribute instance-attribute

Env var that receives the container-side vault socket path (HTTP-over-UNIX agents).

credential_file = '' class-attribute instance-attribute

Credential file path relative to the auth mount (e.g. ".credentials.json").

Drives the read-only credential shadow (terok-ai/terok#873) and the doctor's leaked-secret probe.

credential_type = 'api_key' class-attribute instance-attribute

Default credential type captured for this agent ("oauth"/"api_key"/ "oauth_token"/"pat"). The runtime token selection uses the stored type from the vault DB; this is the authoring default.

config_patch = None class-attribute instance-attribute

Optional shared-config patch applied after auth (e.g. Codex/Vibe config.toml).

Launcher(script, mode) dataclass

How an agent's wrapper hands off to a per-task launcher script.

A launcher script preps the agent — rewrites provider config, scopes a picker, injects instructions — before exec-ing the binary. mode decides when the wrapper routes through it:

  • LAUNCHER_ON_PROVIDER_SELECT — only when a provider override is selected; the default path runs the bare binary (a config-rewriting launcher has nothing to do without a selection).
  • LAUNCHER_ALWAYS — every invocation (the launcher does per-run prep the bare binary can't, such as instruction injection).

script instance-attribute

Launcher script name on PATH (e.g. "pi-provider").

mode instance-attribute

When the launcher runs — one of the LAUNCHER_* modes.

Agent(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_session_hook, supports_add_dir, log_format, refuse_subcommands=(), protocol=None, provider_binding=None, launcher=None) dataclass

Describes how to run one AI coding agent (all modes: interactive + headless).

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 agent 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_session_hook instance-attribute

Whether the agent supports SessionStart hooks (Claude only).

supports_add_dir instance-attribute

Whether the agent supports --add-dir "/" (Claude only).

log_format instance-attribute

Log format identifier: "claude-stream-json" or "plain".

refuse_subcommands = () class-attribute instance-attribute

Subcommands the in-container wrapper refuses with a friendly error.

Used to block credential-handling flows (login, logout, setup-token) that would otherwise pollute the host-shared mount — operators authenticate on the host via terok auth instead. Best effort only; the firewall is the actual enforcement (terok-ai/terok#873).

protocol = None class-attribute instance-attribute

Wire protocol the agent speaks ("anthropic-messages" / "openai-chat" / "openai-responses"), or None for non-LLM tools. Matched against a provider's serves to resolve a runtime agent×provider combo.

provider_binding = None class-attribute instance-attribute

How this agent routes to a provider — see ProviderBinding.

None for entries with no vault route of their own (harnesses, copilot).

launcher = None class-attribute instance-attribute

Per-task launcher the wrapper hands off to — see Launcher.

None runs the bare binary (any provider override arriving via env instead, e.g. Claude).

uses_opencode_instructions property

Whether the agent uses OpenCode's instruction system.

Only the OpenCode harness itself; the curated OpenCode-driven providers (Blablador, KISSKI, OpenRouter) are no longer agents — they run through the same opencode wrapper, which already carries this behaviour.

apply_config(config, overrides=None)

Resolve config values for this provider with best-effort feature mapping.

CLI flag overrides take precedence over config values. When this 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.

Source code in src/terok_executor/provider/providers.py
def apply_config(
    self,
    config: dict[str, Any],
    overrides: CLIOverrides | None = None,
) -> AgentRunConfig:
    """Resolve config values for this provider with best-effort feature mapping.

    CLI flag *overrides* take precedence over *config* values.  When this
    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.
    """
    if overrides is None:
        overrides = CLIOverrides()

    warnings: list[str] = []
    prompt_parts: list[str] = []

    # --- Model ---
    cfg_model = resolve_agent_value("model", config, self.name)
    model = overrides.model or (str(cfg_model) if cfg_model is not None else None)
    if model and not self.model_flag:
        warnings.append(
            f"{self.label} does not support model selection; ignoring model={model!r}"
        )
        model = None

    # --- Max turns ---
    cfg_turns = resolve_agent_value("max_turns", config, self.name)
    max_turns_raw = overrides.max_turns if overrides.max_turns is not None else cfg_turns
    max_turns: int | None = int(max_turns_raw) if max_turns_raw is not None else None
    if max_turns is not None and not self.max_turns_flag:
        # Best-effort: inject into prompt as guidance
        prompt_parts.append(f"Important: complete this task in no more than {max_turns} steps.")
        warnings.append(
            f"{self.label} does not support --max-turns; "
            f"added guidance to prompt instead ({max_turns} steps)"
        )
        max_turns = None

    # --- Timeout ---
    cfg_timeout = resolve_agent_value("timeout", config, self.name)
    timeout = (
        overrides.timeout
        if overrides.timeout is not None
        else (int(cfg_timeout) if cfg_timeout is not None else 1800)
    )

    # --- Instructions ---
    # Claude receives instructions via --append-system-prompt in the wrapper.
    # Codex receives instructions via -c model_instructions_file=... in the wrapper.
    # OpenCode-based providers receive instructions via opencode.json `instructions`
    # array (injected by prepare_agent_config_dir).
    # Remaining providers get best-effort prompt prepending.
    instructions = overrides.instructions
    if (
        instructions
        and self.name not in {"claude", "codex"}
        and not self.uses_opencode_instructions
    ):
        prompt_parts.insert(0, instructions)

    return AgentRunConfig(
        model=model,
        max_turns=max_turns,
        timeout=timeout,
        prompt_extra="\n".join(prompt_parts),
        warnings=tuple(warnings),
    )

build_headless_command(*, 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]. Dispatches to provider-specific assembly: Claude routes through the shell wrapper (which adds --add-dir, git env); everything else uses the generic shape with subcommand + flags.

Source code in src/terok_executor/provider/providers.py
def build_headless_command(
    self,
    *,
    timeout: int,
    model: str | None = None,
    max_turns: int | None = None,
) -> str:
    """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]``.
    Dispatches to provider-specific assembly: Claude routes through the
    shell wrapper (which adds ``--add-dir``, git env); everything else
    uses the generic shape with subcommand + flags.
    """
    if self.name == "claude":
        return self._build_claude_command(timeout=timeout, model=model, max_turns=max_turns)
    return self._build_generic_command(timeout=timeout, model=model, max_turns=max_turns)

resolve_agent_value(key, config, agent_name)

Extract an agent-aware config value.

Supports two forms:

  • Flat valuemodel: opus → same for all agents.
  • Per-agent dictmodel: {claude: opus, codex: o3, _default: fast} → looks up agent_name, falls back to _default, then None.

Returns None when the key is absent or has no match for the agent.

Null override behaviour: when a per-agent dict maps an agent 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 an agent-specific value that a higher-priority layer can effectively unset by mapping it to null, letting the _default (or None) bubble up instead.

Internal to agent config resolution — full config-stack composition (build_agent_config_stack, resolve_agent_config) lives in terok, which owns the global/project/preset layer semantics.

Source code in src/terok_executor/provider/providers.py
def resolve_agent_value(
    key: str,
    config: dict[str, Any],
    agent_name: str,
) -> Any | None:
    """Extract an agent-aware config value.

    Supports two forms:

    * **Flat value** — ``model: opus`` → same for all agents.
    * **Per-agent dict** — ``model: {claude: opus, codex: o3, _default: fast}``
      → looks up *agent_name*, falls back to ``_default``, then ``None``.

    Returns ``None`` when the key is absent or has no match for the agent.

    **Null override behaviour**: when a per-agent dict maps an agent 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 an agent-specific value that a
    higher-priority layer can effectively *unset* by mapping it to ``null``,
    letting the ``_default`` (or ``None``) bubble up instead.

    Internal to agent config resolution — full config-stack composition
    (``build_agent_config_stack``, ``resolve_agent_config``) lives in terok,
    which owns the global/project/preset layer semantics.
    """
    val = config.get(key)
    if val is None:
        return None
    if isinstance(val, dict):
        agent_val = val.get(agent_name)
        if agent_val is not None:
            return agent_val
        return val.get("_default")
    return val

resolve_agent(agents, name, *, default_agent=None)

Look up an agent by name from agents, with fallback chain.

Resolution order: explicit namedefault_agent"claude". Raises SystemExit if the resolved name is not found.

Source code in src/terok_executor/provider/providers.py
def resolve_agent(
    agents: dict[str, Agent],
    name: str | None,
    *,
    default_agent: str | None = None,
) -> Agent:
    """Look up an agent by name from *agents*, with fallback chain.

    Resolution order: explicit *name* → *default_agent* → ``"claude"``.
    Raises ``SystemExit`` if the resolved name is not found.
    """
    resolved = name or default_agent or "claude"
    agent = agents.get(resolved)
    if agent is None:
        valid = ", ".join(sorted(agents))
        raise SystemExit(f"Unknown agent {resolved!r}. Valid agents: {valid}")
    return agent

get_agent(name, *, default_agent=None)

Resolve an agent name against the global AGENTS registry.

Convenience wrapper around resolve_agent.

Source code in src/terok_executor/provider/providers.py
def get_agent(name: str | None, *, default_agent: str | None = None) -> Agent:
    """Resolve an agent name against the global [`AGENTS`][terok_executor.provider.providers.AGENTS] registry.

    Convenience wrapper around [`resolve_agent`][terok_executor.provider.providers.resolve_agent].
    """
    return resolve_agent(AGENTS, name, default_agent=default_agent)