From 4e49762a85ce58f87e0d6847baa60d53d5d5fe41 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:24:22 -0400 Subject: [PATCH] docs: add linux hosting validation runbook --- README.md | 1 + docs/linux-hosting-validation.md | 178 +++++++++++++++++++++++++++++++ install.sh | 3 + scripts/linux-recovery-check.sh | 126 ++++++++++++++++++++++ tests/test_integrity.py | 11 ++ voxvera-install.sh | 3 + 6 files changed, 322 insertions(+) create mode 100644 docs/linux-hosting-validation.md create mode 100644 scripts/linux-recovery-check.sh diff --git a/README.md b/README.md index 21df3e8..afbcdd5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Linux with systemd is the supported persistent-host deployment target. Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey remain experimental until their background Tor hosting and auto-recovery paths are validated end-to-end. See [docs/platform-support-matrix.md](docs/platform-support-matrix.md) for the canonical platform status table and [docs/roadmap-platform-parity.md](docs/roadmap-platform-parity.md) for the parity plan. +For the supported Linux hosting path, use [docs/linux-hosting-validation.md](docs/linux-hosting-validation.md) as the operational validation runbook. ### 1. Standalone Binaries (Recommended) Download the executable for your OS: diff --git a/docs/linux-hosting-validation.md b/docs/linux-hosting-validation.md new file mode 100644 index 0000000..7483d73 --- /dev/null +++ b/docs/linux-hosting-validation.md @@ -0,0 +1,178 @@ +# Linux Hosting Validation + +This runbook defines the operational validation standard for VoxVera's only supported hosting target: + +- Linux CLI with `systemd --user` + +Everything else remains below that bar until equivalent evidence exists. + +## Prerequisites + +- `voxvera` installed and on `PATH` +- `tor` installed +- `onionshare-cli` available +- a user account that can run `systemctl --user` + +## Quick Verification + +Use the helper script first: + +```bash +bash scripts/linux-recovery-check.sh +``` + +This checks: + +- `platform-status --json` +- `doctor --json` +- `autostart status --json` +- expected Linux support tier and platform id +- presence of `~/host` +- Tor and OnionShare discovery +- autostart artifacts and timer state +- per-site state for `.onionshare-session`, `onionshare.log`, and `server.pid` + +## Core Commands + +Install autostart: + +```bash +voxvera autostart +``` + +Inspect autostart: + +```bash +voxvera autostart status +systemctl --user status voxvera-start.timer +systemctl --user status voxvera-start.service +``` + +Remove autostart: + +```bash +voxvera autostart uninstall +``` + +Inspect runtime diagnostics: + +```bash +voxvera doctor +voxvera doctor --json +``` + +## Validation Checklist + +### 1. Fresh install + +Run the Linux installer or install through the supported CLI path, then confirm: + +```bash +voxvera platform-status --json +voxvera doctor --json +voxvera autostart status --json +``` + +Expected: + +- `platform_id` is `linux_cli_systemd` +- support tier is `supported` +- `voxvera_cli`, `onionshare_cli`, and `tor_binary` pass + +### 2. Build and serve + +Create or edit a site, then build and serve it: + +```bash +voxvera build +voxvera serve +``` + +Expected: + +- site output appears under `~/host//` +- `index.html` and QR code images exist +- `onionshare.log` is written +- `.onionshare-session` appears after the hidden service is started + +### 3. Identity persistence + +Confirm the site keeps its identity across rebuilds: + +```bash +ls -la ~/host// +voxvera build --config ~/host//config.json +``` + +Expected: + +- `.onionshare-session` still exists after rebuild + +### 4. Autostart persistence + +Confirm the recovery timer exists and is enabled: + +```bash +voxvera autostart status +systemctl --user is-enabled voxvera-start.timer +systemctl --user is-active voxvera-start.timer +``` + +Expected: + +- timer artifacts exist under `~/.config/systemd/user/` +- timer is enabled + +### 5. Reboot recovery + +After a reboot, verify: + +```bash +bash scripts/linux-recovery-check.sh +systemctl --user status voxvera-start.timer +journalctl --user -u voxvera-start.service --no-pager -n 50 +``` + +Expected: + +- timer remains enabled +- service has retried `start-all` +- configured sites are restorable without manual reconfiguration + +### 6. Offline/reconnect recovery + +Temporarily disconnect the host from the network, reconnect, then wait through at least one timer interval. + +Verify: + +```bash +systemctl --user status voxvera-start.timer +journalctl --user -u voxvera-start.service --no-pager -n 50 +bash scripts/linux-recovery-check.sh +``` + +Expected: + +- timer is still active +- `start-all` retries appear in the service logs +- Tor becomes reachable again + +## Paths That Matter + +- `~/.config/systemd/user/voxvera-start.service` +- `~/.config/systemd/user/voxvera-start.timer` +- `~/host//config.json` +- `~/host//.onionshare-session` +- `~/host//onionshare.log` +- `~/host//server.pid` + +## Failure Interpretation + +If `bash scripts/linux-recovery-check.sh` fails: + +- missing `onionshare_cli` or `tor_binary` means the runtime is incomplete +- missing `host_root` means no sites are built yet +- missing autostart artifacts means recovery was never installed or was removed +- `tor_socks_reachable` warning means Tor is not currently reachable even if it is installed + +Linux should remain the only supported target until this runbook is repeatable on real hosts without caveats. diff --git a/install.sh b/install.sh index 5b99662..20cc693 100755 --- a/install.sh +++ b/install.sh @@ -442,6 +442,9 @@ for strategy in install_tarball install_shallow install_pipx install_pip_git ins msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify." if [[ "$OS_NAME" == linux* ]]; then msg "Linux systemd user autostart should now retry hidden-service startup every few minutes." + msg "Inspect with: voxvera autostart status" + msg "Remove with: voxvera autostart uninstall" + msg "Validate with: bash scripts/linux-recovery-check.sh" fi # Detect if the parent shell will still have a stale hash msg "If 'voxvera' is not found, run: hash -r (or open a new terminal)" diff --git a/scripts/linux-recovery-check.sh b/scripts/linux-recovery-check.sh new file mode 100644 index 0000000..4296e3c --- /dev/null +++ b/scripts/linux-recovery-check.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "linux-recovery-check.sh is only for Linux hosts." >&2 + exit 1 +fi + +command -v voxvera >/dev/null 2>&1 || { + echo "voxvera is not on PATH" >&2 + exit 1 +} + +TMP_DIR="${VOXVERA_RECOVERY_TMPDIR:-$(mktemp -d /tmp/voxvera-linux-recovery-XXXXXX)}" +cleanup() { + if [[ -z "${VOXVERA_RECOVERY_TMPDIR:-}" ]]; then + rm -rf "$TMP_DIR" + fi +} +trap cleanup EXIT + +PLATFORM_JSON="$TMP_DIR/platform-status.json" +DOCTOR_JSON="$TMP_DIR/doctor.json" +AUTOSTART_JSON="$TMP_DIR/autostart-status.json" + +voxvera platform-status --json >"$PLATFORM_JSON" +voxvera doctor --json >"$DOCTOR_JSON" +voxvera autostart status --json >"$AUTOSTART_JSON" + +PLATFORM_JSON="$PLATFORM_JSON" \ +DOCTOR_JSON="$DOCTOR_JSON" \ +AUTOSTART_JSON="$AUTOSTART_JSON" \ +python3 - <<'PY' +import json +import os +import sys +from pathlib import Path + +platform_status = json.loads(Path(os.environ["PLATFORM_JSON"]).read_text(encoding="utf-8")) +doctor = json.loads(Path(os.environ["DOCTOR_JSON"]).read_text(encoding="utf-8")) +autostart = json.loads(Path(os.environ["AUTOSTART_JSON"]).read_text(encoding="utf-8")) + +errors = [] +warnings = [] + +if platform_status.get("platform_id") != "linux_cli_systemd": + errors.append(f"expected linux_cli_systemd, got {platform_status.get('platform_id')}") +if platform_status.get("tier") != "supported": + errors.append(f"expected supported tier, got {platform_status.get('tier')}") + +checks = {check["name"]: check for check in doctor.get("checks", [])} + +required_ok = ["platform", "voxvera_cli", "onionshare_cli", "tor_binary", "host_root"] +for name in required_ok: + check = checks.get(name) + if not check: + errors.append(f"missing doctor check: {name}") + elif not check.get("ok"): + errors.append(f"{name} failed: {check.get('detail')}") + +tor_check = checks.get("tor_socks_reachable") +if not tor_check: + errors.append("missing doctor check: tor_socks_reachable") +elif not tor_check.get("ok"): + warnings.append(f"tor_socks_reachable: {tor_check.get('detail')}") + +timer_check = next((check for check in autostart.get("checks", []) if check["name"] == "systemd_timer_enabled"), None) +if not timer_check: + errors.append("missing autostart check: systemd_timer_enabled") +elif not timer_check.get("ok"): + warnings.append(f"systemd_timer_enabled: {timer_check.get('detail')}") + +artifacts = [Path(path) for path in autostart.get("artifacts", [])] +missing = [str(path) for path in artifacts if not path.exists()] +if missing: + warnings.append("missing autostart artifacts: " + ", ".join(missing)) + +host_root = Path.home() / "host" +sites = [] +if host_root.exists(): + for site_dir in sorted(path for path in host_root.iterdir() if path.is_dir()): + config = site_dir / "config.json" + if not config.exists(): + continue + session = site_dir / ".onionshare-session" + log = site_dir / "onionshare.log" + pid = site_dir / "server.pid" + sites.append( + { + "name": site_dir.name, + "session": session.exists(), + "log": log.exists(), + "pid": pid.exists(), + } + ) + +if errors: + print("Linux recovery check failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + sys.exit(1) + +print(f"Platform: {platform_status['label']} [{platform_status['tier']}]") +print(f"Hosting model: {platform_status.get('hosting_model', '')}") +print(f"Autostart: {autostart.get('message', '')}") +if warnings: + print("Warnings:") + for warning in warnings: + print(f"- {warning}") +else: + print("Warnings: none") + +if sites: + print("Site state:") + for site in sites: + print( + f"- {site['name']}: session={'yes' if site['session'] else 'no'}, " + f"log={'yes' if site['log'] else 'no'}, pid={'yes' if site['pid'] else 'no'}" + ) +else: + print("Site state: no configured sites under ~/host") +PY + +echo "Detailed JSON artifacts written to $TMP_DIR" diff --git a/tests/test_integrity.py b/tests/test_integrity.py index 6f8081a..3a573db 100644 --- a/tests/test_integrity.py +++ b/tests/test_integrity.py @@ -45,6 +45,17 @@ def test_platform_smoke_is_wired_into_validation_surfaces(): for target in ("linux-cli", "macos-cli", "windows-cli", "docker-cli"): assert target in smoke_text + +def test_linux_validation_runbook_and_helper_are_linked(): + runbook = REPO_ROOT / "docs" / "linux-hosting-validation.md" + helper = REPO_ROOT / "scripts" / "linux-recovery-check.sh" + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + + assert runbook.exists() + assert helper.exists() + assert "docs/linux-hosting-validation.md" in readme + assert "scripts/linux-recovery-check.sh" in runbook.read_text(encoding="utf-8") + def test_locale_completeness(): """Verify that all non-English locales have the same keys as English.""" locales_dir = REPO_ROOT / "voxvera" / "locales" diff --git a/voxvera-install.sh b/voxvera-install.sh index 1238d9f..55e5998 100644 --- a/voxvera-install.sh +++ b/voxvera-install.sh @@ -206,4 +206,7 @@ msg " Linux is the supported persistent-host target." msg " Other package/install paths remain experimental." msg "" msg " Run 'voxvera check' to verify your setup after refreshing." +msg " Run 'voxvera autostart status' to inspect the recovery timer." +msg " Run 'voxvera autostart uninstall' to remove the recovery timer." +msg " Run 'bash scripts/linux-recovery-check.sh' from the repo for the Linux validation checklist." msg "------------------------------------------------------------------"