commands
commands
¶
Command registry for terok-sandbox — one module per subsystem.
Follows the same CommandDef /
ArgDef pattern as
terok_shield.registry. Higher-level consumers (terok,
terok-executor) import COMMANDS to build their own CLI frontends
without duplicating argument definitions or handler logic.
Per-subsystem modules:
sandbox— setup/uninstall (composes shield + vault + gate + clearance into one verb).gate— gate server lifecycle.shield— egress-firewall hooks.vault— vault passphrase verbs (the unlock/lock/seal trio that drives the SQLCipher chain).ssh— SSH-key CRUD against the credentials DB.doctor— host-side health checks.credentials— credentials-DB encryption chooser, provisioning, and migration phase.launch— prepare/run/cleanup for user-owned containers.
Shield commands are delegated to terok-shield's own registry —
SHIELD_COMMANDS re-exports the non-standalone subset.
CREDENTIALS_COMMANDS = (CommandDef(name='credentials', help='Credentials DB management', children=(CommandDef(name='encrypt-db', help='Migrate a legacy plaintext credentials DB to SQLCipher-encrypted (deprecated in 0.8.0, removed in 0.9.0)', handler=_handle_credentials_encrypt_db),)),)
module-attribute
¶
DOCTOR_COMMANDS = (CommandDef(name='doctor', help='Run sandbox health checks', handler=_handle_doctor, group='doctor'),)
module-attribute
¶
GATE_COMMANDS = (CommandDef(name='gate', help='Git gate inspection', children=(CommandDef(name='path', help="Print the file:// URL of a project's bare mirror", handler=_handle_gate_path, args=(ArgDef(name='project', help='Project name (the mirror is <project>.git)'),)),)),)
module-attribute
¶
LAUNCH_COMMANDS = (CommandDef(name='prepare', help='Print podman flags for sandboxing a user-owned container', handler=_handle_prepare, epilog=_BRIDGES_EPILOG, args=(ArgDef(name='container', help='Container name (becomes --name)'), ArgDef(name='--no-shield', action='store_true', help='Disable egress firewall (default: on)', dest='no_shield'), ArgDef(name='--no-gate', action='store_true', help='Disable git gate (default: on; requires --scope)', dest='no_gate'), ArgDef(name='--no-broker', action='store_true', help='Disable vault token broker (default: on; requires --scope)', dest='no_broker'), ArgDef(name='--scope', help='Credential scope; enables vault SSH agent and is required by gate/broker'), ArgDef(name='--profiles', type=_csv_list, help="Override shield profiles for this container (comma-separated, e.g. 'dev,pypi')"), ArgDef(name='--json', action='store_true', dest='output_json', help='Output JSON array instead of a shell-quoted string'))), CommandDef(name='run', help='Launch a sandboxed user-owned container (exec into podman run)', handler=_handle_run, epilog=_BRIDGES_EPILOG, args=(ArgDef(name='container', help='Container name (becomes --name)'), ArgDef(name='--no-shield', action='store_true', help='Disable egress firewall (default: on)', dest='no_shield'), ArgDef(name='--no-gate', action='store_true', help='Disable git gate (default: on; requires --scope)', dest='no_gate'), ArgDef(name='--no-broker', action='store_true', help='Disable vault token broker (default: on; requires --scope)', dest='no_broker'), ArgDef(name='--scope', help='Credential scope; enables vault SSH agent and is required by gate/broker'), ArgDef(name='--profiles', type=_csv_list, help="Override shield profiles for this container (comma-separated, e.g. 'dev,pypi')"))), CommandDef(name='cleanup', help='Revoke tokens and drop shield rules for a sandboxed container', handler=_handle_cleanup, args=(ArgDef(name='container', help='Container name to clean up'),)))
module-attribute
¶
SETUP_COMMANDS = (CommandDef(name='setup', help='Install supervisor hooks + shield hooks in one step', handler=_handle_sandbox_setup, args=(ArgDef(name='--no-shield', action='store_true', help='Skip shield install'), ArgDef(name='--no-vault', action='store_true', help='Skip the credentials-DB encryption phase'), ArgDef(name='--echo-passphrase', action='store_true', help='Also print any auto-generated vault passphrase to stdout (default off — the value otherwise only reaches /dev/tty, so non-interactive bootstraps must opt in to capture it)'), ArgDef(name='--passphrase-tier', default=None, help='Force credentials-DB passphrase storage to a specific tier (systemd-creds | keyring | session-file | config) instead of the auto-detect / chooser chain. Required on a non-TTY host without systemd-creds — the silent session-file fallback was removed in v0.0.100 because it minted a passphrase the operator never saw and lost it on the first reboot.'))), CommandDef(name='uninstall', help='Remove supervisor hooks + shield hooks in one step', handler=_handle_sandbox_uninstall, args=(ArgDef(name='--no-shield', action='store_true', help='Skip shield uninstall'),)))
module-attribute
¶
SHIELD_COMMANDS = (CommandDef(name='shield', help='Egress firewall management', children=(_SANDBOX_VERBS + _imported_shield_children())),)
module-attribute
¶
SSH_COMMANDS = (CommandDef(name='ssh', help='SSH keypair management', children=(CommandDef(name='list', help='List SSH keys stored in the vault', handler=_handle_ssh_list, args=(ArgDef(name='--scope', help='Show keys for a specific credential scope only', default=None),)), CommandDef(name='import', help='Import an OpenSSH keypair from files into the vault DB', handler=_handle_ssh_import, args=(ArgDef(name='scope', help='Credential scope to associate the key with'), ArgDef(name='--private-key', help='Path to the private key file', dest='private_key', required=True), ArgDef(name='--public-key', help='Path to the .pub file (default: derive from the private key)', default=None, dest='public_key'), ArgDef(name='--comment', help="Override the key's comment string", default=None))), CommandDef(name='add', help='Generate a new SSH keypair in the vault for a credential scope', handler=_handle_ssh_add, args=(ArgDef(name='scope', help='Credential scope to associate the key with'), ArgDef(name='--key-type', help='Key algorithm: ed25519 (default) or rsa', default='ed25519', dest='key_type'), ArgDef(name='--comment', help='Comment embedded in the public key (default: tk-main:<scope>)', default=None), ArgDef(name='--force', help='Rotate — unassign all existing keys from the scope and generate fresh', action='store_true'))), CommandDef(name='export', help="Export a scope's SSH keypair to standard OpenSSH files", handler=_handle_ssh_export, args=(ArgDef(name='scope', help='Credential scope to export'), ArgDef(name='--out-dir', help='Directory to write files into', dest='out_dir', required=True), ArgDef(name='--key-id', help='Export a specific ssh_keys.id (default: most recently added)', default=None, dest='key_id', type=int), ArgDef(name='--out-name', help='Override the output filename stem (default: id_<type>_<fp8>)', default=None, dest='out_name'))), CommandDef(name='pub', help="Print a scope's public key to stdout", handler=_handle_ssh_pub, args=(ArgDef(name='scope', help='Credential scope'), ArgDef(name='--key-id', help='Specific ssh_keys.id (default: most recently added)', default=None, dest='key_id', type=int), ArgDef(name='--all', help='Print every key assigned to the scope, one per line', action='store_true', dest='all_keys'))), CommandDef(name='link', help='Link an existing vault key to an additional scope', handler=_handle_ssh_link, args=(ArgDef(name='scope', help='Credential scope to link the key to'), ArgDef(name='--key-id', help='ssh_keys.id of the key already stored in the vault', dest='key_id', type=int, required=True))), CommandDef(name='rename', help='Change the comment of a stored SSH key (selected by fingerprint prefix)', handler=_handle_ssh_rename, args=(ArgDef(name='fingerprint', help='Fingerprint prefix identifying the key (min 8 chars recommended)'), ArgDef(name='comment', help='New comment text'))), CommandDef(name='remove', help='Unassign SSH keys from scopes (orphaned keys cascade-delete)', handler=_handle_ssh_remove, args=(ArgDef(name='--scope', help='Filter by credential scope (exact match)', default=None), ArgDef(name='--comment', help='Filter by comment (supports glob wildcards)', default=None), ArgDef(name='--fingerprint', help='Filter by fingerprint prefix (min 8 chars recommended)', default=None), ArgDef(name='--yes', help='Skip confirmation prompts', action='store_true', dest='yes'))))),)
module-attribute
¶
SUPERVISOR_COMMANDS = (CommandDef(name='supervisor', help='Run the per-container supervisor (internal; spawned by the OCI hook)', handler=_handle_supervisor, group='internal', args=(ArgDef(name='container_id', help='Container ID the supervisor manages'), ArgDef(name='sidecar_path', help='Absolute path to the per-container sidecar JSON'))),)
module-attribute
¶
VAULT_COMMANDS = (CommandDef(name='vault', help='Vault passphrase management', children=(CommandDef(name='status', help='Show lock state, the passphrase resolution chain, and stored secrets', handler=_handle_vault_status, args=(ArgDef(name='--json', dest='as_json', action='store_true', help='Machine-readable JSON output'),)), CommandDef(name='unlock', help='Provision the credentials-DB passphrase for this session (tmpfs file)', handler=_handle_vault_unlock, args=(ArgDef(name='--force', action='store_true', help='Write the session file even if a durable tier already unlocks the vault'),)), CommandDef(name='lock', help="Clear every stored copy of the passphrase — you'll need it to unlock again", handler=_handle_vault_lock, args=(ArgDef(name='--force', action='store_true', help="Skip the 'have you saved the passphrase?' confirmation"),)), CommandDef(name='list', help='Inventory stored credentials (and optionally proxy tokens)', handler=_handle_vault_list, args=(ArgDef(name='--include-tokens', action='store_true', help='Also show proxy-token rows (token values are masked)'), ArgDef(name='--json', dest='as_json', action='store_true', help='Machine-readable JSON output'))), _PASSPHRASE_GROUP)),)
module-attribute
¶
COMMANDS = CommandTree(SETUP_COMMANDS + GATE_COMMANDS + SHIELD_COMMANDS + VAULT_COMMANDS + SSH_COMMANDS + CREDENTIALS_COMMANDS + LAUNCH_COMMANDS + DOCTOR_COMMANDS + SUPERVISOR_COMMANDS)
module-attribute
¶
__all__ = ['ArgDef', 'CommandDef', 'CommandTree', 'KeyRow', 'COMMANDS', 'CREDENTIALS_COMMANDS', 'DOCTOR_COMMANDS', 'GATE_COMMANDS', 'LAUNCH_COMMANDS', 'SETUP_COMMANDS', 'SHIELD_COMMANDS', 'SSH_COMMANDS', 'SUPERVISOR_COMMANDS', 'VAULT_COMMANDS', '_ask_passphrase_mode', '_back_up_plaintext_db', '_build_key_rows', '_filter_key_rows', '_forget_config_tier_updates', '_handle_cleanup', '_handle_credentials_encrypt_db', '_handle_doctor', '_handle_gate_path', '_handle_prepare', '_handle_run', '_handle_sandbox_setup', '_handle_sandbox_uninstall', '_handle_shield_setup', '_handle_shield_uninstall', '_handle_ssh_add', '_handle_ssh_export', '_handle_ssh_import', '_handle_ssh_link', '_handle_ssh_list', '_handle_ssh_pub', '_handle_ssh_remove', '_handle_ssh_rename', '_handle_supervisor', '_handle_vault_list', '_handle_vault_lock', 'handle_vault_seal', 'handle_vault_to_keyring', '_handle_vault_unlock', 'SessionProvisionResult', 'SessionShadow', 'clear_redundant_session_file', 'provision_session_passphrase', 'purge_passphrase_tiers', 'session_shadow_state', '_key_id_from_row', '_open_db', '_persist_mode_choice', '_print_key_table', '_provision_passphrase', '_run_credentials_setup_phase', '_validate_scope_name']
module-attribute
¶
SessionProvisionResult(written, validated=False, shadowed_durable=None)
dataclass
¶
Outcome of provision_session_passphrase.
written is the load-bearing bit: False means the write was
refused because a durable tier already resolves the vault, so a
session file would only shadow it (shadowed_durable names that
tier). Refusal is a normal outcome, not an error — callers report
it ("already unlocked via X") rather than raising. validated
says whether the written value was test-opened against an existing
DB (vs. an empty install where it becomes the key on first use).
SessionShadow(durable_source, redundant)
dataclass
¶
A session-file tier sitting on top of a durable tier.
redundant is the actionable bit:
True— the session copy is byte-identical to what the durable tier resolves to, so it's pure residue (a pastunlockon a box that already auto-unlocks). Safe to delete; nothing is lost.False— the two differ. Either a deliberate re-key in progress or a stale unlock masking the durable value; never auto-removed.None— the durable tier is present but couldn't be read to compare (broken seal, dead helper), so the session file may be doing real work. Left alone.
clear_redundant_session_file(cfg)
¶
Remove the session-unlock file iff it merely duplicates a durable tier.
Same-key residue only: a session file whose passphrase differs from
the durable tier is a deliberate override (or a re-key mid-flight)
and is kept. Re-verifies state at call time rather than trusting a
caller's stale read. Returns the durable tier it deduplicated
against, or None when there was nothing safe to remove.
Source code in src/terok_sandbox/commands/vault.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
provision_session_passphrase(cfg, passphrase, *, force=False)
¶
Validate passphrase against the DB, then land it on the session tier.
The single writer of the session-unlock tmpfs file — the CLI
vault unlock and terok's TUI unlock modal both funnel through
here, so the no-shadow and validation guards apply to every caller
by construction; neither can store a value the DB rejects, nor
silently shadow a working durable tier.
Two guards, in order:
- No-shadow. The session tier is the highest-priority slot, so
writing it masks any durable tier (systemd-creds / keyring /
config) underneath — a reboot then wipes the session copy and the
operator only thought the sealed key was in use. When a durable
tier already resolves and force is false, nothing is written and
the result reports
written=False+ the shadowed tier. force (re-key / deliberate override) skips this guard. - Validation. When the DB exists (and isn't a legacy plaintext
file) the value is test-opened first; a mismatch raises
WrongPassphraseErrorand nothing is written. A missing DB skips validation (opening it just to check would create it as a side effect) — the value becomes its key on first use.
Source code in src/terok_sandbox/commands/vault.py
purge_passphrase_tiers(cfg)
¶
Remove every stored copy of the credentials-DB passphrase.
Clears the session-unlock tmpfs file, the OS keyring entry, the
sealed systemd-creds credential, and the plaintext
credentials.passphrase / credentials.passphrase_command
wiring in config.yml — then drops the recovery-acknowledged
marker, since it's meaningless once no tier remains. After this the
vault can only be reopened by re-supplying the passphrase
(vault unlock); it is unrecoverable without an off-host copy.
No prompts and no acknowledgement check: this is the raw destructive
action. The lock verb gates it behind a typed-SAVED
confirmation when recovery is unacknowledged; panic calls it
directly — no questions asked.
Source code in src/terok_sandbox/commands/vault.py
session_shadow_state(cfg)
¶
Describe a session-file-over-durable-tier shadow, or None if there is none.
Returns None in the common cases — no session file, or no durable
tier beneath it — without resolving anything. Only when both are
present does it resolve the durable chain (omitting the session
file) to compare values; that one comparison is the only place
status / remediation pay an unseal. The session secret never leaves
the process — this reads two tiers and compares, exactly what the
resolver already does internally.