Skip to content

_setup

_setup

Sandbox-wide setup orchestration — the phases _handle_sandbox_setup runs.

Each phase is self-contained and idempotent:

  • Prereq probes are report-only. A missing nft or podman later fails the relevant service with a clearer message; reporting here lets the operator spot the root cause before scrolling past install noise.
  • Service install phases do the full stop → uninstall → install → verify cycle so a re-run after pipx install terok-sandbox guarantees the running unit picks up the new code, not just the rewritten on-disk unit file.
  • The clearance phase is optional — headless servers that skip the desktop bridge still get a working shield+vault+gate install.

Stage-line output routes through terok_sandbox._stage (re-exported via the package's public surface) so frontends (terok, terok-executor) that mix their own stage lines in the same log share one renderer and one colour palette. Kept internal (underscore-prefixed module) because every public entry point goes through commands._handle_sandbox_setup.

EXIT_MANUAL_STEP_NEEDED = 5 module-attribute

__all__ = ['EXIT_MANUAL_STEP_NEEDED'] module-attribute

run_prereq_report(cfg)

Print host prerequisites and return the SELinux check result.

The result lets the caller decide whether to fail the setup or re-surface the install hint at the end of output — sandbox#854's fix for the install command getting buried mid-output. Purely informational for the binary checks; never blocks on those. cfg.experimental gates the krun-only probes (currently ip).

Source code in src/terok_sandbox/_setup.py
def run_prereq_report(cfg: SandboxConfig) -> SelinuxCheckResult:
    """Print host prerequisites and return the SELinux check result.

    The result lets the caller decide whether to fail the setup or
    re-surface the install hint at the end of output — sandbox#854's
    fix for the install command getting buried mid-output.  Purely
    informational for the binary checks; never blocks on those.
    ``cfg.experimental`` gates the krun-only probes (currently ``ip``).
    """
    print("Prerequisites:")
    _report_host_binaries()
    _report_firewall_binaries()
    if cfg.experimental:
        _report_krun_binaries()
    _report_apparmor()
    return _report_selinux(cfg)

print_selinux_install_hint(result)

Print the SELinux install command + TCP-mode alternative at end of setup output.

No-op when the SELinux state doesn't require operator action (OK, NOT_APPLICABLE_*). Renders the two alternatives on their own lines so the operator can copy-paste either without surrounding output bleeding in.

Called after all install phases finish so the hint is the last thing the operator sees — sandbox#854's complaint was that the install command landed mid-output and scrolled out of view by the time the install banner printed at the bottom.

Source code in src/terok_sandbox/_setup.py
def print_selinux_install_hint(result: SelinuxCheckResult) -> None:
    """Print the SELinux install command + TCP-mode alternative at end of setup output.

    No-op when the SELinux state doesn't require operator action
    (``OK``, ``NOT_APPLICABLE_*``).  Renders the two alternatives on
    their own lines so the operator can copy-paste either without
    surrounding output bleeding in.

    Called *after* all install phases finish so the hint is the last
    thing the operator sees — sandbox#854's complaint was that the
    install command landed mid-output and scrolled out of view by the
    time the install banner printed at the bottom.
    """
    if result.status not in (SelinuxStatus.POLICY_MISSING, SelinuxStatus.POLICY_OUTDATED):
        return
    outdated = result.status is SelinuxStatus.POLICY_OUTDATED
    print()
    print("─ SELinux policy required ─────────────────────────────────────")
    if outdated:
        print("The loaded terok_socket policy predates the per-container")
        print("supervisor and is missing the rule it binds its sockets with;")
        print("rebuild it so the supervisor can serve the vault / gate / ssh.")
    else:
        print("Socket-transport services need the terok_socket_t policy to be")
        print("loaded; without it, containers can't reach the host sockets.")
    print()
    print("Rebuild the policy (recommended):" if outdated else "Install the policy (recommended):")
    print()
    print(f"  {selinux_install_command()}")
    print()
    print("Or switch to TCP mode (no SELinux policy needed):")
    print()
    print("  yq -yi '.services.mode = \"tcp\"' ~/.config/terok/config.yml")
    print("  terok-sandbox setup")
    print()

print_apparmor_install_hint()

Print the AppArmor addendum install command at end of setup, if needed.

No-op unless the dnsmasq profile addendum is missing. Rendered last (alongside the SELinux hint) so the command isn't scrolled away.

Source code in src/terok_sandbox/_setup.py
def print_apparmor_install_hint() -> None:
    """Print the AppArmor addendum install command at end of setup, if needed.

    No-op unless the dnsmasq profile addendum is missing.  Rendered last
    (alongside the SELinux hint) so the command isn't scrolled away.
    """
    if check_apparmor_status().status is not AppArmorStatus.PROFILE_MISSING:
        return
    print()
    print("─ AppArmor profile recommended ────────────────────────────────")
    print("dnsmasq is AppArmor-confined here; without the terok addendum the")
    print("per-container DNS drops to the dig tier (no live IP-rotation).")
    print()
    print("Install the addendum (keeps dnsmasq otherwise confined):")
    print()
    print(f"  {apparmor_install_command(_apparmor_state_root())}")
    print()

run_supervisor_install_phase()

Install the OCI supervisor hook + wrapper under state_root().

Lays down (with state_root() resolved from paths.root — the operator's single configured root):

  • <state_root>/hooks/supervisor_hook.py + _supervisor_state.py — the OCI hook entrypoint and its stdlib-only ballast.
  • <state_root>/hooks/terok-sandbox-supervisor-<stage>.json — one OCI hook descriptor per stage (createRuntime + poststop). containers.conf is patched at install time to list state_root() / "hooks" in hooks_dir so podman scans the canonical terok-owned directory.
  • <state_root>/supervisor_wrapper.py — the restart-loop the hook spawns, with the terok-sandbox argv baked in at install time.

Idempotent: re-running overwrites the installed files with the current package's copies. Soft-fails on a missing terok-sandbox entry point (degraded install — operator hasn't sourced the venv yet).

Source code in src/terok_sandbox/_setup.py
def run_supervisor_install_phase() -> bool:
    """Install the OCI supervisor hook + wrapper under ``state_root()``.

    Lays down (with ``state_root()`` resolved from ``paths.root`` —
    the operator's single configured root):

    * ``<state_root>/hooks/supervisor_hook.py`` + ``_supervisor_state.py``
      — the OCI hook entrypoint and its stdlib-only ballast.
    * ``<state_root>/hooks/terok-sandbox-supervisor-<stage>.json`` — one
      OCI hook descriptor per stage (createRuntime + poststop).
      ``containers.conf`` is patched at install time to list
      ``state_root() / "hooks"`` in ``hooks_dir`` so podman scans the
      canonical terok-owned directory.
    * ``<state_root>/supervisor_wrapper.py`` — the restart-loop the
      hook spawns, with the ``terok-sandbox`` argv baked in at
      install time.

    Idempotent: re-running overwrites the installed files with the
    current package's copies.  Soft-fails on a missing
    ``terok-sandbox`` entry point (degraded install — operator hasn't
    sourced the venv yet).
    """
    from .supervisor.install import install_supervisor_hooks

    with _stage_line("Supervisor hooks") as s:
        try:
            install_supervisor_hooks()
        except Exception as exc:  # noqa: BLE001 — aggregator uniformity
            s.fail(str(exc))
            return False
        s.ok("installed (OCI hook + wrapper)")
        return True

run_supervisor_uninstall_phase()

Remove every file run_supervisor_install_phase would write.

Idempotent — missing files are tolerated. Leaves any per- container PID files / log files alone; the operator can sweep those manually if a wrapper crashed in a way that left state behind (the wrapper's PID file is unlinked at poststop in the happy path).

Source code in src/terok_sandbox/_setup.py
def run_supervisor_uninstall_phase() -> bool:
    """Remove every file [`run_supervisor_install_phase`][terok_sandbox._setup.run_supervisor_install_phase] would write.

    Idempotent — missing files are tolerated.  Leaves any per-
    container PID files / log files alone; the operator can sweep
    those manually if a wrapper crashed in a way that left state
    behind (the wrapper's PID file is unlinked at poststop in the
    happy path).
    """
    from .supervisor.install import uninstall_supervisor_hooks

    with _stage_line("Supervisor hooks") as s:
        try:
            uninstall_supervisor_hooks()
        except Exception as exc:  # noqa: BLE001
            s.fail(str(exc))
            return False
        s.ok("removed")
        return True

run_shield_install_phase()

Install shield OCI hooks into the canonical terok-owned dir.

Source code in src/terok_sandbox/_setup.py
def run_shield_install_phase() -> bool:
    """Install shield OCI hooks into the canonical terok-owned dir."""
    from .integrations.shield import ShieldHooks, check_environment

    with _stage_line("Shield hooks") as s:
        try:
            ShieldHooks.install()
        except Exception as exc:  # noqa: BLE001 — aggregator reports all failures uniformly
            s.fail(str(exc))
            return False

        env = check_environment()
        if env.health == "ok":
            s.ok("active")
            return True
        if env.health == "bypass":
            s.warn("bypass_firewall_no_protection is active")
            return True
        s.fail(f"installed but health: {env.health}")
        return False

run_shield_uninstall_phase()

Remove shield OCI hooks from the canonical terok-owned dir.

Source code in src/terok_sandbox/_setup.py
def run_shield_uninstall_phase() -> bool:
    """Remove shield OCI hooks from the canonical terok-owned dir."""
    from .integrations.shield import ShieldHooks

    with _stage_line("Shield hooks") as s:
        try:
            ShieldHooks.uninstall()
        except Exception as exc:  # noqa: BLE001 — aggregator uniform error surface
            s.fail(str(exc))
            return False
        s.ok("removed")
        return True

run_legacy_install_cleanup_phase()

Sweep systemd units / sockets / install paths left by pre-supervisor versions.

One-way cleanup. Idempotent — every step soft-fails so a missing systemctl, an absent unit, or a stale socket cannot abort the rest of the sweep. Runs once during terok-sandbox setup; the per-container supervisor lifecycle never invokes it.

Sweeps:

  • the legacy clearance trio (terok-clearance-hub.service, terok-clearance-verdict.service, terok-clearance-notifier.service) from the W5 layout;
  • the legacy vault systemd units (terok-vault.service / terok-vault.socket / terok-vault-socket.service);
  • the legacy gate systemd units (terok-gate.socket / terok-gate@.service / terok-gate-socket.service) now that the gate lives in the per-container supervisor;
  • any terok-clearance-*.service / terok-vault-* / terok-gate* files lingering in the user's systemd unit directory (catches renamed variants from prior alphas);
  • the legacy global shield-events socket ($XDG_RUNTIME_DIR/terok-shield-events.sock) from the single-hub-socket era.

Operators upgrading from a pre-supervisor install lose access to old tasks (per the hard rule: no state preservation across the refactor); the cleanup is purely about removing the host-side machinery that would fight a fresh setup for sockets / unit names.

Source code in src/terok_sandbox/_setup.py
def run_legacy_install_cleanup_phase() -> bool:
    """Sweep systemd units / sockets / install paths left by pre-supervisor versions.

    One-way cleanup.  Idempotent — every step soft-fails so a missing
    ``systemctl``, an absent unit, or a stale socket cannot abort the
    rest of the sweep.  Runs once during ``terok-sandbox setup``; the
    per-container supervisor lifecycle never invokes it.

    Sweeps:

    * the legacy clearance trio (``terok-clearance-hub.service``,
      ``terok-clearance-verdict.service``,
      ``terok-clearance-notifier.service``) from the W5 layout;
    * the legacy vault systemd units
      (``terok-vault.service`` / ``terok-vault.socket`` /
      ``terok-vault-socket.service``);
    * the legacy gate systemd units
      (``terok-gate.socket`` / ``terok-gate@.service`` /
      ``terok-gate-socket.service``) now that the gate lives in the
      per-container supervisor;
    * any ``terok-clearance-*.service`` / ``terok-vault-*`` /
      ``terok-gate*`` files lingering in the user's systemd unit
      directory (catches renamed variants from prior alphas);
    * the legacy global shield-events socket
      (``$XDG_RUNTIME_DIR/terok-shield-events.sock``) from the
      single-hub-socket era.

    Operators upgrading from a pre-supervisor install lose access to old
    tasks (per the hard rule: no state preservation across the
    refactor); the cleanup is purely about removing the *host-side*
    machinery that would fight a fresh setup for sockets / unit names.
    """
    with _stage_line("Legacy install cleanup") as s:
        _disable_legacy_units(_LEGACY_SYSTEMD_UNITS)
        _sweep_legacy_unit_files()
        _systemctl.run_best_effort("daemon-reload")
        _unlink_legacy_shield_events_socket()
        _unlink_legacy_runtime_sockets()
        _unlink_legacy_xdg_data_files()
        _unlink_legacy_shield_global_hooks()
        s.ok("swept (legacy units + sockets, if any)")
        return True