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