ai-viewer Deployment
This skill is the durable record of how ai-viewer gets installed and run on the operator's workstation. Load it for any install/upgrade/uninstall, source-discovery or permission issue, or "how do I use this" question. The contract lives in .agents/sow/specs/deployment.md; this file is the operational quick-reference.
TL;DR
- System install (recommended):
scripts/install-system.sh → binaries + data under /opt/ai-viewer, two system systemd units running as the operator, UI at http://127.0.0.1:7710/.
- User install (no root):
scripts/install-systemd-user.sh → binaries in ~/.local/bin, user systemd units, data in ~/.local/share/ai-viewer/.
- Both are localhost-only (no auth in v1 — workstation-scoped).
The system install (scripts/install-system.sh)
What it does
- Rebuilds from source (
scripts/build.sh — frontend + both Go binaries).
- Probes the installing operator's well-known agent-data paths and renders
--source flags for each that exists:
~/.ai-agent/sessions → aiagent_v3 + aiagent_v2 (same dir backs both)
~/.claude/projects → claude-code
~/.codex/sessions → codex
~/.local/share/opencode/opencode.db → opencode
- Lays out
/opt/ai-viewer/{bin,data,logs}, atomically stages each rebuilt binary and renames it into place.
- Renders the unit templates (
deploy/systemd-system/ai-viewer-{ingest,serve}.service) — substituting __OPERATOR_USER__, __OPERATOR_GROUP__, and __AI_VIEWER_SOURCES__ — into /etc/systemd/system/.
systemctl daemon-reload, enables both units, stops active units, fixes
ownership/permissions while they are down, starts the ingester, verifies
ingester liveness, starts and verifies the server, re-checks ingester
liveness, then prints the URL.
Commands
scripts/install-system.sh # install or upgrade (rebuilds) + start + verify + print URL
scripts/install-system.sh status # systemctl status for both + the URL
scripts/install-system.sh uninstall # stop + disable + remove units + /opt/ai-viewer (data is disposable)
Layout
| Path |
Purpose |
/opt/ai-viewer/bin/ai-viewer-{ingest,serve} |
binaries (rebuilt from source each install) |
/opt/ai-viewer/data/index.db |
SQLite index (owned by the operator) |
/opt/ai-viewer/logs/ |
fallback log dir (journald is the primary sink) |
/etc/systemd/system/ai-viewer-{ingest,serve}.service |
rendered system units (committed templates: deploy/systemd-system/) |
Two design decisions baked in (do NOT re-litigate without a new reason)
1. Runs as the operator (User=<operator>), NOT a dedicated service user.
The operator's agent-data dirs (~/.ai-agent, ~/.claude, ~/.codex, ~/.local/share/opencode) are owner-only (0700), so a dedicated ai-viewer user cannot read them without invasive recursive ACLs (fragile — new files don't inherit unless default ACLs are set; surprises dotfile managers/backup tools). Running as the operator is correct and simple here:
- The app's purpose is "the operator's viewer for the operator's agent data"; localhost-only; read-only on sources by design (
os.O_RDONLY).
- It's the same privilege level the operator already grants every other agent CLI they run (opencode/codex/claude — all run as the operator).
- A dedicated user for this one tool is security theater relative to the rest of the operator's stack and was actively breaking source access (the 0-sources bug during initial install — see History).
- The systemd hardening stays (
NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, PrivateTmp, ReadWritePaths scoped to the data dir for the ingester).
2. Explicit --source flags in the unit, NOT auto-discovery.
Auto-discovery probes $HOME-relative paths. The explicit list is (a) auditable — the unit file shows exactly what the service reads, and (b) independent of $HOME resolution (irrelevant now that we run as the operator, but it survived because it's the cleaner contract). The committed template carries __AI_VIEWER_SOURCES__; the install script renders it.
Verifying an install
systemctl is-active ai-viewer-ingest.service ai-viewer-serve.service # both → active
curl -s http://127.0.0.1:7710/api/health | python3 -m json.tool # status + sources[] (want len ≥ 1)
journalctl -u ai-viewer-ingest -u ai-viewer-serve -f # live tail
scripts/install-system.sh status # the bundled status command
/api/health.status is degraded on a fresh install until sources catch up (parse errors from real-data edge cases + historical staleness) — that's correct (errors are surfaced, not hidden). It is NOT an install failure.
/api/health.sources is empty for the first few seconds (sources register on the first batch flush, after the adapters scan) — wait, don't panic.
- The ingester liveness check catches immediate startup failures. A delayed
migration/startup failure after the poll window is diagnosed from
systemctl status, bounded journalctl output, and the effective
StartLimit* values from systemctl show.
Operating
- Logs:
journalctl -u ai-viewer-ingest -u ai-viewer-serve -f (journald via stdout; the primary sink).
- Restart after a code change:
scripts/install-system.sh (uninstall is NOT needed — install is idempotent; it rebuilds + atomically replaces binaries + re-renders + daemon-reload + stop/start verification).
- Data is disposable.
/opt/ai-viewer/data/index.db is derived. Deleting it (or uninstall) triggers a full re-ingest on next start; the source files are the source of truth. Never back up the index.
- Read-only on sources. Neither binary writes to
~/.ai-agent/~/.claude/etc. The ingester opens source files os.O_RDONLY; the server never touches them.
- Port:
7710 (the spec default; 127.0.0.1:7710). Changing it requires editing the rendered serve unit + the frontend's Vite proxy target (dev only).
- Graceful stop budget: all system and user units use
TimeoutStopSec=45s. The system ingester also has MemoryHigh=4G,
MemoryMax=8G, LimitNOFILE=65536, IOSchedulingClass=idle, and
OOMPolicy=stop.
- Writer one-shots: for a system install, commands such as
rollups-backfill, fts-content-backfill, and reprice that target
/opt/ai-viewer/data/index.db must also pass
--state-dir /opt/ai-viewer/data so they detect the running system daemon
lock before opening a writer handle.
Common issues
sources: 0 after install. Either the units run as the wrong user (permission denied on the 0700 home dirs — re-run install so it renders User=<operator>), or you're checking too early (sources register on first batch flush — wait ~5s).
permission denied on source paths in the ingest log. The unit's User= doesn't own the data. Confirm /etc/systemd/system/ai-viewer-ingest.service has User=<operator> (the install script renders this from SUDO_USER/$USER).
- Server not responding after 20s.
systemctl status ai-viewer-serve + journalctl -u ai-viewer-serve — the usual cause is the schema-not-ready start-order race (the serve unit's Restart=on-failure cycles until the ingester migrates); give it another few seconds.
- Ingester start/restart loop. Inspect
systemctl status ai-viewer-ingest,
journalctl -u ai-viewer-ingest, and
systemctl show ai-viewer-ingest.service -p StartLimitBurst -p StartLimitIntervalUSec.
The project does not hard-code StartLimit* values; use the host's effective
settings when diagnosing migration failures.
bad substitution / sed errors from the installer. The script renders the unit via sed (no -o); if you edit it, don't reintroduce sed -o.
The user install (alternative, no root)
scripts/install-systemd-user.sh — binaries to ~/.local/bin, user systemd units (systemctl --user), data in ~/.local/share/ai-viewer/. Uses auto-discovery (works because it runs as the operator under the user manager). Same ports, same localhost-only. Use this when root is unavailable; the system install is otherwise preferred (runs at boot without a login session).
History (why these decisions)
- Initial system install used a dedicated
ai-viewer user + auto-discovery → discovered 0 sources. Root cause #1: auto-discovery probes $HOME-relative paths, but the service user's $HOME was /home/ai-viewer. Fixed by making sources explicit.
- Re-rendered with explicit sources, still 0 sources →
permission denied on every source path. Root cause #2: the operator's ~/.ai-agent etc. are 0700, so even with correct paths the dedicated user couldn't traverse them. Tried ACLs; rejected as fragile/invasive.
- Final: run as the operator + explicit sources. Correct, simple, matches the mental model, keeps the hardening. Recorded here so the dedicated-user idea is not retried without a genuinely new reason (e.g. the app gains a network-exposed surface — then least-privilege outweighs simplicity and this decision is revisited in its own SOW).
- The deployment spec (
.agents/sow/specs/deployment.md §"System Install") is the authoritative contract; this skill is the quick-reference + the reasoning.
1---2name: project-deployment3description: Install, run, and operate ai-viewer on the operator's workstation — the system install (/opt/ai-viewer, runs-as-operator, explicit --source flags, systemd units), the user install alternative, build/run/upgrade/uninstall commands, and the threat-model reasoning behind the run-as-operator decision. Use when installing or upgrading ai-viewer, debugging source-discovery / permission-denied errors, or answering "how do I run this / what port / where's the data".4---56# ai-viewer Deployment78This skill is the durable record of how ai-viewer gets installed and run on the operator's workstation. Load it for any install/upgrade/uninstall, source-discovery or permission issue, or "how do I use this" question. The contract lives in `.agents/sow/specs/deployment.md`; this file is the operational quick-reference.910## TL;DR1112- **System install (recommended):** `scripts/install-system.sh` → binaries + data under `/opt/ai-viewer`, two **system** systemd units running **as the operator**, UI at **<http://127.0.0.1:7710/>**.13- **User install (no root):** `scripts/install-systemd-user.sh` → binaries in `~/.local/bin`, **user** systemd units, data in `~/.local/share/ai-viewer/`.14- Both are localhost-only (no auth in v1 — workstation-scoped).1516## The system install (`scripts/install-system.sh`)1718### What it does19201. Rebuilds from source (`scripts/build.sh` — frontend + both Go binaries).212. Probes the **installing operator's** well-known agent-data paths and renders `--source` flags for each that exists:22 - `~/.ai-agent/sessions` → `aiagent_v3` + `aiagent_v2` (same dir backs both)23 - `~/.claude/projects` → `claude-code`24 - `~/.codex/sessions` → `codex`25 - `~/.local/share/opencode/opencode.db` → `opencode`263. Lays out `/opt/ai-viewer/{bin,data,logs}`, atomically stages each rebuilt binary and renames it into place.274. Renders the unit templates (`deploy/systemd-system/ai-viewer-{ingest,serve}.service`) — substituting `__OPERATOR_USER__`, `__OPERATOR_GROUP__`, and `__AI_VIEWER_SOURCES__` — into `/etc/systemd/system/`.285. `systemctl daemon-reload`, enables both units, stops active units, fixes29 ownership/permissions while they are down, starts the ingester, verifies30 ingester liveness, starts and verifies the server, re-checks ingester31 liveness, then prints the URL.3233### Commands3435```bash36scripts/install-system.sh # install or upgrade (rebuilds) + start + verify + print URL37scripts/install-system.sh status # systemctl status for both + the URL38scripts/install-system.sh uninstall # stop + disable + remove units + /opt/ai-viewer (data is disposable)39```4041### Layout4243| Path | Purpose |44|---|---|45| `/opt/ai-viewer/bin/ai-viewer-{ingest,serve}` | binaries (rebuilt from source each install) |46| `/opt/ai-viewer/data/index.db` | SQLite index (owned by the operator) |47| `/opt/ai-viewer/logs/` | fallback log dir (journald is the primary sink) |48| `/etc/systemd/system/ai-viewer-{ingest,serve}.service` | rendered system units (committed templates: `deploy/systemd-system/`) |4950### Two design decisions baked in (do NOT re-litigate without a new reason)5152**1. Runs as the operator (`User=<operator>`), NOT a dedicated service user.**53The operator's agent-data dirs (`~/.ai-agent`, `~/.claude`, `~/.codex`, `~/.local/share/opencode`) are owner-only (`0700`), so a dedicated `ai-viewer` user **cannot read them** without invasive recursive ACLs (fragile — new files don't inherit unless default ACLs are set; surprises dotfile managers/backup tools). Running as the operator is correct and simple here:5455- The app's purpose is "the operator's viewer for the operator's agent data"; localhost-only; read-only on sources by design (`os.O_RDONLY`).56- It's the same privilege level the operator already grants every other agent CLI they run (opencode/codex/claude — all run as the operator).57- A dedicated user for *this one tool* is security theater relative to the rest of the operator's stack and was actively breaking source access (the 0-sources bug during initial install — see History).58- The systemd hardening stays (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome=read-only`, `PrivateTmp`, `ReadWritePaths` scoped to the data dir for the ingester).5960**2. Explicit `--source` flags in the unit, NOT auto-discovery.**61Auto-discovery probes `$HOME`-relative paths. The explicit list is (a) auditable — the unit file shows exactly what the service reads, and (b) independent of `$HOME` resolution (irrelevant now that we run as the operator, but it survived because it's the cleaner contract). The committed template carries `__AI_VIEWER_SOURCES__`; the install script renders it.6263### Verifying an install6465```bash66systemctl is-active ai-viewer-ingest.service ai-viewer-serve.service # both → active67curl -s http://127.0.0.1:7710/api/health | python3 -m json.tool # status + sources[] (want len ≥ 1)68journalctl -u ai-viewer-ingest -u ai-viewer-serve -f # live tail69scripts/install-system.sh status # the bundled status command70```7172- `/api/health.status` is `degraded` on a fresh install until sources catch up (parse errors from real-data edge cases + historical staleness) — that's **correct** (errors are surfaced, not hidden). It is NOT an install failure.73- `/api/health.sources` is empty for the first few seconds (sources register on the first batch flush, after the adapters scan) — wait, don't panic.74- The ingester liveness check catches immediate startup failures. A delayed75 migration/startup failure after the poll window is diagnosed from76 `systemctl status`, bounded `journalctl` output, and the effective77 `StartLimit*` values from `systemctl show`.7879## Operating8081- **Logs:** `journalctl -u ai-viewer-ingest -u ai-viewer-serve -f` (journald via stdout; the primary sink).82- **Restart after a code change:** `scripts/install-system.sh` (uninstall is NOT needed — install is idempotent; it rebuilds + atomically replaces binaries + re-renders + `daemon-reload` + stop/start verification).83- **Data is disposable.** `/opt/ai-viewer/data/index.db` is derived. Deleting it (or `uninstall`) triggers a full re-ingest on next start; the source files are the source of truth. Never back up the index.84- **Read-only on sources.** Neither binary writes to `~/.ai-agent`/`~/.claude`/etc. The ingester opens source files `os.O_RDONLY`; the server never touches them.85- **Port:** `7710` (the spec default; `127.0.0.1:7710`). Changing it requires editing the rendered serve unit + the frontend's Vite proxy target (dev only).86- **Graceful stop budget:** all system and user units use87 `TimeoutStopSec=45s`. The system ingester also has `MemoryHigh=4G`,88 `MemoryMax=8G`, `LimitNOFILE=65536`, `IOSchedulingClass=idle`, and89 `OOMPolicy=stop`.90- **Writer one-shots:** for a system install, commands such as91 `rollups-backfill`, `fts-content-backfill`, and `reprice` that target92 `/opt/ai-viewer/data/index.db` must also pass93 `--state-dir /opt/ai-viewer/data` so they detect the running system daemon94 lock before opening a writer handle.9596## Common issues9798- **`sources: 0` after install.** Either the units run as the wrong user (permission denied on the `0700` home dirs — re-run install so it renders `User=<operator>`), or you're checking too early (sources register on first batch flush — wait ~5s).99- **`permission denied` on source paths in the ingest log.** The unit's `User=` doesn't own the data. Confirm `/etc/systemd/system/ai-viewer-ingest.service` has `User=<operator>` (the install script renders this from `SUDO_USER`/`$USER`).100- **Server not responding after 20s.** `systemctl status ai-viewer-serve` + `journalctl -u ai-viewer-serve` — the usual cause is the schema-not-ready start-order race (the serve unit's `Restart=on-failure` cycles until the ingester migrates); give it another few seconds.101- **Ingester start/restart loop.** Inspect `systemctl status ai-viewer-ingest`,102 `journalctl -u ai-viewer-ingest`, and103 `systemctl show ai-viewer-ingest.service -p StartLimitBurst -p StartLimitIntervalUSec`.104 The project does not hard-code `StartLimit*` values; use the host's effective105 settings when diagnosing migration failures.106- **`bad substitution` / sed errors from the installer.** The script renders the unit via `sed` (no `-o`); if you edit it, don't reintroduce `sed -o`.107108## The user install (alternative, no root)109110`scripts/install-systemd-user.sh` — binaries to `~/.local/bin`, **user** systemd units (`systemctl --user`), data in `~/.local/share/ai-viewer/`. Uses auto-discovery (works because it runs as the operator under the user manager). Same ports, same localhost-only. Use this when root is unavailable; the system install is otherwise preferred (runs at boot without a login session).111112## History (why these decisions)113114- **Initial system install used a dedicated `ai-viewer` user + auto-discovery** → discovered 0 sources. Root cause #1: auto-discovery probes `$HOME`-relative paths, but the service user's `$HOME` was `/home/ai-viewer`. Fixed by making sources explicit.115- **Re-rendered with explicit sources, still 0 sources** → `permission denied` on every source path. Root cause #2: the operator's `~/.ai-agent` etc. are `0700`, so even with correct paths the dedicated user couldn't traverse them. Tried ACLs; rejected as fragile/invasive.116- **Final: run as the operator + explicit sources.** Correct, simple, matches the mental model, keeps the hardening. Recorded here so the dedicated-user idea is not retried without a genuinely new reason (e.g. the app gains a network-exposed surface — then least-privilege outweighs simplicity and this decision is revisited in its own SOW).117- The deployment spec (`.agents/sow/specs/deployment.md` §"System Install") is the authoritative contract; this skill is the quick-reference + the reasoning.