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.

Two template tokens are substituted into patch values:

  • {vault_url} — HTTP URL the container should reach the vault on.
  • {vault_socket} — filesystem path of a Unix socket the container can connect to for the vault.

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

Container-side addresses of the vault in both transports.

One or both fields are set depending on the active transport:

  • Socket mode: socket points at the mounted host socket; url points at the in-container TCP→UNIX loopback bridge for HTTP-only clients.
  • TCP mode: url points at host.containers.internal:<broker_port>; socket points at a local socat bridge that forwards to the same broker over TCP (for clients that can only speak HTTP-over-UNIX).

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.

write_vault_config(provider_name)

Apply shared_config_patch from the YAML roster after auth.

Patches a TOML or YAML config file in the provider's shared config dir to redirect API traffic through the vault. The patch spec is declared in the agent YAML — no provider-specific code needed.

Source code in src/terok_executor/credentials/vault_config.py
def write_vault_config(provider_name: str) -> None:
    """Apply ``shared_config_patch`` from the YAML roster after auth.

    Patches a TOML or YAML config file in the provider's shared config dir
    to redirect API traffic through the vault.  The patch spec is declared
    in the agent YAML — no provider-specific code needed.
    """
    from terok_executor.roster import AgentRoster

    roster = AgentRoster.shared()
    route = roster.vault_routes.get(provider_name)
    if not route or not route.shared_config_patch:
        return

    auth_info = roster.auth_providers.get(provider_name)
    if not auth_info:
        return

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

    # Shared-mount patches are host-singletons (one file per provider,
    # 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)

    patch = route.shared_config_patch
    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)

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,
) -> 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.
    """
    patched_routes = {
        name: route
        for name, route in roster.vault_routes.items()
        if route.shared_config_patch and (providers is None or name in providers)
    }
    disabled_routes = {
        name: route
        for name, route in roster.vault_routes.items()
        if route.shared_config_patch and disabled_providers and name in disabled_providers
    }

    for name in disabled_routes:
        auth_info = roster.auth_providers.get(name)
        if not auth_info:
            continue
        try:
            _remove_managed_patch_values(mounts_base / auth_info.host_dir_name, name)
            _logger.debug("Removed managed config patch for disabled provider %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_routes:
        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, route in patched_routes.items():
        auth_info = roster.auth_providers.get(name)
        if not auth_info:
            continue

        patch = route.shared_config_patch
        if patch is None:  # filtered out above; narrows for mypy
            continue
        try:
            shared_dir = mounts_base / 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:
                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). token_broker_port picks the socket-facade shape for socket-only clients: /run/terok/vault.sock in socket mode (the bind-mounted host socket), /tmp/terok-vault.sock in TCP mode (in-container socat unix→host-TCP).

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).  *token_broker_port* picks the socket-facade shape for
    socket-only clients: ``/run/terok/vault.sock`` in socket mode (the
    bind-mounted host socket), ``/tmp/terok-vault.sock`` in TCP mode
    (in-container socat unix→host-TCP).
    """
    from terok_executor.vault_addr import (
        CONTAINER_VAULT_SOCKET,
        LOOPBACK_BRIDGE_SOCKET,
        LOOPBACK_VAULT_PORT,
    )

    socket = LOOPBACK_BRIDGE_SOCKET if token_broker_port is not None else CONTAINER_VAULT_SOCKET
    return VaultLocation(
        url=f"http://localhost:{LOOPBACK_VAULT_PORT}",
        socket=socket,
    )