Skip to content

Providers

providers

Agent provider registry + per-provider behaviour.

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

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

AGENT_PROVIDERS = {} module-attribute

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

PROVIDER_NAMES = () module-attribute

OpenCodeProviderConfig(display_name, base_url, preferred_model, fallback_model, env_var_prefix, config_dir, auth_key_url) dataclass

Immutable descriptor for an OpenCode-based provider wrapper.

display_name instance-attribute

Human-readable display name (e.g., 'Helmholtz Blablador').

base_url instance-attribute

Base URL for the OpenAI-compatible API (e.g., 'https://api.helmholtz-blablador.fz-juelich.de/v1').

preferred_model instance-attribute

Preferred model ID (e.g., 'alias-huge').

fallback_model instance-attribute

Fallback model ID if preferred is unavailable (e.g., 'alias-code').

env_var_prefix instance-attribute

Environment variable prefix for API key (e.g., 'BLABLADOR' → BLABLADOR_API_KEY).

config_dir instance-attribute

Configuration directory name (e.g., '.blablador').

auth_key_url instance-attribute

URL where users can obtain API keys for documentation.

to_env(name)

Return env vars for container injection, keyed by TEROK_OC_{NAME}_*.

Source code in src/terok_executor/provider/providers.py
def to_env(self, name: str) -> dict[str, str]:
    """Return env vars for container injection, keyed by TEROK_OC_{NAME}_*."""
    prefix = f"TEROK_OC_{name.upper()}_"
    return {
        f"{prefix}BASE_URL": self.base_url,
        f"{prefix}PREFERRED_MODEL": self.preferred_model,
        f"{prefix}FALLBACK_MODEL": self.fallback_model,
        f"{prefix}DISPLAY_NAME": self.display_name,
        f"{prefix}ENV_VAR_PREFIX": self.env_var_prefix,
        f"{prefix}CONFIG_DIR": self.config_dir,
    }

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

Resolved per-run config for a headless provider.

Produced by AgentProvider.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.

AgentProvider(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, refuse_subcommands=()) 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 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.

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).

uses_opencode_instructions property

Whether the provider uses OpenCode's instruction system.

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,
) -> ProviderConfig:
    """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_provider_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_provider_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_provider_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)
    )

    # --- Subagents (warning only — filtering is handled elsewhere) ---
    subagents = config.get("subagents")
    if subagents and not self.supports_agents_json:
        warnings.append(
            f"{self.label} does not support sub-agents (--agents); "
            f"sub-agent definitions will be ignored"
        )

    # --- 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 ProviderConfig(
        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, --agents, 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``, ``--agents``, 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_provider_value(key, config, provider_name)

Extract a provider-aware config value.

Supports two forms:

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

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.

Internal to provider 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_provider_value(
    key: str,
    config: dict[str, Any],
    provider_name: str,
) -> Any | None:
    """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``, then ``None``.

    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.

    Internal to provider 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):
        provider_val = val.get(provider_name)
        if provider_val is not None:
            return provider_val
        return val.get("_default")
    return val

resolve_provider(providers, name, *, default_agent=None)

Look up a provider by name from providers, 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_provider(
    providers: dict[str, AgentProvider],
    name: str | None,
    *,
    default_agent: str | None = None,
) -> AgentProvider:
    """Look up a provider by name from *providers*, 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"
    provider = providers.get(resolved)
    if provider is None:
        valid = ", ".join(sorted(providers))
        raise SystemExit(f"Unknown provider {resolved!r}. Valid providers: {valid}")
    return provider

get_provider(name, *, default_agent=None)

Resolve a provider name against the global AGENT_PROVIDERS registry.

Convenience wrapper around resolve_provider.

Source code in src/terok_executor/provider/providers.py
def get_provider(name: str | None, *, default_agent: str | None = None) -> AgentProvider:
    """Resolve a provider name against the global [`AGENT_PROVIDERS`][terok_executor.provider.providers.AGENT_PROVIDERS] registry.

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

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_executor/provider/providers.py
def collect_opencode_provider_env() -> dict[str, str]:
    """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}_*.
    """
    env: dict[str, str] = {}
    for provider in AGENT_PROVIDERS.values():
        if provider.opencode_config is not None:
            env.update(provider.opencode_config.to_env(provider.name))
    return env