terok_sandbox
terok_sandbox
¶
terok-sandbox: hardened Podman container runner with gate and shield integration.
Delegates to domain subsystems:
gate— authenticated git serving: HTTP server, token CRUD, upstream mirror management, systemd/daemon lifecycle.vault— secret injection: per-container token broker with phantom credentials, SSH signing proxy, SQLite credential store.shield— egress firewall adapter (delegates to terok-shield).runtime— Podman CLI wrapper (state queries, GPU, log streaming).sandbox— facade composing the above behindSandboxConfig.commands— CLI command registry and handler implementations.
The top-level surface here is the published contract that
terok_executor and terok consume. Internal
helpers (raw config schema fragments, runtime concrete types like
Container/LogStream/PortReservation, SSH keypair helpers,
selinux probe internals, port-registry primitives, shield error
classes) stay in their submodules; reach into terok_sandbox.<sub>
when you need them.
CONTAINER_RUNTIME_DIR = '/run/terok'
module-attribute
¶
Container-side mount point for the host runtime directory (socket mode).
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.
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.
DEFAULT_GUEST_SSHD_PORT = 22
module-attribute
¶
DEFAULT_SSH_HOST = '127.0.0.1'
module-attribute
¶
READY_MARKER = '>> init complete'
module-attribute
¶
Default log line emitted by init-ssh-and-repo.sh when the container is ready.
CODEX_SHARED_OAUTH_MARKER = 'terok-proxy-codex-oauth-marker:vault-handles-real-auth'
module-attribute
¶
PHANTOM_CREDENTIALS_MARKER = 'terok-proxy-phantom-token:vault-handles-real-auth'
module-attribute
¶
__version__ = _meta_version('terok-sandbox')
module-attribute
¶
__all__ = ['CONTAINER_RUNTIME_DIR', 'ConfigScope', 'RawRunSection', 'RawSSHSection', 'SERVICES_TCP_OPTOUT_YAML', 'Sandbox', 'SandboxConfig', 'SandboxConfigView', 'ServicesMode', 'gate_use_personal_ssh_default', 'SetupVerdict', 'installed_versions', 'needs_setup', 'read_stamp', 'sandbox_uninstall', 'stamp_path', 'GateServer', 'PerContainerResources', 'allocate_per_container_resources', 'mint_gate_token', 'ContainerRuntime', 'DEFAULT_GUEST_SSHD_PORT', 'DEFAULT_SSH_HOST', 'ExecResult', 'GpuConfigError', 'Image', 'KrunRuntime', 'LifecycleHooks', 'NullRuntime', 'PodmanRuntime', 'READY_MARKER', 'RunSpec', 'Sharing', 'TcpSSHTransport', 'VolumeSpec', 'check_gpu_available', 'podman_port_resolver', 'EnvironmentCheck', 'ShieldHooks', 'ShieldManager', 'check_environment', 'resolve_container_state_dir', 'GateAuthNotConfigured', 'GateStalenessInfo', 'GitGate', 'is_ssh_url', 'CODEX_SHARED_OAUTH_MARKER', 'CredentialDB', 'NoPassphraseError', 'PHANTOM_CREDENTIALS_MARKER', 'RecoveryStatus', 'WrongPassphraseError', 'systemd_creds_has_tpm2', 'handle_vault_seal', 'handle_vault_to_keyring', 'SSHInitResult', 'SSHManager', 'ensure_infra_keypair', 'public_line_of', 'claim_port', 'release_port', 'CheckVerdict', 'DoctorCheck', 'sandbox_doctor_checks', 'SelinuxCheckResult', 'SelinuxStatus', 'check_selinux_status', 'selinux_install_command', 'selinux_install_script', 'AppArmorCheckResult', 'AppArmorStatus', 'check_apparmor_status', 'apparmor_install_command', 'apparmor_install_script', 'CommandTree', 'bold', 'red', 'stage_line', 'yaml_update_section', 'yellow', '__version__']
module-attribute
¶
AppArmorCheckResult(status)
dataclass
¶
Structured outcome of check_status.
status
instance-attribute
¶
AppArmorStatus
¶
Bases: Enum
Outcome of check_status.
NOT_APPLICABLE = 'not_applicable'
class-attribute
instance-attribute
¶
No AppArmor, no dnsmasq, or no dnsmasq profile — nothing to do.
PROFILE_MISSING = 'profile_missing'
class-attribute
instance-attribute
¶
dnsmasq is AppArmor-profiled but the terok addendum isn't installed.
OK = 'ok'
class-attribute
instance-attribute
¶
The terok addendum is installed.
SelinuxCheckResult(status, missing_policy_tools=tuple())
dataclass
¶
Structured outcome of check_status.
Callers decide how to present the result; this struct only carries
the decision tree's output so that terok setup (printed multi-
line warnings) and terok sickbay (tuple-based check result) can
share one source of truth for the branching.
SelinuxStatus
¶
Bases: Enum
Outcome of check_status — the single decision tree behind
both terok setup's prereq check and terok sickbay's health check.
NOT_APPLICABLE_TCP_MODE = 'not_applicable_tcp_mode'
class-attribute
instance-attribute
¶
Transport is tcp; the terok_socket_t policy is irrelevant.
NOT_APPLICABLE_PERMISSIVE = 'not_applicable_permissive'
class-attribute
instance-attribute
¶
Socket transport, but SELinux is disabled or permissive.
POLICY_MISSING = 'policy_missing'
class-attribute
instance-attribute
¶
Enforcing host, socket transport, but terok_socket module is not loaded.
POLICY_OUTDATED = 'policy_outdated'
class-attribute
instance-attribute
¶
Enforcing host, socket transport, terok_socket loaded — but an
older revision missing the container_runtime_t rule the per-container
supervisor needs. Re-running the installer rebuilds + upgrades it.
LIBSELINUX_MISSING = 'libselinux_missing'
class-attribute
instance-attribute
¶
Policy is loaded but libselinux.so.1 cannot be dlopen'd — silent-
failure case where sockets would bind as unconfined_t regardless.
OK = 'ok'
class-attribute
instance-attribute
¶
Enforcing, policy installed, libselinux loadable — all good.
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
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
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
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
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
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
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
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
¶
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
¶
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
¶
CheckVerdict(severity, detail, fixable=False)
dataclass
¶
DoctorCheck(category, label, probe_cmd, evaluate, fix_cmd=None, fix_description='', host_side=False)
dataclass
¶
A single health check to run inside (or against) a container.
The probe_cmd is executed via podman exec <cname> ... by the
orchestrator. The evaluate callable interprets the result.
If fix_cmd is set, the orchestrator may offer it when the check
fails with fixable=True.
Dual execution modes:
- Container mode (
host_side=False): the orchestrator runsprobe_cmdviapodman execand passes the result toevaluate. The standalonedoctorcommand runs the sameprobe_cmddirectly viasubprocesson the host. - Host-side mode (
host_side=True): the orchestrator bypassesprobe_cmdentirely and performs the check via Python APIs (e.g.ShieldManager), then passes resolved state toevaluate. The standalonedoctorcommand callsevaluate(0, "", "")and the function performs the check itself or reports a neutral result.
category
instance-attribute
¶
Grouping key: "bridge", "env", "mount", "network",
"shield", "git".
label
instance-attribute
¶
Human-readable check name shown in output.
probe_cmd
instance-attribute
¶
Shell command to run inside the container via podman exec.
evaluate
instance-attribute
¶
(returncode, stdout, stderr) → CheckVerdict.
fix_cmd = None
class-attribute
instance-attribute
¶
Optional remediation command for podman exec.
fix_description = ''
class-attribute
instance-attribute
¶
Shown to the operator before applying the fix.
host_side = False
class-attribute
instance-attribute
¶
If True, the check runs on the host (not via podman exec).
The orchestrator calls evaluate(0, "", "") and the evaluate
function performs the host-side check itself.
GateAuthNotConfigured(scope)
¶
Bases: RuntimeError
Raised when a scope has no vault key and personal-SSH fallback is not opted in.
Callers (the gate-sync CLI dispatch) turn this into a two-door
remediation hint:
- generate a terok-managed key with
terok ssh-init <project>and register it upstream, or - opt in to the user's own
~/.sshkeys with--use-personal-ssh(orssh.use_personal: truein the project YAML).
Source code in src/terok_sandbox/gate/mirror.py
scope = scope
instance-attribute
¶
GateStalenessInfo(branch, gate_head, upstream_head, is_stale, commits_behind, commits_ahead, last_checked, error)
dataclass
¶
GitGate(*, scope, gate_path, upstream_url=None, default_branch=None, use_personal_ssh=False, validate_gate_fn=None, clone_cache_base=None)
¶
Repository + Gateway for a host-side git gate mirror.
Manages the bare git mirror that containers clone from. Provides operations for initial creation, incremental sync from upstream, selective branch fetching, and staleness detection.
Constructor takes plain parameters — no terok-specific types.
Initialise with plain parameters.
Parameters¶
scope:
Credential scope for this gate's owner. Used to locate the
per-scope vault SSH-agent socket.
gate_path:
Path to the bare git mirror on the host.
upstream_url:
Git upstream URL to sync from.
default_branch:
Branch name used for staleness comparisons.
use_personal_ssh:
When True, skip the vault socket entirely and let git fall
through to the user's ~/.ssh keys / loaded agent. Default
False — "terok never touches your real keys" is the advertised
property. Opt in per-invocation (--use-personal-ssh) or
per-project (ssh.use_personal: true in project YAML).
validate_gate_fn:
Optional callback (scope) -> None that validates no other
scope uses the same gate with a different upstream. Injected by
the orchestration layer; omitted for standalone use.
clone_cache_base:
Base directory for non-bare clone caches. When set,
sync refreshes a working-tree cache at
clone_cache_base / scope after updating the bare mirror.
The cache accelerates task startup by enabling a host-side
file copy instead of a full git clone.
Source code in src/terok_sandbox/gate/mirror.py
cache_path
property
¶
Clone cache directory for this scope, or None if caching is disabled.
close()
¶
Stop the ephemeral signer this gate started, if any.
Idempotent. Long-lived processes (the TUI) should call this explicitly so the signer thread and temp socket don't outlive the gate's last use.
Source code in src/terok_sandbox/gate/mirror.py
__del__()
¶
sync(branches=None, force_reinit=False)
¶
Sync the host-side git mirror gate from upstream.
With an upstream configured, clones (or fetches) from it using the project's SSH setup. Without one, initialises a bare repo in place and returns a no-op sync — the gate then serves as a local-only remote that the container can push to, giving the agent somewhere to stage commits even when there is nothing external to mirror.
A remoteless gate that already exists is a proper no-op: nothing re-initialises, and the returned branch list is empty.
Source code in src/terok_sandbox/gate/mirror.py
sync_branches(branches=None)
¶
Sync specific branches in the gate from upstream.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branches
|
list[str] | None
|
List of branches to sync (default: all via remote update) |
None
|
Returns:
| Type | Description |
|---|---|
BranchSyncResult
|
Dict with keys: success, updated_branches, errors |
Source code in src/terok_sandbox/gate/mirror.py
compare_vs_upstream(branch=None)
¶
Compare gate HEAD vs upstream HEAD for a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str | None
|
Branch to compare (default: configured default_branch) |
None
|
Returns:
| Type | Description |
|---|---|
GateStalenessInfo
|
GateStalenessInfo with comparison results |
Source code in src/terok_sandbox/gate/mirror.py
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | |
last_commit()
¶
Get information about the last commit on the configured branch.
Returns None if the gate doesn't exist or is not accessible.
Source code in src/terok_sandbox/gate/mirror.py
GateServer(*, mirror_root, token, scope, socket_path=None, host=None, port=None)
¶
Per-container git gate, composed by the supervisor alongside the vault.
Serves the task's repo out of the shared per-project bare mirror at
mirror_root, gated on the single token (scoped to scope).
Binds either a per-container Unix socket (socket_path) or a
per-container 127.0.0.1 TCP port (host + port); exactly one
transport must be supplied.
Stateless and self-contained — the only terok dependency is the SELinux socket-labelling helper the Unix listener needs.
Bind the gate's configuration; start brings the listener up.
Source code in src/terok_sandbox/gate/server.py
start()
async
¶
Bind the listener and serve it on a daemon thread.
Source code in src/terok_sandbox/gate/server.py
stop()
async
¶
Stop the listener and join the serving thread.
shutdown() blocks until the accept loop exits, so it runs in
an executor rather than inline on the event loop — calling it on
the loop thread would deadlock.
Source code in src/terok_sandbox/gate/server.py
ShieldHooks
¶
Host-wide OCI hooks installer — no task context.
Thin pass-through to terok-shield's
HooksInstaller. Kept as a class
so the sandbox setup aggregator can swap it out in tests without
poking around terok-shield internals.
install()
staticmethod
¶
Install global OCI hooks for shield egress firewalling.
Global hooks are required on all podman versions to survive
container stop/start cycles (terok-shield#122). Single
layout: scripts, ballast, and JSON descriptors all land in
namespace_state_dir("shield") / "hooks";
containers.conf is patched to register that path.
Source code in src/terok_sandbox/integrations/shield.py
ShieldManager(task_dir, cfg=None, *, runtime=ShieldRuntime.DEFAULT, loopback_ports_override=None)
¶
Per-task wrapper around Shield.
Holds the (task_dir, cfg, runtime) tuple a Shield is built from
and caches the constructed instance — the previous free-function
surface rebuilt a Shield on every call, which paid the
ShieldConfig + collaborator-wiring cost twice for every
transition pair (pre_start → up, up → down, …).
Bypassable methods (pre_start, up, down) short-circuit
when shield_bypass is set on the configuration.
Non-bypassable methods (quarantine, state) always run —
panic overrides every safety bypass, and state probes report what
nft actually sees regardless of operator intent.
Bind the manager to a task directory and shield configuration.
runtime selects the container runtime category — DEFAULT
for crun/runc/youki (dnsmasq on netns 127.0.0.1), KRUN
for the libkrun microVM path (dnsmasq on a link-local address
the guest can reach via passt). Callers that drive the launch
path map their runtime string (RunSpec.runtime) to the
enum.
loopback_ports_override replaces the cfg-derived
(gate_port, token_broker_port, ssh_signer_port) triple — the
per-container launch path passes the freshly-allocated broker
and signer ports so shield's nft rules allow the actual host
ports the supervisor binds.
Source code in src/terok_sandbox/integrations/shield.py
state_dir
property
¶
Per-task shield state directory: {task_dir}/shield.
bypass
property
¶
True when shield_bypass is set on the sandbox configuration.
shield
cached
property
¶
Lazily constructed Shield instance.
Built from a ShieldConfig whose
loopback_ports reflect the actual gate/broker/signer
ports — auto-allocated configs default those fields to None,
which would otherwise silently produce an empty tuple and a
shield ruleset with no
tcp dport <p> ip daddr 169.254.1.2 accept rules, causing
container→host TCP traffic to fall through to the
private-range reject (#156 regression follow-up).
pre_start(container)
¶
Return extra podman run args for egress firewalling.
Returns an empty list (no firewall args) when the dangerous
bypass_firewall_no_protection override is active.
Raises SystemExit with setup instructions when
the podman environment requires one-time hook installation.
Source code in src/terok_sandbox/integrations/shield.py
up(container, container_id)
¶
Set shield to deny-all mode for a running container.
container is the operator-facing podman name (audit-log key);
container_id is the full podman UUID — terok-shield's per-
container hub socket is keyed on it. Both are mandatory:
terok-shield removed the global-hub fallback in
feat/per-container-supervisor.
Source code in src/terok_sandbox/integrations/shield.py
down(container, container_id, *, allow_all=False)
¶
Set shield to bypass mode (allow egress) for a running container.
container / container_id — see
up. When
allow_all is True, also permits private-range (RFC 1918)
traffic.
Source code in src/terok_sandbox/integrations/shield.py
quarantine(container)
¶
Total network blackout — drop all traffic, log dropped traffic.
Ignores shield_bypass because panic overrides every safety bypass.
Source code in src/terok_sandbox/integrations/shield.py
state(container)
¶
Return the live shield state for a running container.
Queries actual nft state even when bypass is set, because containers started before bypass was enabled may still have active rules.
Source code in src/terok_sandbox/integrations/shield.py
status()
¶
Return shield status dict from the sandbox configuration.
Reads only the sandbox configuration — does not instantiate the underlying Shield, so callers that only want configuration-level shape don't pay the Shield wire-up cost.
Source code in src/terok_sandbox/integrations/shield.py
check_environment()
¶
Check the podman environment for shield compatibility.
Returns a synthetic EnvironmentCheck
with bypass info when the dangerous bypass override is active.
Source code in src/terok_sandbox/integrations/shield.py
interactive_session(container)
¶
Run the terminal clearance fallback for this task's shield.
Thin wrapper that spares callers from reaching into
terok_shield.simple_clearance
and rebuilding the state_dir themselves. Refuses to run
when the D-Bus clearance hub is already handling the session.
Source code in src/terok_sandbox/integrations/shield.py
watch_session(container)
¶
Stream shield blocked-access events for this task as JSON lines.
Thin wrapper that spares callers from reaching into
terok_shield.watch and rebuilding the
state_dir themselves.
Source code in src/terok_sandbox/integrations/shield.py
PerContainerResources(container_runtime_dir, token_broker_port, ssh_signer_port, gate_port)
dataclass
¶
Per-container socket dir + (for TCP mode) ports.
Allocated once per launch so the same values reach mount flags, env vars, and the sidecar JSON the supervisor reads. Keeps concurrent containers from colliding on host-global filenames or ports.
container_runtime_dir
instance-attribute
¶
Host-side directory that becomes /run/terok/ inside the
container. Contains the supervisor-bound vault.sock /
ssh-agent.sock. Created (mode 0700) before the bind mount.
token_broker_port
instance-attribute
¶
Per-container TCP port for the vault proxy in TCP mode; None
in socket mode.
ssh_signer_port
instance-attribute
¶
Per-container TCP port for the SSH signer in TCP mode; None
in socket mode.
gate_port
instance-attribute
¶
Per-container TCP port for the git gate in TCP mode; None
in socket mode.
ContainerRuntime
¶
Bases: Protocol
The container runtime — factory for handles, plus operations that have no single-object receiver.
One instance per process, typically constructed at the top-level entry
point and threaded down through higher layers (Sandbox, executor's
AgentRunner, terok's CLI/TUI).
container(name)
¶
Return a handle to the container named name.
Does not verify existence; call Container.state for that.
containers_with_prefix(prefix)
¶
image(ref)
¶
Return a handle to the image identified by tag or ID ref.
Does not verify existence; call Image.exists for that.
images(*, dangling_only=False)
¶
Enumerate local images.
dangling_only narrows to untagged images (those listed as
<none>:<none>).
exec(container, cmd, *, timeout=None)
¶
Run cmd inside container and return its completion record.
The operation that diverges most across backends: podman uses
podman exec; the krun backend uses SSH over a passt-forwarded
TCP port.
Source code in src/terok_sandbox/runtime/protocol.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Run cmd inside container with stdio bridged to caller-supplied streams.
Forwards bytes bidirectionally between stdin/stdout/stderr and the
spawned process — distinct from exec, which captures output into
an ExecResult. Used by the host-side ACP proxy to bridge a Unix
socket to an in-container ACP-stdio agent without the runtime ever
materialising the conversation.
Blocks until the child exits; returns the exit code. EOF on either side terminates forwarding cleanly. Implementations are expected to be transport-agnostic — stdin/stdout are arbitrary byte streams (a socket's file-object face, a pipe end, a test buffer).
Source code in src/terok_sandbox/runtime/protocol.py
force_remove(containers)
¶
Forcibly stop and remove containers.
Best-effort — continues through individual failures and returns
one ContainerRemoveResult per input. An already-absent
container counts as removed (the post-condition holds).
Source code in src/terok_sandbox/runtime/protocol.py
reserve_port(host='127.0.0.1')
¶
Reserve a free TCP port on host.
The returned PortReservation exposes the port number via
reservation.port and releases the socket on close. Use to
pass a pre-reserved port to an external process.
Source code in src/terok_sandbox/runtime/protocol.py
ExecResult(exit_code, stdout, stderr)
dataclass
¶
Outcome of ContainerRuntime.exec.
Backend-neutral so the SSH-over-passt krun backend can fill it from
an SSH response without pretending to be a subprocess.CompletedProcess.
GpuConfigError(message, *, hint=_CDI_HINT)
¶
Bases: RuntimeError
CDI/NVIDIA misconfiguration detected during container launch.
Store the CDI hint alongside the standard error message.
Source code in src/terok_sandbox/runtime/podman.py
hint = hint
instance-attribute
¶
Image
¶
Bases: Protocol
Handle to a local container image. Cheap to construct.
ref
instance-attribute
¶
Tag ("terok-l2-cli:abcd") or ID ("sha256:...") used on lookup.
id
property
¶
Resolved image ID, or None if the image is not present.
repository
property
¶
Repository portion of the tag ("<none>" for dangling).
tag
property
¶
Tag portion ("<none>" for dangling).
size
property
¶
Podman-rendered human-readable size ("1.2GB").
created
property
¶
Podman-rendered creation timestamp.
exists()
¶
labels()
¶
history()
¶
KrunRuntime(*, transport, podman=None)
¶
Container runtime that launches tasks inside KVM microVMs.
Composition, not inheritance: holds a
PodmanRuntime for every
lifecycle verb (podman --runtime krun is just podman driving a
different OCI runtime) and a
KrunTransport for the
one verb that can't go through podman — exec.
The transport is required: there is no sensible default beyond a
real SSH-over-passt-TCP implementation, and the fake exists explicitly
for tests. Production callers wire the real transport at the
ContainerRuntime selection point
in the orchestrator.
Source code in src/terok_sandbox/runtime/krun.py
container(name)
¶
Return a KrunContainer
handle wrapping the podman container — same lifecycle, krun-aware
login_command.
Return type stays the Container
Protocol rather than the narrower concrete class: mypy treats
Protocol method return types as invariant, so a narrower
annotation breaks structural ContainerRuntime matching for
downstream consumers (terok's _runtime: ContainerRuntime
assignment was the loud failure). The runtime value is
genuinely a KrunContainer — callers needing the concrete
type cast at the call site.
Source code in src/terok_sandbox/runtime/krun.py
containers_with_prefix(prefix)
¶
Same prefix lookup as podman; rewrap each handle as a
KrunContainer so its
login_command routes through the TCP-SSH transport.
Same Protocol-invariance rationale as
container
for the wider declared return type.
Source code in src/terok_sandbox/runtime/krun.py
image(ref)
¶
images(*, dangling_only=False)
¶
exec(container, cmd, *, timeout=None)
¶
Route to the transport — typically SSH-over-passt-TCP.
Source code in src/terok_sandbox/runtime/krun.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Route stdio-bridged exec to the transport.
Source code in src/terok_sandbox/runtime/krun.py
force_remove(containers)
¶
NullRuntime()
¶
Stub ContainerRuntime for tests and dry-run modes.
All state lives in dictionaries on the runtime instance. Tests
pre-populate fixtures via the set_container_state,
add_image, etc. helpers.
Source code in src/terok_sandbox/runtime/null.py
set_container_state(name, state)
¶
set_container_image(name, image_ref)
¶
set_container_rw_size(name, bytes_)
¶
set_exit_code(name, code)
¶
Record the exit code Container.wait will return for name.
set_ready_result(name, ready)
¶
Record the outcome Container.stream_initial_logs returns.
add_image(ref, *, repository='', tag='', size='', created='', labels=None, history=())
¶
Register an image fixture.
Source code in src/terok_sandbox/runtime/null.py
set_exec_result(container_name, cmd, result)
¶
Pre-register the result exec returns for exact cmd.
Source code in src/terok_sandbox/runtime/null.py
set_exec_stdio_script(container_name, cmd, script, *, exit_code=0)
¶
Pre-register a stdio interaction for exec_stdio.
script is a sequence of ("read", bytes) / ("write", bytes)
steps replayed in order: read consumes the matching prefix from
the caller-supplied stdin; write emits the bytes to stdout.
Use this to drive deterministic ACP-handshake tests without spinning
up a real container.
Source code in src/terok_sandbox/runtime/null.py
container(name)
¶
Return a NullContainer handle.
containers_with_prefix(prefix)
¶
Return fixtures whose name starts with prefix-.
Source code in src/terok_sandbox/runtime/null.py
image(ref)
¶
images(*, dangling_only=False)
¶
Return fixture images; dangling_only filters by tag == "<none>".
Source code in src/terok_sandbox/runtime/null.py
exec(container, cmd, *, timeout=None)
¶
Return a pre-registered result, or a default empty success.
Source code in src/terok_sandbox/runtime/null.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Replay a pre-registered stdio script, or no-op with exit code 0.
Records every call (with env) for test inspection. When a script is
registered for (container, cmd), replays it in order: read
consumes from stdin and asserts a match; write pushes bytes to
stdout. Without a script, returns immediately with exit code 0
— matches the empty-success default of exec.
Source code in src/terok_sandbox/runtime/null.py
force_remove(containers)
¶
Record the call and clear every fixture for each container.
Source code in src/terok_sandbox/runtime/null.py
reserve_port(host='127.0.0.1')
¶
Reserve a real host port (even null backend callers want a live port).
PodmanRuntime
¶
The default ContainerRuntime — talks to the podman CLI.
container(name)
¶
containers_with_prefix(prefix)
¶
Return handles for every container whose name starts with prefix-.
Single podman ps -a call under the hood; the returned handles
are lazy (fresh inspect on property access).
Source code in src/terok_sandbox/runtime/podman.py
image(ref)
¶
images(*, dangling_only=False)
¶
Enumerate local images.
dangling_only narrows to untagged <none>:<none> entries.
Source code in src/terok_sandbox/runtime/podman.py
exec(container, cmd, *, timeout=None)
¶
Run cmd inside container via podman exec.
Lets FileNotFoundError (podman missing) and
subprocess.TimeoutExpired propagate unchanged.
Raises ValueError if cmd is empty — podman exec with
no argv is never a valid request and catching it here avoids a
later IndexError in the debug log.
Source code in src/terok_sandbox/runtime/podman.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Bridge byte streams to podman exec -i for cmd inside container.
Synchronous: spawns the child, runs three daemon pump threads
(one per direction) copying bytes until either side reaches
EOF or the child exits, joins the pumps, returns the exit code.
Async callers drive this via
run_in_executor.
Lets FileNotFoundError (podman missing) propagate. On
timeout, terminates the child (terminate → 2 s wait → kill) and
re-raises TimeoutExpired.
Source code in src/terok_sandbox/runtime/podman.py
force_remove(containers)
¶
Best-effort podman rm -f of each container.
Continues through individual failures. An already-absent container counts as removed — the post-condition holds.
Source code in src/terok_sandbox/runtime/podman.py
reserve_port(host='127.0.0.1')
¶
container_states(prefix)
¶
Return {container_name: state} for matching containers.
Optimisation over [c.state for c in containers_with_prefix(prefix)]
— single podman ps -a instead of N inspects. Backend-specific;
not part of the ContainerRuntime protocol.
Source code in src/terok_sandbox/runtime/podman.py
container_rw_sizes(prefix)
¶
Return {container_name: rw_bytes} for matching containers.
Single podman ps --size call — --size is expensive (overlay
diffs) but one bulk call beats N inspects. Backend-specific; not
part of the ContainerRuntime protocol.
Source code in src/terok_sandbox/runtime/podman.py
TcpSSHTransport(*, identity_file, endpoint_resolver, ssh_user=DEFAULT_SSH_USER, ssh_binary='ssh')
¶
OpenSSH-over-loopback-TCP implementation of
KrunTransport.
Holds the host-side identity (private key path) and an endpoint
resolver that maps a Container
to a TcpEndpoint.
The transport never touches the credentials vault directly — the
orchestrator exports the %host key to a tmpfs file and passes
that path in, keeping vault access out of the runtime layer.
Source code in src/terok_sandbox/runtime/krun_transport.py
exec(container, cmd, *, timeout=None)
¶
Run cmd in the guest and return its outcome.
Each cmd token is shlex.quoted into a single remote
command string so the in-guest shell treats embedded
metacharacters as literal data — argv semantics are preserved
across the inherently-shell-parsed ssh wire format.
Source code in src/terok_sandbox/runtime/krun_transport.py
exec_stdio(container, cmd, *, stdin, stdout, stderr=None, env=None, timeout=None)
¶
Bridge byte streams to cmd in the guest; return its exit code.
Environment variables are propagated via a remote env prefix
rather than SendEnv so the transport doesn't depend on the
guest's AcceptEnv whitelist. Env var names are
validated against [A-Za-z_][A-Za-z0-9_]* because the remote
env command expects bare identifiers; values and cmd
tokens are shlex.quoted so embedded shell metacharacters
cross the wire as literal data.
Source code in src/terok_sandbox/runtime/krun_transport.py
login_command(container, *, command=())
¶
Return an ssh argv that attaches a PTY to the guest's shell.
Mirrors what PodmanContainer.login_command
does for the conventional runtime — emits the argv the operator
(or terok login) execs into. Adds -tt so sshd allocates
a real PTY even when stdin isn't a terminal (the caller may be
running under tmux or an IDE proxy).
Both the empty-command path (interactive login → bash -l)
and the explicit-command path land at /workspace via
_at_workspace, so the operator's starting cwd matches what
podman exec gives under crun. Argv tokens past -- are shlex.quoted
(same helper the exec paths use) so the SSH wire format
preserves argv semantics across the login-shell parse on the
far side.
Source code in src/terok_sandbox/runtime/krun_transport.py
LifecycleHooks(pre_start=None, post_start=None, post_ready=None, post_stop=None)
dataclass
¶
Optional callbacks fired at container lifecycle transitions.
All slots are None by default. Sandbox.run() fires pre_start
before podman run and post_start after a successful launch.
post_ready and post_stop are available for callers to invoke at
the appropriate time (e.g. after log streaming or container exit).
pre_start = None
class-attribute
instance-attribute
¶
Fired before podman run.
post_start = None
class-attribute
instance-attribute
¶
Fired after a successful podman run.
post_ready = None
class-attribute
instance-attribute
¶
Fired when the container reports ready (caller responsibility).
post_stop = None
class-attribute
instance-attribute
¶
Fired after the container exits (caller responsibility).
RunSpec(container_name, image, env, volumes, command, task_dir, gpu_enabled=False, memory=None, cpus=None, extra_args=(), unrestricted=True, sealed=False, hostname=None, runtime=None, annotations=(lambda: MappingProxyType({}))(), loopback_ports=())
dataclass
¶
Everything needed for a single podman run invocation.
container_name
instance-attribute
¶
Unique container name.
image
instance-attribute
¶
Image tag to run (e.g. terok-l1-cli:ubuntu-24.04).
env
instance-attribute
¶
Environment variables injected into the container.
volumes
instance-attribute
¶
Host↔container directory bindings (mounted or injected per sealed).
command
instance-attribute
¶
Command to execute inside the container.
task_dir
instance-attribute
¶
Host-side task directory (for shield state, logs, etc.).
gpu_enabled = False
class-attribute
instance-attribute
¶
Whether to pass GPU device args to podman.
memory = None
class-attribute
instance-attribute
¶
Podman --memory value (e.g. "4g", "512m"). None = unlimited.
cpus = None
class-attribute
instance-attribute
¶
Podman --cpus value (e.g. "2.0", "0.5"). None = unlimited.
extra_args = ()
class-attribute
instance-attribute
¶
Additional podman run arguments (e.g. port publishing).
unrestricted = True
class-attribute
instance-attribute
¶
When False, adds --security-opt no-new-privileges.
sealed = False
class-attribute
instance-attribute
¶
When True, volumes are injected via podman cp instead of bind-mounted.
hostname = None
class-attribute
instance-attribute
¶
Override the in-container hostname (podman --hostname).
When None (default), podman assigns the short container ID as the
hostname. Orchestrators may set this to a value that correlates with
their own task/container identity — e.g. so a shell prompt inside the
container matches the name the operator sees in task lists. Must be a
valid DNS hostname (letters/digits/hyphens, ≤253 chars); podman enforces
the rule when parsing the flag.
runtime = None
class-attribute
instance-attribute
¶
OCI runtime to use (podman --runtime).
None (default) lets podman pick — its built-in default is
crun. Set to "krun" to launch the task inside a KVM
microVM (Phase 3 KrunRuntime). Backend-neutral here; the runtime
string is passed through verbatim and any compatibility decisions
live higher up (e.g. orchestrator config validation).
annotations = field(default_factory=(lambda: MappingProxyType({})))
class-attribute
instance-attribute
¶
OCI annotations forwarded as podman --annotation k=v entries.
Keys must be on
SAFE_ANNOTATION_KEYS.
Declared as Mapping so callers can pass plain dicts;
__post_init__ snapshots into a MappingProxyType so the
frozen-dataclass guarantee holds against caller mutation.
loopback_ports = ()
class-attribute
instance-attribute
¶
Per-container host ports shield's nft rules must allow.
Empty falls back to the cfg-resolved
(gate_port, token_broker_port, ssh_signer_port) triple
(legacy / single-daemon shape). The per-container launch path
passes (gate_port, per_container.token_broker_port,
per_container.ssh_signer_port) so shield allows the actual
ports the supervisor binds — without this override, shield
blocks the per-container broker/signer with "No route to host".
__post_init__()
¶
Snapshot annotations so a caller-owned dict can't mutate the spec.
Callers may legitimately pass a plain dict (Pydantic, JSON-load,
tests) — we'd lose the frozen guarantee if we kept the live
reference. Take a copy, wrap it in a MappingProxyType, and
write it back through object.__setattr__ since the dataclass
itself is frozen=True.
Source code in src/terok_sandbox/sandbox.py
Sandbox(config=None, *, runtime=None)
¶
Per-task orchestrator composing runtime + services.
Holds a ContainerRuntime (defaulting to PodmanRuntime)
and a SandboxConfig, and exposes gate / shield / lifecycle
verbs bundled in one place. Container lifecycle verbs delegate to the
runtime; the launch path (run, create) still drives
podman directly because shield / gate integration is podman-specific
today.
Source code in src/terok_sandbox/sandbox.py
config
property
¶
Return the sandbox configuration.
runtime
property
¶
Return the injected container runtime.
mint_gate_token()
¶
Mint a fresh per-container gate token.
The gate lives in each container's supervisor; the token travels to the container via the sidecar and is validated in-process, so there is nothing to persist.
Source code in src/terok_sandbox/sandbox.py
gate_url(repo_path, token)
¶
Build the in-container HTTP URL for gate access to repo_path.
Always uses the fixed loopback bridge port (see
_CONTAINER_GATE_PORT): the container reaches the per-container
gate through the socat bridge in both transport modes, so the URL
carries no host address (gate_port is None in socket mode).
Source code in src/terok_sandbox/sandbox.py
pre_start_args(container, task_dir, *, runtime=None, loopback_ports=())
¶
Return extra podman args for shield integration.
runtime is the podman --runtime selector — passed to
ShieldRuntime.from_runtime_name
so shield picks the right dnsmasq bind for the krun guest's
isolated loopback.
loopback_ports overrides shield's cfg-derived allowlist
with per-container ports (see RunSpec.loopback_ports).
Source code in src/terok_sandbox/sandbox.py
shield_down(container, container_id, task_dir)
¶
Remove shield rules for a container (allow all egress).
container is the operator-facing podman name (audit-log key); container_id is the full podman UUID — terok-shield's per- container hub socket is keyed on it. Both are mandatory.
Source code in src/terok_sandbox/sandbox.py
run(spec, *, hooks=None)
¶
Launch a detached container from spec.
In shared mode (default), assembles and executes a single
podman run -d with bind mounts.
In sealed mode (spec.sealed), splits into create → inject →
start: the container is created without volumes, directories are
copied in via podman cp, and the container is then started.
Fires hooks.pre_start before creation and hooks.post_start
after a successful start. Raises GpuConfigError when the
launch fails due to NVIDIA CDI misconfiguration.
Source code in src/terok_sandbox/sandbox.py
create(spec, *, hooks=None)
¶
Create a container without starting it.
Returns the container name. Fires hooks.pre_start before
podman create. The container can then receive injected files
via copy_to before being started with start.
Source code in src/terok_sandbox/sandbox.py
start(container_name, *, hooks=None)
¶
Start a previously created container via the runtime.
Fires hooks.post_start after a successful start.
Source code in src/terok_sandbox/sandbox.py
copy_to(container_name, src, dest)
¶
Copy a host path into a stopped container via the runtime.
stream_logs(container, *, timeout=None, ready_check=None)
¶
Stream container logs until ready_check matches or timeout.
Source code in src/terok_sandbox/sandbox.py
wait_for_exit(container, timeout=None)
¶
stop(containers)
¶
Best-effort stop and remove containers.
Returns one ContainerRemoveResult per entry.
Source code in src/terok_sandbox/sandbox.py
task_state_dir(container)
¶
Per-container state directory used by the launch / cleanup verbs.
The path is consumed by the
launch module: compose writes
the plan + readiness markers under it, and
launch.cleanup removes it on
teardown. The facade owns the derivation — state_dir /
"sandbox" / "runs" / {container} — so the runs subtree
layout has a single canonical owner.
Source code in src/terok_sandbox/sandbox.py
init_ssh(scope)
¶
Create an SSH manager for scope that owns its own CredentialDB.
Callers receive an SSHManager whose DB connection is opened
against SandboxConfig.db_path. Use it as a context
manager (with sandbox.init_ssh(scope) as m: ...) or call
SSHManager.close when done.
Source code in src/terok_sandbox/sandbox.py
Sharing
¶
Directory sharing semantics — expresses intent, not backend details.
The sandbox translates these into backend-specific flags (e.g. SELinux
relabel :z / :Z for Podman) and uses them to drive sealed-mode
decisions (private dirs are injected, shared dirs may be skipped).
VolumeSpec(host_path, container_path, sharing=Sharing.SHARED, read_only=False, live=False)
dataclass
¶
Typed description of a host↔container directory binding.
Replaces raw volume strings ("host:container:z") with structured data
so the sandbox can decide how to materialise each binding — as a bind
mount (shared mode) or a podman cp injection (sealed mode).
sharing expresses the caller's intent (private vs shared); the sandbox translates that into backend-specific flags (e.g. SELinux relabeling for Podman). In sealed mode, sharing semantics can also drive whether a directory is injected (private) or skipped (shared config that the vault replaces).
host_path
instance-attribute
¶
Absolute host-side path to mount or copy in.
container_path
instance-attribute
¶
Absolute path inside the container (e.g. "/workspace").
sharing = Sharing.SHARED
class-attribute
instance-attribute
¶
Sharing semantics: Sharing.PRIVATE or Sharing.SHARED.
read_only = False
class-attribute
instance-attribute
¶
When True, mount the volume read-only inside the container.
Used to layer immutable views on top of writable directory mounts — e.g. exposing a credential file to the agent while preventing it from overwriting the host-side phantom token.
live = False
class-attribute
instance-attribute
¶
When True, this volume is bind-mounted even in sealed mode.
Service plumbing (per-container vault/ssh-agent socket dir, gate
socket, sourced-at-runtime bridge scripts) must be live: sealed-mode
podman cp would snapshot an empty dir on the container side and
the supervisor's later-bound sockets would never appear inside.
Operator state (workspace, agent config) leaves this False so
sealed mode gets fresh copies as designed.
to_mount_arg()
¶
Format as a -v flag value for podman run.
Source code in src/terok_sandbox/sandbox.py
SetupVerdict
¶
Bases: Enum
Result of needs_setup — five possible states a launch can be in.
OK = 'ok'
class-attribute
instance-attribute
¶
Stamp matches all installed package versions exactly.
FIRST_RUN = 'first_run'
class-attribute
instance-attribute
¶
No stamp on disk — the user has never run setup (or wiped state).
STALE_AFTER_UPDATE = 'stale_after_update'
class-attribute
instance-attribute
¶
At least one installed package is newer than the stamped version.
STALE_AFTER_DOWNGRADE = 'stale_after_downgrade'
class-attribute
instance-attribute
¶
At least one installed package is older than the stamped version.
Downgrades aren't tested and can leave systemd units / state DB in forms the older code can't interpret. Frontends should treat this as a hard stop until the user explicitly overrides.
STAMP_CORRUPT = 'stamp_corrupt'
class-attribute
instance-attribute
¶
Stamp file exists but can't be parsed. Frontends should treat as FIRST_RUN.
SSHInitResult
¶
SSHManager(*, scope, db)
¶
Mints SSH keypairs for a scope and stores them in the vault.
Each scope may hold multiple keys (e.g. GitHub + GitLab), each with a
distinct fingerprint. init is idempotent for the default
invocation: re-running ssh-init on a scope that already has a
tk-main: key returns that key without minting a new one — the
operator sees the same public line they registered upstream rather
than a fresh side key they'd have to re-register. force=True
rotates atomically (new key takes the scope in a single
transaction that revokes prior assignments), and a custom
comment opts back into the additive path so multi-deploy-key
setups (GitHub + GitLab on one scope) still work — but only when
asked for explicitly.
Two constructors for two ownership stories:
SSHManager(scope=..., db=...)binds the manager to a caller-ownedCredentialDB. The manager uses it and never closes it. Right shape for tests and pooled connections.SSHManager.open_for_configopens its own DB via the supplied config's chain seam (cfg.open_credential_db) and closes it onclose/ context exit / garbage collection. Right shape for one-shot CLI commands. Passdb_pathwhen the caller already holds a runtime path (typicallyVaultStatus.db_path) so the open targets that DB while still using cfg's tier policy.
Bind the manager to a caller-provided CredentialDB.
Source code in src/terok_sandbox/vault/ssh/manager.py
open_for_config(*, scope, cfg, db_path=None, prompt_on_tty=False)
classmethod
¶
Return a manager that owns a connection opened via cfg.open_credential_db.
db_path defaults to cfg.db_path; callers with a runtime
path override (e.g. the daemon's actual VaultStatus.db_path)
pass it explicitly. Tier knobs always come from cfg — no
cross-package fan-out when sandbox adds a new chain tier.
Source code in src/terok_sandbox/vault/ssh/manager.py
close()
¶
Close the DB connection if this manager opened it (idempotent).
__enter__()
¶
__exit__(*exc)
¶
__del__()
¶
init(key_type='ed25519', comment=None, force=False)
¶
Provision a keypair for the scope.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key_type
|
str
|
|
'ed25519'
|
comment
|
str | None
|
Comment to embed in the public key. When |
None
|
force
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
SSHInitResult
|
Metadata sufficient to display the key to the user or register |
SSHInitResult
|
it with a remote. No filesystem paths. |
Raises:
| Type | Description |
|---|---|
InvalidScopeName
|
if the scope fails validation. Checked
before any key material is generated so a rejected
call leaves no orphaned row in |
Source code in src/terok_sandbox/vault/ssh/manager.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
CredentialDB(db_path, *, passphrase)
¶
SQLite-backed store for provider credentials, SSH keys, and phantom tokens.
The on-disk file is always SQLCipher-encrypted. Callers either
supply passphrase explicitly or leave it None to walk the
runtime resolution chain (keyring → credentials.passphrase).
A missing passphrase raises NoPassphraseError;
a stale plaintext file raises PlaintextDBFoundError
— both are diagnostic-only. Operator-facing remediation (which CLI
verb to run, which doc page to read) is the caller's job: library
code shouldn't bake one frontend's verbs into its exception text.
Source code in src/terok_sandbox/vault/store/db.py
transaction()
¶
Run the body in an explicit BEGIN IMMEDIATE transaction.
Take the write lock up front so callers can compose
read-then-write sequences and trust the whole thing serialises
against concurrent writers. Every mutating method on this
class (credentials, SSH keys, phantom tokens) consults the
self._in_outer_tx flag this context manager sets and skips
its own per-call commit — so the API contract is "any
composition of write methods inside with db.transaction():
is atomic", with no kwarg plumbing at the call site.
On exit: COMMIT on clean exit, ROLLBACK on any
BaseException (KeyboardInterrupt / SystemExit
included — leaving a half-written %scope keypair around
would be worse than a re-mint on retry).
Source code in src/terok_sandbox/vault/store/db.py
store_credential(credential_set, provider, data)
¶
Insert or replace a credential entry.
Source code in src/terok_sandbox/vault/store/db.py
load_credential(credential_set, provider)
¶
Return the credential dict, or None if not found.
Source code in src/terok_sandbox/vault/store/db.py
list_credentials(credential_set)
¶
Return provider names that have stored credentials.
Source code in src/terok_sandbox/vault/store/db.py
list_credential_sets()
¶
Return distinct credential-set names with at least one stored credential.
Source code in src/terok_sandbox/vault/store/db.py
delete_credential(credential_set, provider)
¶
Remove a credential entry (idempotent).
Source code in src/terok_sandbox/vault/store/db.py
store_ssh_key(key_type, private_der, public_blob, comment, fingerprint)
¶
Register a keypair, dedup-by-fingerprint; return the ssh_keys.id.
When a row with the same fingerprint already exists the stored bytes
and comment are left untouched (the caller is re-asserting an
already-known key, which is expected on repeat ssh-import).
Auto-commits unless called inside a
transaction()
scope — in which case the outer block owns the commit.
Source code in src/terok_sandbox/vault/store/db.py
get_ssh_key_by_fingerprint(fingerprint)
¶
Look up a key by fingerprint; returns metadata only.
Source code in src/terok_sandbox/vault/store/db.py
set_ssh_key_comment(fingerprint, comment)
¶
Update the comment of the key with fingerprint.
Returns True if a row was updated, False if the fingerprint
is unknown. The comment is validated by the same safety helper
that gates import_ssh_keypair — control characters and
overlong strings raise
UnsafeCommentError
so the storage-entry-point invariant holds for this path too.
The new comment surfaces to subsequent ssh-add -L queries from
the container because the signer resolves keys fresh from the DB
on every request.
Source code in src/terok_sandbox/vault/store/db.py
assign_ssh_key(scope, key_id, *, allow_infra=False)
¶
Grant scope access to key_id (idempotent).
Rejects unsafe scope names with InvalidScopeName — the
value is later embedded in per-scope Unix-socket paths, so
traversal-like strings (../, /) must not be persisted.
By default also rejects %-prefixed infrastructure scopes so
callers driven by user input can't write to sandbox-reserved
names (%host for the krun host-side keypair, future
%name slots). Sandbox internals that legitimately provision
infrastructure scopes pass allow_infra=True.
Auto-commits unless called inside a
transaction()
scope — in which case the outer block owns the commit.
Source code in src/terok_sandbox/vault/store/db.py
unassign_ssh_key(scope, key_id, *, allow_infra=False)
¶
Revoke scope's access to key_id; drop the key row if orphaned.
Refuses %-prefixed infrastructure scopes by default — pair
with allow_infra=True for sandbox internals that need to
decommission a reserved scope.
Source code in src/terok_sandbox/vault/store/db.py
replace_ssh_keys_for_scope(scope, *, keep_key_id, allow_infra=False)
¶
Atomically make keep_key_id the scope's sole assigned key.
Wraps the "assign new + revoke every other" sequence in a single
SQLite transaction so two concurrent init(force=True) calls
can't both leave their own keys assigned — whichever transaction
commits last wins the scope, and exactly one primary survives.
Orphaned ssh_keys rows for revoked keys are cleaned up in the
same step via unassign_ssh_key semantics.
Refuses %-prefixed infrastructure scopes by default; sandbox
internals provisioning infra keys pass allow_infra=True.
Source code in src/terok_sandbox/vault/store/db.py
unassign_all_ssh_keys(scope, *, allow_infra=False)
¶
Revoke every key currently assigned to scope. Returns count removed.
Refuses %-prefixed infrastructure scopes by default — pair
with allow_infra=True for sandbox internals.
Source code in src/terok_sandbox/vault/store/db.py
list_ssh_keys_for_scope(scope)
¶
Return metadata rows for every key assigned to scope.
Ordered by assigned_at with k.id as a secondary key so
two assignments inside the same SQLite-second (datetime('now')
has 1-second resolution) sort by insert order rather than
implementation-defined order. Callers that do rows[-1] to
pick "the most recently assigned" get a deterministic answer
even under sub-second concurrency.
Source code in src/terok_sandbox/vault/store/db.py
load_ssh_keys_for_scope(scope)
¶
Return full records (with raw bytes) for every key assigned to scope.
Same deterministic ordering as
list_ssh_keys_for_scope
— assigned_at first, then k.id as the sub-second tiebreak.
Source code in src/terok_sandbox/vault/store/db.py
list_scopes_with_ssh_keys()
¶
Return every scope that currently has at least one assigned key.
Source code in src/terok_sandbox/vault/store/db.py
count_ssh_keys()
¶
Return the number of distinct keypairs stored in the DB.
Counts ssh_keys rows (deduplicated by fingerprint) rather
than ssh_key_assignments rows — a single key shared across
scopes is one stored key, not N. Surfaces to TUI/CLI status
consumers so they can show a count without opening the DB
themselves.
Source code in src/terok_sandbox/vault/store/db.py
create_token(scope, subject, credential_set, provider)
¶
Mint a phantom token bound to (scope, subject, credential_set, provider).
subject is an opaque caller-supplied correlation label — the
sandbox stores it verbatim and never interprets its contents.
Today terok puts the orchestrator's task id there; the sandbox
treats the value as a string.
Token format: terok-p-<32 hex chars>.
Source code in src/terok_sandbox/vault/store/db.py
lookup_token(token)
¶
Return {scope, subject, credential_set, provider} or None.
Source code in src/terok_sandbox/vault/store/db.py
list_tokens()
¶
Return every proxy-token row as a list of dicts.
Read-only inventory for operator-facing CLI inspection
(terok vault list --include-tokens). The raw token value
is included so the operator can cross-reference what's actually
mounted into containers; callers MUST mask it before display.
Source code in src/terok_sandbox/vault/store/db.py
revoke_tokens(scope, subject)
¶
Revoke every phantom token bound to (scope, subject).
Returns the number of rows removed. The sandbox makes no claim
about what subject identifies; callers (the orchestrator) pass
whatever opaque label they used at
create_token
time.
Source code in src/terok_sandbox/vault/store/db.py
close()
¶
__del__()
¶
NoPassphraseError
¶
Bases: RuntimeError
No SQLCipher passphrase resolved — the DB cannot be opened.
WrongPassphraseError
¶
Bases: RuntimeError
SQLCipher could not decrypt the DB — passphrase doesn't match its encryption key.
RecoveryStatus(acknowledged, source)
dataclass
¶
Combined marker + resolved-source view for the recovery-key warning surfaces.
Returned by RecoveryStatus.load
so sickbay / doctor / TUI / post-launch CLI all paint the same picture
of "is the operator one reboot away from losing their vault?".
acknowledged
instance-attribute
¶
True iff the zero-byte marker file is present.
source
instance-attribute
¶
Whichever resolver tier unlocked the chain right now, or None if locked.
session_only
property
¶
True iff the passphrase lives only in the tmpfs session-unlock file.
That tier dies on the next reboot — without an off-host copy the vault becomes unrecoverable the moment the machine restarts. Severity should escalate accordingly on every surface that renders this status.
urgent
property
¶
True iff unacknowledged AND session-only (one reboot away from loss).
load(cfg=None)
classmethod
¶
Resolve marker + passphrase source for cfg (defaults if None).
Single seam for every "recovery key unconfirmed" surface — doctor, sickbay, TUI pill, post-task-launch CLI footer. Walking the resolver chain to find the source is cheap (no DB open, just tier knobs) and bundling it with the marker check here means no caller has to repeat the "is this session-only?" lookup.
Source code in src/terok_sandbox/vault/store/recovery.py
is_acknowledged(cfg=None)
staticmethod
¶
Cheap marker-only check (no passphrase resolution).
The vault's resolver tiers (systemd-creds, keyring,
session-file) are all bound to this machine, account, or
boot — a hardware failure or TPM transplant strands the vault
without an off-host copy of the passphrase. This check is
what surfaces the "unconfirmed recovery key" warning in
sickbay / doctor / the TUI pill: presence of a zero-byte
marker file at
vault_recovery_marker_file
means the operator has acknowledged at some point. Absence
(or an unreadable marker) reports False — the warning is
conservative by design.
Source code in src/terok_sandbox/vault/store/recovery.py
acknowledge(cfg=None)
staticmethod
¶
Mark the recovery key as saved (writes the zero-byte sidecar marker).
Always succeeds — the marker is independent of the passphrase resolver, so a locked vault doesn't block acknowledgement. Idempotent; safe to call on an already-acknowledged vault.
Source code in src/terok_sandbox/vault/store/recovery.py
bold(text)
¶
Return text wrapped in ANSI bold when supports_color is true.
red(text)
¶
stage_line(label)
¶
Return a StageLine context manager for progressive rendering.
Thin factory so the call site reads with stage_line("Vault") as
s: rather than the class name.
Source code in src/terok_sandbox/_stage.py
yellow(text)
¶
check_apparmor_status()
¶
Evaluate whether the dnsmasq AppArmor addendum is needed or installed.
File-based and unprivileged: an AppArmor-enabled host with dnsmasq and
a stock dnsmasq profile but no terok addendum is PROFILE_MISSING;
everything else is NOT_APPLICABLE or OK.
Source code in src/terok_sandbox/_util/_apparmor.py
apparmor_install_command(state_root)
¶
Return the sudo bash <script> <state_root> installer invocation.
state_root is the sandbox-live root whose tasks/*/*/shield tree
the rendered profile must permit. The caller supplies it because the
script runs under sudo and cannot resolve the operator's home.
Source code in src/terok_sandbox/_util/_apparmor.py
apparmor_install_script()
cached
¶
Return the path to the bundled install_profile.sh AppArmor installer.
Installation is delegated to this short, inspectable shell script —
run with sudo bash <path> <state_root> — so it can be cat-ed
and audited before the privilege escalation.
Source code in src/terok_sandbox/_util/_apparmor.py
check_selinux_status(*, services_mode)
¶
Evaluate SELinux readiness for socket-transport services.
services_mode is the caller's configured transport (tcp or
socket) — passed in rather than read from sandbox config so the
helper stays free of cross-package config plumbing. Consumers
(terok setup, terok sickbay) call
terok_sandbox.config.services_mode themselves.
Source code in src/terok_sandbox/_util/_selinux.py
selinux_install_command()
¶
Return the full sudo bash <path> shell command for the installer.
Single source for the command string so the setup hint, the sickbay check, and any future caller all render the same invocation.
Source code in src/terok_sandbox/_util/_selinux.py
selinux_install_script()
cached
¶
Return the path to the bundled install_policy.sh installer.
Installation is delegated to this short, inspectable shell script —
which users run with sudo bash <path> — rather than a Python
wrapper. Running Python as root imports a large dependency graph;
a dedicated shell script can be cat-ed and audited in seconds
before the privilege escalation.
Source code in src/terok_sandbox/_util/_selinux.py
yaml_update_section(path, section, updates)
¶
Merge updates into data[section] at path, preserving comments.
Source code in src/terok_sandbox/_yaml.py
sandbox_uninstall(*, no_shield=False, cfg=None)
¶
Tear down the stack in reverse install order.
Losing supervisor hooks mid-flight is recoverable, but losing shield hooks while containers are live is the most disruptive — shield goes last so live containers stay firewalled as long as possible.
Best-effort across phases: a failing phase reports the error and the next phase runs anyway, so a partial-install teardown still removes what it can instead of leaving orphans behind. Exits non-zero only after every phase has had its attempt.
The git gate has no host-side install, so there is no gate uninstall phase — the legacy sweep removes any pre-supervisor gate units.
Source code in src/terok_sandbox/commands/sandbox.py
handle_vault_seal(*, cfg=None, key='auto')
¶
Seal the credentials-DB passphrase into a systemd-creds credential.
Adds the systemd-creds tier to the resolution chain: machine-bound
(TPM2 + host key, or either alone), survives reboot, no OS
keyring required. After sealing, every new supervisor resolves the
passphrase via systemd-creds decrypt on start — no operator
interaction needed at boot, no plaintext-on-disk.
Requires an already-resolvable passphrase — typically from a fresh
vault unlock in the current session.
Source code in src/terok_sandbox/commands/vault.py
handle_vault_to_keyring(*, cfg=None)
¶
Move the current passphrase from its current tier into the OS keyring.
Resolves the passphrase via the chain (or prompts as a last resort),
writes it to the keyring, flips credentials.use_keyring to true
in config.yml, clears any plaintext credentials.passphrase /
credentials.passphrase_command wiring, and removes the
session-file and sealed systemd-creds copies.
The validate-before-destroy ordering is deliberate: if the keyring write fails, the source tier is still intact.
Source code in src/terok_sandbox/commands/vault.py
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.
Source code in src/terok_sandbox/config_schema.py
sandbox_doctor_checks(*, token_broker_port=None, ssh_signer_port=None, desired_shield_state=None)
¶
Return sandbox-level health checks for in-container diagnostics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token_broker_port
|
int | None
|
Token broker TCP port (skip check if |
None
|
ssh_signer_port
|
int | None
|
SSH signer TCP port (skip check if |
None
|
desired_shield_state
|
str | None
|
Expected shield state from |
None
|
Returns:
| Type | Description |
|---|---|
list[DoctorCheck]
|
List of |
Source code in src/terok_sandbox/doctor.py
is_ssh_url(url)
¶
Return True for SSH-scheme git URLs.
Accepts the two forms git itself accepts:
ssh://[user@]host[:port]/path— explicit URL scheme.[user@]host:path— scp-style shorthand. The user part is optional (git@github.com:foo.git,deploy@host:repo.git, baregithub.com:foo.git).
Shared with terok-main: both the gate's env builder and callers that branch on "does this project use SSH?" (e.g. deploy-key prompts, gate-sync fallback hints) must agree on one definition.
Source code in src/terok_sandbox/gate/mirror.py
mint_gate_token()
¶
Generate a fresh 128-bit hex gate token.
Uses secrets.token_hex(16) for cryptographic randomness. The
supervisor validates this single token directly via
_SingleTokenStore,
so there is nothing to persist.
Source code in src/terok_sandbox/gate/tokens.py
check_environment(cfg=None)
¶
Probe the podman environment with no task context.
Returns a synthetic EnvironmentCheck
when shield_bypass is set; otherwise constructs a throwaway
ShieldManager
bound to a temp directory and delegates to its
check_environment.
Kept as a free function because the setup CLI runs before any
task directory exists.
Source code in src/terok_sandbox/integrations/shield.py
allocate_per_container_resources(cfg, container)
¶
Compute per-container paths + (for TCP mode) ports.
Both transport modes get a per-container directory under
cfg.runtime_dir/run/<container> (mode 0700) that the caller
bind-mounts at /run/terok/ inside the container. In TCP mode,
two free ports are claimed via bind(0) + getsockname +
close so each container gets its own pair instead of fighting
over the singleton from cfg.
The narrow window between bind(0)'s close and the supervisor's
re-bind on the same port is an EADDRINUSE-loud failure mode, not
silent breakage.
Source code in src/terok_sandbox/launch.py
claim_port(service_key, preferred=None, *, explicit=False)
¶
Claim one port via the default registry.
release_port(service_key)
¶
check_gpu_available()
¶
Return True when a CDI spec declares the nvidia.com/gpu kind.
Wizards call this to decide whether to offer the NVIDIA base image;
the on-launch check_gpu_error
path is the authoritative one and stays in place. Any failure
(missing podman, missing CDI dirs, unreadable spec) collapses to
False so callers can treat this as a pure yes/no signal.
Source code in src/terok_sandbox/runtime/podman.py
podman_port_resolver(*, guest_port=DEFAULT_GUEST_SSHD_PORT, host=DEFAULT_SSH_HOST)
¶
Return a resolver that reads the forwarded host port via podman port.
The orchestrator launches the container with -p <reserved>:22;
podman already records that mapping in its own metadata, so this
resolver just asks for it back — no terok-private annotation in the
middle. podman port <name> <guest_port>/tcp emits a single
<host_ip>:<host_port> line per matching mapping, which is
exactly what we need.
The resolved host is overridden to host (loopback by default) so
the SSH connect goes through 127.0.0.1 even when pasta bound
the forward to 0.0.0.0; trusting whatever podman reports would
open the door to reaching the guest via a routable interface.
Source code in src/terok_sandbox/runtime/krun_transport.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |
installed_versions()
¶
Return {package: version} for every tracked package present in the install.
Missing packages are silently dropped — a standalone terok-sandbox
install doesn't have terok available, and that's fine. The
invariant we check on the read side is that every package the stamp
knows about is also installed (and at the right version).
Source code in src/terok_sandbox/setup_stamp.py
needs_setup()
¶
Compare the on-disk stamp against currently-installed package versions.
See SetupVerdict for the five possible outcomes. Designed
to be cheap enough to call on every TUI startup.
Source code in src/terok_sandbox/setup_stamp.py
read_stamp(path)
¶
Parse the stamp file, returning the packages mapping.
Raises ValueError if the schema version doesn't match — a
schema bump should be handled explicitly, not silently coerced.
Source code in src/terok_sandbox/setup_stamp.py
stamp_path()
¶
Return the canonical on-disk location of the setup stamp.
Honours the umbrella paths.root resolver so a user who relocates
the state tree (paths.root: /virt/terok in config.yml) sees
the stamp move with it — same place every package would look.
Source code in src/terok_sandbox/setup_stamp.py
ensure_infra_keypair(scope, *, db, comment=None, key_type='ed25519')
¶
Load or generate the %scope infrastructure keypair.
The single place sandbox-internal callers go for the load-or-mint dance:
- If scope already has an assigned key, re-serialise it as OpenSSH PEM + render the public line and return.
- Otherwise mint a fresh keypair, persist it under scope with
assign_ssh_key(..., allow_infra=True), and return the same shape.
Only accepts %-prefixed scopes (the infrastructure form the
DB-layer safe-scope validator recognises) — user scopes go through
the normal ssh init / import_ssh_keypair
paths.
The load-or-mint sequence runs inside a single
db.transaction()
so two concurrent callers can't both observe "empty" and both
proceed to mint. Trust model: the returned private_pem is
plaintext key material; possession of an unlocked
CredentialDB is
already operator-equivalent in this design, so callers with a DB
handle can read any infra key. Callers MUST NOT log, serialise,
or otherwise persist private_pem outside the intended
consumer (e.g. ssh -i file or in-process signer). The keypair material is intended
for sandbox-owned services that need a stable host-side identity
(krun %host, future infrastructure slots); user-controlled
code never goes through this helper.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
|
required |
db
|
CredentialDB
|
Open |
required |
comment
|
str | None
|
Comment to embed in the public line on fresh
generation. Ignored when the keypair already exists
(existing comment is preserved). Defaults to
|
None
|
key_type
|
str
|
|
'ed25519'
|
Returns:
| Type | Description |
|---|---|
InfraKeypair
|
An |
InfraKeypair
|
with the OpenSSH PEM private + public line. |
Source code in src/terok_sandbox/vault/ssh/keypair.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
public_line_of(record)
¶
Render record as the one-line OpenSSH public key form.
Format: <algo> <base64-blob> <comment> — matches what
ssh-keygen writes to .pub files and what a remote's deploy-key
field expects. Callers that rendered this inline now go through this
single helper so the algo-name mapping lives in one place.
Source code in src/terok_sandbox/vault/ssh/keypair.py
systemd_creds_has_tpm2()
¶
Return True when the host has a TPM2 device usable by systemd-creds.
Mirrors systemd-creds has-tpm2's exit code. A preference
probe, not a precondition: a missing TPM doesn't break the tier —
host-key sealing still works in --user mode — so callers use
this to choose between TPM2 and host-key, not to gate availability.