config_schema
config_schema
¶
Pydantic schema for the sandbox-owned slice of the shared config.yml.
The ecosystem uses one ~/.config/terok/config.yml file shared across
every package (Podman model — see terok_sandbox.paths for the
prior decision around umbrella roots). Each package owns the schema
for the sub-sections it consumes; higher-level packages compose the
full file by importing from their dependencies.
This module is sandbox's contribution: the nine top-level sections
sandbox actually reads (paths, credentials, vault,
gate_server, services, shield, network, ssh,
run), each strict on its own keys (extra="forbid"), wrapped
in SandboxConfigView
whose top level is tolerant (extra="allow") so unknown
sections — those owned by terok-executor or terok — pass through
silently when sandbox is run standalone.
Validation strategy:
- Owned sub-sections are strict. A typo inside
paths.roootis rejected at load time with a clear pydantic error. - Unknown top-level sections are tolerated. Sandbox doesn't know
about terok's
tui:or executor'simage:; rejecting them would make the standalonepython -m terok_sandboxflow crash on any complete ecosystem config.
Higher-level packages inherit from SandboxConfigView and add
their own sections. The topmost layer (terok) flips back to
extra="forbid" because it knows every section in the v0 ecosystem.
ServicesMode = Literal['tcp', 'socket']
module-attribute
¶
Type alias for the services.mode Literal; re-exported from
RawServicesSection.model_fields['mode'] so downstream modules
(sandbox's SandboxConfig, terok's
make_sandbox_config) can annotate without re-declaring the shape.
SERVICES_TCP_OPTOUT_YAML = 'services: {mode: tcp}'
module-attribute
¶
User-facing opt-out snippet shown in SELinux hints — keep in one place so setup, sickbay, tests and docs stay in sync.
__all__ = ['SERVICES_TCP_OPTOUT_YAML', 'RawCredentialsSection', 'RawGateServerSection', 'RawNetworkSection', 'RawPathsSection', 'RawSSHSection', 'RawServicesSection', 'RawShieldSection', 'RawVaultSection', 'SandboxConfigView', 'ServicesMode', 'gate_use_personal_ssh_default']
module-attribute
¶
RawCredentialsSection
¶
Bases: BaseModel
The credentials: section — vault routing for proxy DB and agent mounts.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
dir = Field(default=None, description='Shared credentials directory (proxy DB, agent config mounts)')
class-attribute
instance-attribute
¶
passphrase = Field(default=None, description='Unsafe headless fallback for the SQLCipher passphrase; only set when no OS keyring or systemd-creds is available.')
class-attribute
instance-attribute
¶
use_keyring = Field(default=False, description='Opt-in switch for the OS keyring tier of the passphrase resolution chain. Off by default because Linux Secret Service has per-collection (not per-item) ACLs.')
class-attribute
instance-attribute
¶
passphrase_command = Field(default=None, description='Operator-supplied shell command (e.g. ``pass show terok-sandbox/vault-passphrase``) that prints the SQLCipher passphrase on stdout. Tokenised with ``shlex.split``; resolver tier slots between OS keyring and the plaintext config-file fallback. Canonical headless option for hosts without systemd ≥ 257 — covers ``pass``, ``bw``, ``op``, HashiCorp ``vault``, and the cloud secret-manager CLIs (AWS, GCP, Azure).')
class-attribute
instance-attribute
¶
RawPathsSection
¶
Bases: BaseModel
The paths: section — umbrella state root and per-purpose overrides.
root is the namespace state root read by every ecosystem package
(Podman model — see also terok_sandbox.paths.umbrella_state_dir).
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
root = Field(default=None, description='Namespace state root shared by all ecosystem packages (Podman model — one config, multiple readers)')
class-attribute
instance-attribute
¶
build_dir = Field(default=None, description='Build artifacts directory (generated Dockerfiles)')
class-attribute
instance-attribute
¶
sandbox_live_dir = Field(default=None, description='Container-writable runtime data (tasks, agent mounts). For hardened installs, mount the target with ``noexec,nosuid,nodev``')
class-attribute
instance-attribute
¶
user_projects_dir = Field(default=None, description='User projects directory (per-user project configs)')
class-attribute
instance-attribute
¶
user_presets_dir = Field(default=None, description='User presets directory (per-user preset configs)')
class-attribute
instance-attribute
¶
port_registry_dir = Field(default=None, description='Shared port registry directory for multi-user isolation')
class-attribute
instance-attribute
¶
RawShieldSection
¶
Bases: BaseModel
The shield: section — egress firewall policy + audit + task lifecycle defaults.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
bypass_firewall_no_protection = Field(default=False, description='**Dangerous**: disable egress firewall entirely')
class-attribute
instance-attribute
¶
profiles = Field(default=None, description='Named shield profiles for per-project firewall rules')
class-attribute
instance-attribute
¶
audit = Field(default=True, description='Enable shield audit logging')
class-attribute
instance-attribute
¶
drop_on_task_run = True
class-attribute
instance-attribute
¶
on_task_restart = 'retain'
class-attribute
instance-attribute
¶
RawServicesSection
¶
Bases: BaseModel
The services: section — transport mode for host ↔ container IPC.
RawVaultSection
¶
Bases: BaseModel
The vault: section — token broker and SSH signer ports.
The container-side transport was previously configured via
vault.transport; since 0.7.4 it is derived from
services.mode so the two knobs stay in lockstep (tcp listener
↔ direct routing, socket listener ↔ socket routing). Any prior
vault.transport: line in config.yml must be removed.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
bypass_no_secret_protection = False
class-attribute
instance-attribute
¶
port = Field(default=None, ge=1, le=65535)
class-attribute
instance-attribute
¶
ssh_signer_port = Field(default=None, ge=1, le=65535)
class-attribute
instance-attribute
¶
RawGateServerSection
¶
Bases: BaseModel
The gate_server: section — host-side gate listen port + repo dir.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
port = Field(default=None, ge=1, le=65535, description='Gate server listen port')
class-attribute
instance-attribute
¶
repos_dir = Field(default=None, description='Override gate repo directory (default: ``state_dir/gate``)')
class-attribute
instance-attribute
¶
suppress_systemd_warning = Field(default=False, description='Suppress the systemd unit installation suggestion')
class-attribute
instance-attribute
¶
RawNetworkSection
¶
Bases: BaseModel
The network: section — port range for service / container ports.
RawSSHSection
¶
Bases: BaseModel
The ssh: section — auth strategy for the host-side gate.
Default is None (not False) so model_dump(exclude_none=True)
can distinguish unset from explicitly false. Higher layers may
layer this with a project.yml ssh: section of the same shape;
the None sentinel keeps the project layer from stomping the
global value when the user omits it. The effective False default
happens at the consumer end.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
use_personal = Field(default=None, description="Opt in to the user's ``~/.ssh`` keys for host-side ``gate-sync``. Default ``false`` — terok uses only its vault-managed key. Resolves through ConfigStack: ``terok-global config.yml`` → ``project.yml`` → CLI ``--use-personal-ssh`` (highest).")
class-attribute
instance-attribute
¶
RawHooksSection
¶
Bases: BaseModel
Task lifecycle hook commands.
Run on the host (not inside the container) around container lifecycle events. Sandbox owns them because the lifecycle events themselves are sandbox-mediated — the orchestrator just opts into being notified. The four hook points map to sandbox-internal transitions:
pre_start: before the container exists (host-side prep).post_start: after the container is created but possibly not ready.post_ready: after the readiness marker has been observed.post_stop: after the container has stopped (cleanup hook).
Each value is a shell command string, run by the host shell with
the orchestrator's environment. None means no hook.
RawRunSection
¶
Bases: BaseModel
The run: section — "how the container runs".
Covers OCI-runtime selection, container resource limits, capability toggles, environment, and lifecycle hooks. Sandbox owns this because every field translates to a podman/runtime flag or annotation sandbox emits at launch time.
Inheritable in both directions:
- At the global level, defaults apply to every project
(e.g. set
runtime: krunonce to opt the whole installation into microVM isolation). - At the project level, fields override the global default one-by-one via the orchestrator's merge logic.
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
shutdown_timeout = Field(default=10, description='Seconds to wait before SIGKILL on container stop')
class-attribute
instance-attribute
¶
gpus = Field(default=None, description='GPU passthrough: ``true``, ``"all"``, or omit to disable')
class-attribute
instance-attribute
¶
memory = Field(default=None, description='Podman ``--memory`` value (e.g. ``"4g"``, ``"512m"``, ``"4gib"``, plain ``"1024"`` for bytes); ``None`` = unlimited. Format mirrors what podman accepts — see ``man podman-run(1)`` --memory.')
class-attribute
instance-attribute
¶
cpus = Field(default=None, description='Podman ``--cpus`` value (e.g. ``"2.0"``, ``"0.5"``); ``None`` = unlimited. Non-negative decimal.')
class-attribute
instance-attribute
¶
nested_containers = Field(default=False, description='Declares that the project runs podman/docker inside its container. When true, the outer container is launched with ``--security-opt label=nested`` and ``--device /dev/fuse`` so rootless nested containers work under SELinux without disabling labels wholesale.')
class-attribute
instance-attribute
¶
runtime = Field(default=None, description='OCI runtime: ``crun`` (default) for conventional containers, or ``krun`` for KVM-microVM isolation (experimental). ``None`` resolves to ``crun`` — the OCI runtime podman picks by default on every supported distro. ``krun`` requires the global ``experimental: true`` flag at task launch.')
class-attribute
instance-attribute
¶
timezone = Field(default=None, description="IANA timezone for the task container (e.g. ``Europe/Prague``, ``UTC``). Propagated as ``TZ`` — resolved against the image's ``tzdata``. Unset (default) means follow the host's timezone.")
class-attribute
instance-attribute
¶
hooks = Field(default_factory=RawHooksSection)
class-attribute
instance-attribute
¶
SandboxConfigView
¶
Bases: BaseModel
The slice of config.yml sandbox owns and validates.
extra="allow" at the top level so unknown sections (executor's
image:, terok's tui: / logs: / tasks: / git: /
hooks:) pass through silently when sandbox is run standalone —
the ecosystem's shared config file is expected to contain every
package's keys, and rejecting them would make python -m
terok_sandbox crash on any complete config.
Higher layers compose by inheriting from this class and adding their own typed fields:
terok_executor.config_schema.ExecutorConfigViewinherits and adds theimage:section.- terok's
RawGlobalConfiginherits and adds the remaining five terok-owned sections, then flips toextra="forbid"— the topmost layer knows every section, so a typo at the top level is caught there.
model_config = ConfigDict(extra='allow')
class-attribute
instance-attribute
¶
credentials = Field(default_factory=RawCredentialsSection)
class-attribute
instance-attribute
¶
paths = Field(default_factory=RawPathsSection)
class-attribute
instance-attribute
¶
shield = Field(default_factory=RawShieldSection)
class-attribute
instance-attribute
¶
services = Field(default_factory=RawServicesSection)
class-attribute
instance-attribute
¶
vault = Field(default_factory=RawVaultSection)
class-attribute
instance-attribute
¶
gate_server = Field(default_factory=RawGateServerSection)
class-attribute
instance-attribute
¶
network = Field(default_factory=RawNetworkSection)
class-attribute
instance-attribute
¶
ssh = Field(default_factory=RawSSHSection)
class-attribute
instance-attribute
¶
run = Field(default_factory=RawRunSection)
class-attribute
instance-attribute
¶
experimental = Field(default=False, description="Cross-package opt-in for experimental features. Gates terok's krun runtime and sandbox's krun-only host-binary prereq probes (``ip``). Lives on the top level rather than in any one section because it's shared between sandbox, executor, and terok — the topmost layer (terok) inherits this declaration.")
class-attribute
instance-attribute
¶
gate_use_personal_ssh_default()
¶
Resolve the host gate's ssh.use_personal global default.
Reads the ssh: section from the shared config.yml, validates
via RawSSHSection, and returns the bool. An unset section,
a missing key, or a malformed value collapses to False — the
safe historical default ("terok never touches your real keys").
Higher layers compose this with project-level and per-invocation overrides; the resolution chain ends up:
CLI ``--use-personal-ssh`` (highest)
project ``project.yml`` ssh
global ``config.yml`` ssh ← THIS function
False (default)
Lives in sandbox because the consumer
(_git_env_with_ssh) is here too —
same package owns the schema and the reader.