Skip to content

config

config

Sandbox configuration — plain dataclass for standalone and embedded use.

SandboxConfig captures directory paths and settings that sandbox modules need. In standalone terok-sandbox use, it is resolved from environment variables and XDG defaults. When embedded in terok, the orchestration layer constructs it from core.config values.

CONTAINER_RUNTIME_DIR = '/run/terok' module-attribute

Container-side mount point for the host runtime directory (socket mode).

SandboxConfig(state_dir=_state_root(), runtime_dir=_runtime_root(), config_dir=_config_root(), vault_dir=_vault_root(), gate_port=_default_gate_port(), token_broker_port=_default_token_broker_port(), ssh_signer_port=_default_ssh_signer_port(), shield_profiles=('dev-standard',), shield_audit=_default_shield_audit(), shield_bypass=False, credentials_passphrase=_default_credentials_passphrase(), credentials_use_keyring=_default_credentials_use_keyring(), credentials_passphrase_command=_default_credentials_passphrase_command(), services_mode=_default_services_mode(), experimental=_default_experimental()) dataclass

Immutable configuration for the sandbox layer.

All paths default to the XDG/FHS-resolved values from paths. Override individual fields when constructing from terok's global config or when using terok-sandbox standalone.

state_dir = field(default_factory=_state_root) class-attribute instance-attribute

Writable state root (tokens, gate repos, task data).

runtime_dir = field(default_factory=_runtime_root) class-attribute instance-attribute

Transient runtime directory (PID files, sockets).

config_dir = field(default_factory=_config_root) class-attribute instance-attribute

Sandbox-scoped configuration root.

Note: shield profiles are resolved by shield_profiles_dir via namespace_config_root, not from this directory.

vault_dir = field(default_factory=_vault_root) class-attribute instance-attribute

Shared vault directory (DB, routes, env mounts).

gate_port = field(default_factory=_default_gate_port) class-attribute instance-attribute

HTTP port for the gate server (None = auto-allocate via registry).

Default-factory reads gate_server.port from config.yml; missing or unset keys fall through to None so the port registry can pick one. Direct SandboxConfig(gate_port=…) always wins.

token_broker_port = field(default_factory=_default_token_broker_port) class-attribute instance-attribute

TCP port for the vault's token broker (None = auto-allocate via registry).

Default-factory reads vault.port from config.yml.

ssh_signer_port = field(default_factory=_default_ssh_signer_port) class-attribute instance-attribute

TCP port for the vault's SSH signer (None = auto-allocate via registry).

Default-factory reads vault.ssh_signer_port from config.yml.

shield_profiles = ('dev-standard',) class-attribute instance-attribute

Shield egress firewall profile names.

shield_audit = field(default_factory=_default_shield_audit) class-attribute instance-attribute

Whether shield audit logging is enabled.

Default-factory reads shield.audit from the layered config.yml via the RawShieldSection schema; missing/typo'd keys fall back to the schema's True default. Direct SandboxConfig(shield_audit=…) always wins.

shield_bypass = False class-attribute instance-attribute

DANGEROUS: when True, the egress firewall is completely disabled.

Hardcoded False here — sandbox refuses to read this field from config.yml because the layered chain includes a user-writable scope (~/.config/terok/config.yml) and an $ENV-controllable override (TEROK_CONFIG_FILE), so anything that drops a file in $HOME could silently disable the egress firewall. Orchestrators that want bypass must pass it explicitly to SandboxConfig(shield_bypass=True) after resolving from their own trusted source.

credentials_passphrase = field(default_factory=_default_credentials_passphrase) class-attribute instance-attribute

Headless-no-keyring fallback for the SQLCipher passphrase.

Read from credentials.passphrase in config.yml at construct time. None (the default) means "no config-file fallback set" — callers fall through to the next tier in the resolution chain.

credentials_use_keyring = field(default_factory=_default_credentials_use_keyring) class-attribute instance-attribute

Opt-in switch for the OS keyring tier in the passphrase resolution chain.

Off by default. Linux Secret Service has per-collection (not per-item) ACLs, so authorising terok against the default collection grants read access to every other secret stored there. Operators opt in via terok setup after weighing that trade-off.

credentials_passphrase_command = field(default_factory=_default_credentials_passphrase_command) class-attribute instance-attribute

Operator-supplied shell command that prints the SQLCipher passphrase on stdout.

Resolver tier slotted between keyring and config. Canonical headless option for hosts without systemd ≥ 257 — same shape as git config credential.helper or BORG_PASSCOMMAND. Read from credentials.passphrase_command in config.yml at construct time; None (the default) means "no helper configured" and the resolver skips this tier.

services_mode = field(default_factory=_default_services_mode) class-attribute instance-attribute

Transport for host↔container IPC, resolved once at construction.

Validated through the same RawServicesSection schema terok's RawGlobalConfig composes, so standalone and embedded paths agree on the value. Lives as an instance attribute rather than a free-function call per site so downstream code can't bypass config resolution — no manager without a SandboxConfig, every SandboxConfig carries a resolved mode.

experimental = field(default_factory=_default_experimental) class-attribute instance-attribute

Whether the ecosystem-wide experimental: opt-in is on.

Cross-package switch: gates terok's krun runtime at task launch and sandbox's krun-only prereq probes (currently just ip) at terok-sandbox setup. Read from the top-level experimental: key in the layered config.yml at construct time; missing / typo'd values fall back to False. Direct SandboxConfig(experimental=…) always wins.

gate_base_path property

Return the gate server's repo base path.

shield_profiles_dir property

Return the directory for terok-managed shield profiles.

db_path property

Return the path to the vault sqlite3 database.

vault_socket_path property

Return the Unix socket path for the vault.

vault_pid_path property

Return the PID file path for the managed vault daemon.

vault_passphrase_file property

Return the session-unlock tmpfs path for the SQLCipher passphrase.

Lives under runtime_dir ($XDG_RUNTIME_DIR/...), so it is RAM-backed and cleared on reboot. Written by terok-sandbox vault unlock; read at daemon startup as the highest-priority tier of the passphrase resolution chain.

vault_systemd_creds_file property

Return the sealed-credential path for the systemd-creds tier.

Lives under vault_dir (persistent state, 0o600) — the credential is machine-bound (TPM2 or host key), so persistence across reboots is the whole point. Written by terok-sandbox vault seal; read on every chain walk via terok_sandbox.vault.store.systemd_creds.

vault_recovery_marker_file property

Return the sidecar marker path for "operator saved the recovery passphrase".

Lives next to the sealed-credential file (persistent state, 0o600). Contents are the SHA-256 fingerprint of the acknowledged passphrase, so a re-key invalidates the marker and re-prompts on the next surface that reads it (terok_sandbox.vault.store.recovery).

routes_path property

Return the path to the vault route configuration JSON.

credential_audit_log_path property

Return the path to the credential-use audit JSONL.

One file under the vault state dir, shared across every subject the broker has ever served — sandbox doesn't model "subject" semantically, so per-subject layout is the consumer's concern (terok's review CLI filters by scope / subject).

ssh_signer_socket_path property

Return the Unix socket path for the vault's SSH signer.

The vault binds this socket and serves the SSH-agent protocol on it (clients use it as $SSH_AUTH_SOCK). Filename uses the protocol name so its purpose is recognisable to anyone tracing socket activity.

clone_cache_base_path property

Return the base directory for per-scope non-bare clone caches.

ssh_keys_dir property

Return the base directory for per-scope SSH keys.

with_resolved_ports()

Return a copy with TCP ports allocated via the shared port registry.

Idempotent — returns self (no copy) when there is nothing to allocate: socket mode never needs TCP listeners, and already-fully-resolved cfgs short-circuit.

Side-effectful: allocation hits the shared port registry, bind-tests each candidate, and persists the claim to state_dir/port-claims.json. Keep this call OUT of construction paths that don't actually launch services (sickbay checks, config inspection, tests) — that's why it's opt-in rather than baked into __post_init__. The consumers that do need real ports (ShieldManager, Sandbox) wrap their stored cfg in self._cfg = self._cfg.with_resolved_ports() at construction time so downstream code never sees None for the port it needs.

Source code in src/terok_sandbox/config.py
def with_resolved_ports(self) -> SandboxConfig:
    """Return a copy with TCP ports allocated via the shared port registry.

    Idempotent — returns ``self`` (no copy) when there is nothing
    to allocate: socket mode never needs TCP listeners, and
    already-fully-resolved cfgs short-circuit.

    **Side-effectful**: allocation hits the shared port registry,
    bind-tests each candidate, and persists the claim to
    ``state_dir/port-claims.json``.  Keep this call OUT of
    construction paths that don't actually launch services
    (sickbay checks, config inspection, tests) — that's why it's
    opt-in rather than baked into ``__post_init__``.  The
    consumers that *do* need real ports (``ShieldManager``,
    ``Sandbox``) wrap their stored cfg in
    ``self._cfg = self._cfg.with_resolved_ports()`` at construction
    time so downstream code never sees ``None`` for the port it
    needs.
    """
    if self.services_mode == "socket":
        return self
    if (
        self.gate_port is not None
        and self.token_broker_port is not None
        and self.ssh_signer_port is not None
    ):
        return self
    from dataclasses import replace

    from .port_registry import resolve_service_ports

    ports = resolve_service_ports(
        self.gate_port,
        self.token_broker_port,
        self.ssh_signer_port,
        gate_explicit=self.gate_port is not None,
        proxy_explicit=self.token_broker_port is not None,
        ssh_explicit=self.ssh_signer_port is not None,
        state_dir=self.state_dir,
    )
    return replace(
        self,
        gate_port=self.gate_port if self.gate_port is not None else ports.gate,
        token_broker_port=(
            self.token_broker_port if self.token_broker_port is not None else ports.proxy
        ),
        ssh_signer_port=(
            self.ssh_signer_port if self.ssh_signer_port is not None else ports.ssh_agent
        ),
    )

open_credential_db(db_path=None, *, prompt_on_tty=False)

Open the credentials DB with this config's resolution-chain knobs.

Single seam over open_credential_db so call sites never plumb tier-selection kwargs by hand — adding a new tier is one entry in the private _chain_kwargs helper, no cross-package fan-out.

db_path defaults to self.db_path; callers that already hold a path (a sidecar-pinned DB path, or a test override) pass it explicitly so the open targets that DB while still using this config's tier policy. CLI consumers pass prompt_on_tty=True to unlock the interactive fallback; the per-container supervisor leaves it off.

Source code in src/terok_sandbox/config.py
def open_credential_db(
    self, db_path: Path | None = None, *, prompt_on_tty: bool = False
) -> Any:
    """Open the credentials DB with this config's resolution-chain knobs.

    Single seam over [`open_credential_db`][terok_sandbox.vault.store.db.open_credential_db]
    so call sites never plumb tier-selection kwargs by hand — adding
    a new tier is one entry in the private ``_chain_kwargs`` helper,
    no cross-package fan-out.

    *db_path* defaults to ``self.db_path``; callers that already
    hold a path (a sidecar-pinned DB path, or a test override) pass
    it explicitly so the open targets that DB while still using
    this config's tier policy.  CLI consumers pass
    ``prompt_on_tty=True`` to unlock the interactive fallback;
    the per-container supervisor leaves it off.
    """
    from .vault.store.db import open_credential_db  # noqa: PLC0415

    return open_credential_db(
        db_path if db_path is not None else self.db_path,
        **self._chain_kwargs(prompt_on_tty=prompt_on_tty),
    )

open_credential_db_with_source(db_path=None, *, prompt_on_tty=False)

Same as open_credential_db but also returns which tier of the chain hit.

db_path override semantics match open_credential_db. The returned source lets callers (status reports, the supervisor startup log) name which tier unlocked the vault instead of second-guessing the resolver.

Source code in src/terok_sandbox/config.py
def open_credential_db_with_source(
    self, db_path: Path | None = None, *, prompt_on_tty: bool = False
) -> tuple[CredentialDB, PassphraseSource]:
    """Same as [`open_credential_db`][terok_sandbox.SandboxConfig.open_credential_db]
    but also returns which tier of the chain hit.

    *db_path* override semantics match
    [`open_credential_db`][terok_sandbox.SandboxConfig.open_credential_db].
    The returned source lets callers (status reports, the
    supervisor startup log) name which tier unlocked the vault
    instead of second-guessing the resolver.
    """
    from .vault.store.db import open_credential_db_with_source  # noqa: PLC0415

    return open_credential_db_with_source(
        db_path if db_path is not None else self.db_path,
        **self._chain_kwargs(prompt_on_tty=prompt_on_tty),
    )

open_sqlcipher_connection(db_path=None, **connect_kwargs)

Open a raw sqlcipher3 connection via the chain (vault daemon path).

Source code in src/terok_sandbox/config.py
def open_sqlcipher_connection(self, db_path: Path | None = None, **connect_kwargs: Any) -> Any:
    """Open a raw sqlcipher3 connection via the chain (vault daemon path)."""
    from .vault.store.encryption import open_sqlcipher_via_chain  # noqa: PLC0415

    return open_sqlcipher_via_chain(
        db_path or self.db_path,
        **self._chain_kwargs(prompt_on_tty=False),
        **connect_kwargs,
    )

resolve_passphrase(*, prompt_on_tty=False)

Walk the resolution chain with this config's knobs; return the passphrase or None.

Diagnostic seam — never opens the DB. Used by host-side doctor / sickbay and by vault seal to reuse whatever tier currently has the key. Same chain order as open_credential_db because both delegate here.

Source code in src/terok_sandbox/config.py
def resolve_passphrase(self, *, prompt_on_tty: bool = False) -> str | None:
    """Walk the resolution chain with this config's knobs; return the passphrase or ``None``.

    Diagnostic seam — never opens the DB.  Used by host-side
    doctor / sickbay and by ``vault seal`` to reuse whatever tier
    currently has the key.  Same chain order as
    [`open_credential_db`][terok_sandbox.SandboxConfig.open_credential_db]
    because both delegate here.
    """
    from .vault.store.encryption import resolve_passphrase  # noqa: PLC0415

    return resolve_passphrase(**self._chain_kwargs(prompt_on_tty=prompt_on_tty))

resolve_passphrase_with_source(*, prompt_on_tty=False)

Walk the resolution chain with this config's knobs; return (passphrase, source).

Diagnostic counterpart to resolve_passphrase — feeds the daemon startup log so the operator sees which tier unlocked the vault on this boot.

Source code in src/terok_sandbox/config.py
def resolve_passphrase_with_source(
    self, *, prompt_on_tty: bool = False
) -> tuple[str | None, PassphraseSource | None]:
    """Walk the resolution chain with this config's knobs; return ``(passphrase, source)``.

    Diagnostic counterpart to
    [`resolve_passphrase`][terok_sandbox.SandboxConfig.resolve_passphrase]
    — feeds the daemon startup log so the operator sees *which*
    tier unlocked the vault on this boot.
    """
    from .vault.store.encryption import resolve_passphrase_with_source  # noqa: PLC0415

    return resolve_passphrase_with_source(**self._chain_kwargs(prompt_on_tty=prompt_on_tty))

ssh_signer_local_socket_path(scope)

Return the per-scope vault SSH-agent socket path for scope.

The vault binds one 0600 Unix socket per scope with at least one assigned key, under the same runtime_dir as the main signer. Host-side gate-sync points SSH_AUTH_SOCK at this path.

Rejects unsafe scope names with InvalidScopeName as a belt-and-braces guard — writers in the DB layer enforce the same policy, but the socket path is public API and may be called without a preceding DB write.

Source code in src/terok_sandbox/config.py
def ssh_signer_local_socket_path(self, scope: str) -> Path:
    """Return the per-scope vault SSH-agent socket path for *scope*.

    The vault binds one 0600 Unix socket per scope with at least one
    assigned key, under the same ``runtime_dir`` as the main signer.
    Host-side ``gate-sync`` points ``SSH_AUTH_SOCK`` at this path.

    Rejects unsafe scope names with [`InvalidScopeName`][terok_sandbox.vault.store.db.InvalidScopeName]
    as a belt-and-braces guard — writers in the DB layer enforce the
    same policy, but the socket path is public API and may be called
    without a preceding DB write.
    """
    from .vault.store.db import _require_safe_scope

    _require_safe_scope(scope)
    return self.runtime_dir / f"ssh-agent-local-{scope}.sock"

services_mode()

Resolve the services.mode setting through sandbox's own pydantic schema.

Source code in src/terok_sandbox/config.py
def services_mode() -> ServicesMode:
    """Resolve the ``services.mode`` setting through sandbox's own pydantic schema."""
    from .config_schema import RawServicesSection

    return _validate_section(RawServicesSection, "services").mode

credentials_passphrase()

Resolve the credentials.passphrase headless fallback through the schema.

Source code in src/terok_sandbox/config.py
def credentials_passphrase() -> str | None:
    """Resolve the ``credentials.passphrase`` headless fallback through the schema."""
    return _credentials_section().passphrase

credentials_use_keyring()

Resolve the credentials.use_keyring opt-in flag through the schema.

Source code in src/terok_sandbox/config.py
def credentials_use_keyring() -> bool:
    """Resolve the ``credentials.use_keyring`` opt-in flag through the schema."""
    return _credentials_section().use_keyring

credentials_passphrase_command()

Resolve the credentials.passphrase_command shell-helper recipe through the schema.

Source code in src/terok_sandbox/config.py
def credentials_passphrase_command() -> str | None:
    """Resolve the ``credentials.passphrase_command`` shell-helper recipe through the schema."""
    return _credentials_section().passphrase_command

shield_audit()

Resolve the shield.audit setting through the schema.

Source code in src/terok_sandbox/config.py
def shield_audit() -> bool:
    """Resolve the ``shield.audit`` setting through the schema."""
    return _shield_section().audit

experimental_enabled()

Resolve the top-level experimental: opt-in from the layered config.

Ecosystem-wide flag: shared between sandbox (krun host-binary prereqs), executor (krun runtime construction), and terok (krun runtime selection at task launch). Defaults to False when the key is absent or malformed.

Source code in src/terok_sandbox/config.py
def experimental_enabled() -> bool:
    """Resolve the top-level ``experimental:`` opt-in from the layered config.

    Ecosystem-wide flag: shared between sandbox (krun host-binary
    prereqs), executor (krun runtime construction), and terok (krun
    runtime selection at task launch).  Defaults to ``False`` when the
    key is absent or malformed.
    """
    raw = read_config_top_level("experimental")
    if isinstance(raw, bool):
        return raw
    return False

gate_server_port()

Resolve gate_server.port through the schema; None = auto-allocate.

Source code in src/terok_sandbox/config.py
def gate_server_port() -> int | None:
    """Resolve ``gate_server.port`` through the schema; ``None`` = auto-allocate."""
    return _gate_server_section().port

vault_token_broker_port()

Resolve vault.port through the schema; None = auto-allocate.

Source code in src/terok_sandbox/config.py
def vault_token_broker_port() -> int | None:
    """Resolve ``vault.port`` through the schema; ``None`` = auto-allocate."""
    return _vault_section().port

vault_ssh_signer_port()

Resolve vault.ssh_signer_port through the schema; None = auto-allocate.

Source code in src/terok_sandbox/config.py
def vault_ssh_signer_port() -> int | None:
    """Resolve ``vault.ssh_signer_port`` through the schema; ``None`` = auto-allocate."""
    return _vault_section().ssh_signer_port