Skip to content

Vault config

vault_config

Patches provider config files to route API traffic through the vault.

Applies shared_config_patch from the YAML roster after authentication and — crucially — on every task start. Writes vault URLs / socket paths (not secrets) to provider config files so agents route traffic through the vault instead of hitting upstream directly with phantom tokens.

Patch values are Jinja templates over three variables:

  • {{ vault_url }} — HTTP URL the container should reach the vault on.
  • {{ vault_tls_url }} — the same vault over TLS, for clients that refuse plain HTTP.
  • {{ vault_socket }} — filesystem path of a Unix socket the container can connect to for the vault.

Any other name fails the patch rather than landing in the config verbatim.

The concrete values are mode-dependent (socket vs TCP transport) and resolved centrally — agent YAMLs only need to reference the tokens.

ConfigPatchError

Bases: RuntimeError

Raised when a shared config patch fails and the task must not start.

VaultLocation(url, socket, tls_url) dataclass

Vault addresses that the resolver returns for container clients.

The resolver returns every field for every transport. The url field is the loopback URL, and tls_url the TLS bridge in front of it. The socket field is LOOPBACK_BRIDGE_SOCKET.

url instance-attribute

Base URL an in-container HTTP client should use (always non-empty).

socket instance-attribute

Filesystem path for a Unix-socket-speaking HTTP client.

tls_url instance-attribute

Base URL for a client that insists on https — the TLS bridge in front of url.

write_vault_config(name, credential_set='default')

Apply an entry's provider.config_patch from the YAML roster after auth.

Patches a TOML or YAML config file in the entry's shared config dir to redirect API traffic through the vault. name is the auth-provider name (agent OR tool — e.g. gh); the patch rides on the entry's vault route (sourced from its provider: binding). No provider-specific code needed. credential_set selects which stored credential's type drives a by_credential_type overlay (codex's ChatGPT-backend vs /v1 routing).

Source code in src/terok_executor/credentials/vault_config.py
def write_vault_config(name: str, credential_set: str = "default") -> None:
    """Apply an entry's ``provider.config_patch`` from the YAML roster after auth.

    Patches a TOML or YAML config file in the entry's shared config dir to
    redirect API traffic through the vault.  *name* is the auth-provider name
    (agent OR tool — e.g. ``gh``); the patch rides on the entry's vault route
    (sourced from its ``provider:`` binding).  No provider-specific code needed.
    *credential_set* selects which stored credential's type drives a
    ``by_credential_type`` overlay (codex's ChatGPT-backend vs ``/v1`` routing).
    """
    from terok_executor.roster import AgentRoster

    roster = AgentRoster.shared()
    auth_info = roster.auth_providers.get(name)
    if not auth_info:
        return
    cred_key = auth_info.credential_provider or name
    route = roster.vault_routes.get(cred_key)
    patch = route.shared_config_patch if route else None
    if not patch:
        return
    patch = _resolve_patch(patch, route, cred_key, credential_set)

    from terok_executor.integrations.sandbox import SandboxConfig
    from terok_executor.paths import mounts_dir

    # Shared-mount patches are host-singletons (one file per agent, mounted
    # into every container).  Use the cfg's singleton broker port —
    # per-container ports would need per-container config files, which is a
    # deeper refactor.
    location = resolve_vault_location(SandboxConfig().token_broker_port)

    shared_dir = mounts_dir() / auth_info.host_dir_name
    shared_dir.mkdir(parents=True, exist_ok=True)
    config_path = _safe_config_path(shared_dir, patch["file"])

    if "yaml_set" in patch:
        _apply_yaml_patch(config_path, patch, location)
    elif "toml_set" in patch:
        _apply_toml_patch(config_path, patch, location)

    print(f"Vault config written to {config_path}")

apply_shared_config_patches(roster, mounts_base, *, providers=None, disabled_providers=None, credential_set='default')

Reconcile shared_config_patch for enabled and disabled providers.

Called during task start so shared mount directories (which may have been recreated empty) always contain the correct vault addresses. Idempotent: safe to call on every launch. Disabled providers have previously managed values removed only when the live config still matches the sidecar value terok wrote last time; user-edited values are preserved and ownership is dropped.

Parameters:

Name Type Description Default
roster AgentRoster

Loaded agent roster.

required
mounts_base Path

Shared config mount root.

required
providers frozenset[str] | None

None means "all providers with a patch". An empty set disables patching entirely. A non-empty set restricts patching to that provider subset.

None
disabled_providers frozenset[str] | None

Provider subset whose previously managed patch values should be reconciled away. None removes nothing; callers pass an explicit set when a feature mode disables provider routing.

None

Raises ConfigPatchError on failure — callers must not start the container if vault routing cannot be established.

Source code in src/terok_executor/credentials/vault_config.py
def apply_shared_config_patches(
    roster: AgentRoster,
    mounts_base: Path,
    *,
    providers: frozenset[str] | None = None,
    disabled_providers: frozenset[str] | None = None,
    credential_set: str = "default",
) -> None:
    """Reconcile ``shared_config_patch`` for enabled and disabled providers.

    Called during task start so shared mount directories (which may have
    been recreated empty) always contain the correct vault addresses.
    Idempotent: safe to call on every launch.  Disabled providers have
    previously managed values removed only when the live config still
    matches the sidecar value terok wrote last time; user-edited values
    are preserved and ownership is dropped.

    Args:
        roster: Loaded agent roster.
        mounts_base: Shared config mount root.
        providers:
            ``None`` means "all providers with a patch".  An empty set
            disables patching entirely.  A non-empty set restricts
            patching to that provider subset.
        disabled_providers:
            Provider subset whose previously managed patch values should
            be reconciled away.  ``None`` removes nothing; callers pass an
            explicit set when a feature mode disables provider routing.

    Raises [`ConfigPatchError`][terok_executor.credentials.vault_config.ConfigPatchError] on failure — callers must not start
    the container if vault routing cannot be established.
    """
    # Config patches are a delivery concern keyed by the *auth-provider* name —
    # agent OR tool (tools like ``gh`` aren't in ``roster.agents``, so iterating
    # agents would silently skip their patches).  The patch rides on the entry's
    # vault route (sourced from its provider binding); apply it to the entry's
    # own shared config dir.  ``providers`` / ``disabled_providers`` filter by
    # that name.
    all_patches: dict[str, tuple[str, dict]] = {}
    for name, auth_info in roster.auth_providers.items():
        route = roster.vault_routes.get(auth_info.credential_provider or name)
        patch = route.shared_config_patch if route else None
        if patch:
            all_patches[name] = (auth_info.host_dir_name, patch)

    disabled = {
        name: spec
        for name, spec in all_patches.items()
        if disabled_providers and name in disabled_providers
    }
    patched = {
        name: spec
        for name, spec in all_patches.items()
        if (providers is None or name in providers)
        and not (disabled_providers and name in disabled_providers)
    }

    for name, (host_dir, _patch) in disabled.items():
        try:
            _remove_managed_patch_values(mounts_base / host_dir, name)
            _logger.debug("Removed managed config patch for disabled agent %s", name)
        except ConfigPatchError:
            raise
        except Exception as exc:
            raise ConfigPatchError(f"Failed to remove vault config patch for {name}") from exc

    if not patched:
        return

    from terok_executor.integrations.sandbox import SandboxConfig

    # See note in ``write_vault_config`` — shared mounts force a
    # singleton port here regardless of per-container allocation.
    location = resolve_vault_location(SandboxConfig().token_broker_port)

    for name, (host_dir, patch) in patched.items():
        try:
            cred_key = roster.auth_providers[name].credential_provider or name
            route = roster.vault_routes.get(cred_key)
            if route is not None:
                patch = _resolve_patch(patch, route, cred_key, credential_set)
            shared_dir = mounts_base / host_dir
            shared_dir.mkdir(parents=True, exist_ok=True)
            config_path = _safe_config_path(shared_dir, patch["file"])

            if "yaml_set" in patch:
                records = _apply_yaml_patch(config_path, patch, location)
            elif "toml_set" in patch:
                records = _apply_toml_patch(config_path, patch, location)
            else:
                records = []
            _record_managed_patch_values(shared_dir, name, patch["file"], records)
            _logger.debug("Applied config patch for %s → %s", name, config_path)
        except ConfigPatchError:
            raise
        except Exception as exc:
            raise ConfigPatchError(
                f"Failed to apply vault config patch for {name} (file={patch.get('file')!r})"
            ) from exc

resolve_vault_location(token_broker_port=None)

Return the in-container vault address.

URL is always the loopback bridge on LOOPBACK_VAULT_PORT — the bridge runs in both transports and forwards to the transport-specific target (host unix socket or per-container host TCP port). The socket is always the bridge-owned /tmp/terok-vault.sock: a real socat socket in TCP mode, a link to this generation's mounted host socket in socket mode. One generation-independent path, so the shared singleton agent configs stay valid when the mount layout changes between container generations. token_broker_port is accepted for the callers that already resolved the transport; both shapes now share the address.

Source code in src/terok_executor/credentials/vault_config.py
def resolve_vault_location(token_broker_port: int | None = None) -> VaultLocation:
    """Return the in-container vault address.

    URL is always the loopback bridge on
    [`LOOPBACK_VAULT_PORT`][terok_executor.vault_addr.LOOPBACK_VAULT_PORT]
    — the bridge runs in both transports and forwards to the
    transport-specific target (host unix socket or per-container host
    TCP port).  The socket is always the bridge-owned
    ``/tmp/terok-vault.sock``: a real socat socket in TCP mode, a link
    to this generation's mounted host socket in socket mode.  One
    generation-independent path, so the shared singleton agent configs
    stay valid when the mount layout changes between container
    generations.  *token_broker_port* is accepted for the callers that
    already resolved the transport; both shapes now share the address.
    """
    from terok_executor.vault_addr import (
        LOOPBACK_BRIDGE_SOCKET,
        LOOPBACK_VAULT_PORT,
        LOOPBACK_VAULT_TLS_PORT,
    )

    del token_broker_port  # both transports share the bridge-owned address
    return VaultLocation(
        url=f"http://localhost:{LOOPBACK_VAULT_PORT}",
        tls_url=f"https://localhost:{LOOPBACK_VAULT_TLS_PORT}",
        socket=LOOPBACK_BRIDGE_SOCKET,
    )