Skip to content

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='unlock', help='Provision the credentials-DB passphrase for this session (tmpfs file)', handler=_handle_vault_unlock), CommandDef(name='lock', help='Remove the session-unlock tmpfs file', handler=_handle_vault_lock), 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_destroy_passphrase', '_handle_vault_list', '_handle_vault_lock', 'handle_vault_seal', 'handle_vault_to_keyring', '_handle_vault_unlock', '_key_id_from_row', '_open_db', '_persist_mode_choice', '_print_key_table', '_provision_passphrase', '_run_credentials_setup_phase', '_validate_scope_name'] module-attribute

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
def handle_vault_seal(*, cfg: SandboxConfig | None = None, key: str = "auto") -> None:
    """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.
    """
    from ..vault.store import systemd_creds
    from ..vault.store.encryption import WrongPassphraseError

    if cfg is None:
        cfg = SandboxConfig()

    if not systemd_creds.is_available():
        raise SystemExit(
            "systemd-creds unavailable: needs systemd ≥ 257 with the Varlink"
            " io.systemd.Credentials interface (Fedora ≥ 42, Debian ≥ 13)"
        )

    key_mode = _SEAL_KEY_MODES.get(key)
    if key_mode is None:
        choices = ", ".join(sorted(_SEAL_KEY_MODES))
        raise SystemExit(f"unknown --key value: {key!r} (expected one of: {choices})")

    # A prompt here would accept a freshly-typed value and seal *that*,
    # leaving the next chain walk holding a key that doesn't open the DB.
    try:
        passphrase = cfg.resolve_passphrase()
    except WrongPassphraseError as exc:
        raise SystemExit(f"cannot seal: {exc}") from exc
    if passphrase is None:
        raise SystemExit("no current passphrase to seal — run `terok-sandbox vault unlock` first")

    try:
        systemd_creds.seal(passphrase, cfg.vault_systemd_creds_file, key_mode=key_mode)
    except RuntimeError as exc:
        # ``tpm2`` requested on a TPM-less host surfaces as a CalledProcessError
        # bubbled to RuntimeError — pass it through with the hint attached.
        raise SystemExit(str(exc)) from exc

    print(f"→ sealed passphrase to {cfg.vault_systemd_creds_file} (--with-key={key_mode})")
    print(
        "  the resolution chain will pick this up the next time a supervisor"
        " starts; no restart required"
    )

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
def handle_vault_to_keyring(*, cfg: SandboxConfig | None = None) -> 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.
    """
    from .. import config as _config
    from ..vault.store.encryption import (
        WrongPassphraseError,
        store_passphrase_in_keyring,
    )

    if cfg is None:
        cfg = SandboxConfig()

    try:
        passphrase, source = cfg.resolve_passphrase_with_source(prompt_on_tty=True)
    except WrongPassphraseError as exc:
        raise SystemExit(f"cannot move to keyring: {exc}") from exc

    if not passphrase:
        raise SystemExit("no current passphrase resolvable; run `terok-sandbox vault unlock` first")
    if source == "keyring":
        print("→ passphrase is already in the keyring; nothing to do")
        return

    if not store_passphrase_in_keyring(passphrase):
        raise SystemExit("OS keyring is unreachable or denied; aborting (nothing was changed)")
    print(f"→ stored passphrase in keyring (was: {source})")

    # Switch the config's tier wiring atomically: flip use_keyring on,
    # drop the plaintext + helper fallbacks so the chain can't re-resolve
    # via a stale lower tier.
    from ..paths import config_file_paths

    user_config = next((p for label, p in config_file_paths() if label == "user"), None)
    if user_config is not None:
        # nosec: B105 — clearing config keys to None, not hardcoding secrets
        updates = {  # nosec: B105
            "use_keyring": True,
            "passphrase": None,  # nosec: B105
            "passphrase_command": None,  # nosec: B105
        }
        _yaml_update_section(user_config, "credentials", updates)
        _config._credentials_section.cache_clear()
        print(f"→ updated {user_config} (use_keyring: true, plaintext fields cleared)")

    # Remove the old tier's persistent copy.  Session file is removed
    # because the chain prefers it over keyring; sealed systemd-creds
    # likewise outranks keyring on the resolution order.
    for stale in (cfg.vault_passphrase_file, cfg.vault_systemd_creds_file):
        if stale.exists():
            stale.unlink()
            print(f"→ removed {sanitize_tty(str(stale))}")