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.

run_prereq_report(cfg)

Print host prerequisites; return the SELinux and AppArmor results.

The results let the caller decide whether to fail the setup and re-surface each install hint at the end of output — sandbox#854's fix for the install command getting buried mid-output — from the same observation the stage lines above reported. 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) -> tuple[SelinuxCheckResult, AppArmorCheckResult]:
    """Print host prerequisites; return the SELinux and AppArmor results.

    The results let the caller decide whether to fail the setup and
    re-surface each install hint at the end of output — sandbox#854's
    fix for the install command getting buried mid-output — from the
    same observation the stage lines above reported.  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_git_http_backend()
    _report_firewall_binaries()
    if cfg.experimental:
        _report_krun_binaries()
    apparmor = _report_apparmor()
    return _report_selinux(cfg), apparmor

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. The verb it names owns the detail (state, destination, exact command, rules), so this stays a pointer plus the alternative the verb does not offer.

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.  The verb it names
    owns the detail (state, destination, exact command, rules), so this
    stays a pointer plus the alternative the verb does not offer.
    """
    if not result.status.action_needed:
        return
    print()
    print("─ SELinux policy required ─────────────────────────────────────")
    print("Socket-transport services need the terok_socket_t policy loaded;")
    print("without it, containers cannot reach the host sockets.")
    print()
    print(f"  {setup_invocation()} selinux")
    print()
    print("Or switch to TCP mode (no SELinux policy needed):")
    print()
    print("  yq -yi '.services.mode = \"tcp\"' ~/.config/terok/config.yml")
    print(f"  {setup_invocation()}")
    print()

print_apparmor_install_hint(result)

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

No-op unless the dnsmasq profile addendum is missing or outdated. Rendered last (alongside the SELinux hint) so the command isn't scrolled away. Takes the result run_prereq_report already observed — one probe, one verdict.

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

    No-op unless the dnsmasq profile addendum is missing or outdated.
    Rendered last (alongside the SELinux hint) so the command isn't
    scrolled away.  Takes the result
    [`run_prereq_report`][terok_sandbox._setup.run_prereq_report]
    already observed — one probe, one verdict.
    """
    if not result.status.action_needed:
        return
    print()
    print("─ AppArmor profile recommended ────────────────────────────────")
    print("dnsmasq is AppArmor-confined here; without the terok addendum the")
    print("per-container DNS drops to the lookup tier (no live IP-rotation).")
    print()
    print(f"  {setup_invocation()} apparmor")
    print()

run_supervisor_install_phase(*, root=None)

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(*, root: Path | None = None) -> 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(root=root)
        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(*, root=None)

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(*, root: Path | None = None) -> 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(root=root)
        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 == "disabled":
            s.warn("disable_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.

Only obsolete host-side installation artifacts are removed. Task state, credentials, and operator configuration are preserved.

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.

    Only obsolete host-side installation artifacts are removed. Task state,
    credentials, and operator configuration are preserved.
    """
    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()
        with contextlib.suppress(OSError):
            (namespace_state_dir() / _LEGACY_SETUP_STAMP).unlink(missing_ok=True)
        s.ok("swept (legacy units + sockets, if any)")
        return True