diff --git a/configs/qemu-ga/svos/50-svos b/configs/qemu-ga/svos/50-svos index 5195c0a..18fa8c3 100755 --- a/configs/qemu-ga/svos/50-svos +++ b/configs/qemu-ga/svos/50-svos @@ -11,7 +11,8 @@ import sys import tempfile import time -SERVICES = ('svos-kernel.service', 'svos-interface.service') +SERVICES = ('svos-kernel.service', 'svos-interface.service', 'svos-terminal.service', + 'svos-knowledge.service', 'svos-postoffice.service') class Quiesce: def __init__(self, directory, *, run=None): diff --git a/configs/qemu-ga/svos/README.md b/configs/qemu-ga/svos/README.md index 157afd3..0b3eca6 100644 --- a/configs/qemu-ga/svos/README.md +++ b/configs/qemu-ga/svos/README.md @@ -10,3 +10,6 @@ - Runs under `../fsfreeze-dispatch`, which qemu-ga calls via `/etc/qemu/qemu-ga.conf`. Without that config, Debian's qemu-ga runs no hooks at all. - If the voice service is ever enabled, add it to `SERVICES` before enabling it (svos-dev). +- 2026-10-07 (a737a06 refresh): `SERVICES` gained `svos-terminal`, `svos-knowledge` and `svos-postoffice` before + they were started. The hook restores only the units that were active at freeze, so a declared-but-stopped app + (postoffice until its bridge handle exists) is recorded as inactive and left stopped. diff --git a/configs/qemu-ga/svos/test_quiesce.py b/configs/qemu-ga/svos/test_quiesce.py index e5b0132..fd3be13 100644 --- a/configs/qemu-ga/svos/test_quiesce.py +++ b/configs/qemu-ga/svos/test_quiesce.py @@ -9,7 +9,8 @@ class QuiesceTests(unittest.TestCase): loader=importlib.machinery.SourceFileLoader('svos_quiesce',str(Path(__file__).with_name('50-svos'))) spec=importlib.util.spec_from_loader('svos_quiesce',loader) self.module=importlib.util.module_from_spec(spec);spec.loader.exec_module(self.module) - self.states={'svos-kernel.service':('active','success'),'svos-interface.service':('inactive','success')} + self.states={'svos-kernel.service':('active','success'),'svos-interface.service':('inactive','success'), + 'svos-terminal.service':('inactive','success'),'svos-knowledge.service':('inactive','success'),'svos-postoffice.service':('inactive','success')} self.calls=[];self.stop_fail=False;self.start_fail=False;self.dirty_stop=False def run(args,**kwargs): self.calls.append(args) diff --git a/configs/svos-apps/svos-knowledge.service b/configs/svos-apps/svos-knowledge.service new file mode 100644 index 0000000..31c5ff6 --- /dev/null +++ b/configs/svos-apps/svos-knowledge.service @@ -0,0 +1,30 @@ +# eshpfi configs/svos-apps/ (infra-ops, 2026-10-07, a737a06 refresh), modelled on svoperatingsystem +# deployment/nh3-svos/svos-interface.service. The credential is /etc/svos/knowledge.env (root 0600, SVOS_APP_CREDENTIAL), +# minted by enrollment on the private kernel listener. Listed in the qemu-ga quiesce hook (configs/qemu-ga/svos/50-svos). +[Unit] +Description=SVOS Knowledge-read app (app-knowledge, U14) +After=svos-kernel.service +Requires=svos-kernel.service +ConditionPathExists=/etc/svos/knowledge.env +[Service] +Type=simple +User=svc-svos +Group=svc-svos +UMask=0077 +WorkingDirectory=/var/lib/svos +EnvironmentFile=/etc/svos/knowledge.env +Environment=SVOS_KERNEL_URL=http://127.0.0.1:8400 +Environment=SVOS_DATA_ROOT=/var/lib/svos +Environment=SVOS_APP_LISTEN=127.0.0.1:8405 +ExecStart=/opt/svos/current/apps-env/bin/svos-knowledge +Restart=on-failure +RestartSec=3 +TimeoutStopSec=20 +KillMode=control-group +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +ReadWritePaths=/var/lib/svos +[Install] +WantedBy=multi-user.target diff --git a/configs/svos-apps/svos-postoffice.service b/configs/svos-apps/svos-postoffice.service new file mode 100644 index 0000000..6eba596 --- /dev/null +++ b/configs/svos-apps/svos-postoffice.service @@ -0,0 +1,30 @@ +# eshpfi configs/svos-apps/ (infra-ops, 2026-10-07, a737a06 refresh), modelled on svoperatingsystem +# deployment/nh3-svos/svos-interface.service. The credential is /etc/svos/postoffice.env (root 0600, SVOS_APP_CREDENTIAL), +# minted by enrollment on the private kernel listener. Listed in the qemu-ga quiesce hook (configs/qemu-ga/svos/50-svos). +[Unit] +Description=SVOS Post office bridge (app-postoffice, U9) +After=svos-kernel.service +Requires=svos-kernel.service +ConditionPathExists=/etc/svos/postoffice.env +[Service] +Type=simple +User=svc-svos +Group=svc-svos +UMask=0077 +WorkingDirectory=/var/lib/svos +EnvironmentFile=/etc/svos/postoffice.env +Environment=SVOS_KERNEL_URL=http://127.0.0.1:8400 +Environment=SVOS_DATA_ROOT=/var/lib/svos +Environment=SVOS_APP_LISTEN=127.0.0.1:8406 +ExecStart=/opt/svos/current/apps-env/bin/svos-postoffice +Restart=on-failure +RestartSec=3 +TimeoutStopSec=20 +KillMode=control-group +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +ReadWritePaths=/var/lib/svos +[Install] +WantedBy=multi-user.target diff --git a/configs/svos-apps/svos-terminal.service b/configs/svos-apps/svos-terminal.service new file mode 100644 index 0000000..d3e6d6f --- /dev/null +++ b/configs/svos-apps/svos-terminal.service @@ -0,0 +1,30 @@ +# eshpfi configs/svos-apps/ (infra-ops, 2026-10-07, a737a06 refresh), modelled on svoperatingsystem +# deployment/nh3-svos/svos-interface.service. The credential is /etc/svos/terminal.env (root 0600, SVOS_APP_CREDENTIAL), +# minted by enrollment on the private kernel listener. Listed in the qemu-ga quiesce hook (configs/qemu-ga/svos/50-svos). +[Unit] +Description=SVOS Terminal connector (app-terminal, U8) +After=svos-kernel.service +Requires=svos-kernel.service +ConditionPathExists=/etc/svos/terminal.env +[Service] +Type=simple +User=svc-svos +Group=svc-svos +UMask=0077 +WorkingDirectory=/var/lib/svos +EnvironmentFile=/etc/svos/terminal.env +Environment=SVOS_KERNEL_URL=http://127.0.0.1:8400 +Environment=SVOS_DATA_ROOT=/var/lib/svos +Environment=SVOS_APP_LISTEN=127.0.0.1:8404 +ExecStart=/opt/svos/current/apps-env/bin/svos-terminal +Restart=on-failure +RestartSec=3 +TimeoutStopSec=20 +KillMode=control-group +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +ReadWritePaths=/var/lib/svos +[Install] +WantedBy=multi-user.target diff --git a/configs/svos-front/Caddyfile b/configs/svos-front/Caddyfile index 9b312e7..984aab0 100644 --- a/configs/svos-front/Caddyfile +++ b/configs/svos-front/Caddyfile @@ -110,3 +110,90 @@ https://svos.phasefinal.com:443 { } } } + +# app-terminal's own origin (svos-dev 2026-10-07, U8 + U10c "a pane needs its own origin"). The interface embeds this +# page in a cross-origin iframe; the browser derives its URL from the registry's base_url +# (instance.toml: base_url = "https://svos-terminal.phasefinal.com"), so the kernel's signed calls (/health, /verbs/*) +# also arrive here, from the box itself, and pass to the connector on 127.0.0.1:8404. +# - NO Basic auth and no identity header: Chromium cancels Basic prompts for cross-origin frames and subresources, so a +# gate here kills the pane. The lease is the page's auth (GET / without one is a waiting page by ruling A105; +# /terminal/session|stream|close refuse without the bearer); /verbs/* and /health refuse without the kernel's +# Ed25519 signature. As defence in depth, /verbs/* and /health are refused unless they come from the box itself. +# - The app sets Content-Security-Policy: frame-ancestors https://svos.phasefinal.com (its frame_ancestors config); +# it passes through unmodified. /terminal/stream is a long-lived ndjson stream: no buffering (flush_interval -1) +# and no write/idle deadline (none is set anywhere in this file). Same 1 MiB body cap. +# - Mesh-only like the main site: headscale MagicDNS extra record -> 100.64.0.9; nh3-svos resolves it via /etc/hosts. +(svos_terminal_upstream) { + reverse_proxy 127.0.0.1:8404 { + flush_interval -1 + } +} + +https://svos-terminal.phasefinal.com:443 { + bind 100.64.0.9 + tls { + dns cloudflare {env.CF_API_TOKEN} + resolvers 1.1.1.1 8.8.8.8 + } + route { + request_header -Tailscale-User-* + request_header -X-Forwarded-* + request_header -Tailscale-Headers-Info + request_header -X-Real-IP + request_header -Forwarded + request_header -X-SVOS-Peer + + request_body { + max_size 1MiB + } + + # The kernel's signed calls come from the box itself (its source on the mesh address); anything else is refused. + @not_from_box { + path /verbs/* /health + not remote_ip 100.64.0.9 127.0.0.1 + } + handle @not_from_box { + respond 403 + } + + # Exactly the paths the connector serves (svos-dev 2026-10-07), method-pinned; everything else is 404. + @page { + method GET HEAD + path / /assets/* + } + handle @page { + import svos_terminal_upstream + } + @lease_get { + method GET + path /terminal/session /terminal/stream + } + handle @lease_get { + import svos_terminal_upstream + } + @lease_close { + method POST + path /terminal/close + } + handle @lease_close { + import svos_terminal_upstream + } + @verbs { + method POST + path /verbs/* + } + handle @verbs { + import svos_terminal_upstream + } + @health { + method GET + path /health + } + handle @health { + import svos_terminal_upstream + } + handle { + respond 404 + } + } +} diff --git a/persistent-memory.md b/persistent-memory.md index 4243576..efb075c 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -186,6 +186,7 @@ Every vLLM seat on a shared card (GPU 0, GPU 1) has byte-pinned KV (`--kv-cache- - `[2026-10-06]` **Scriberr diarization fixed: gated pyannote, a torchcodec/torch mismatch, and WhisperX's CPU defaults; model caches on /tank, HF_TOKEN a placeholder (Prime), GPU+speaker profiles.** Patch 0003 waits for the next rebuild (tracked at `stacks/scriberr/patches/README.md`). → `persistent-memory.d/2026-10-06-scriberr-diarization-fixed.md` - `[2026-10-06]` **Backup checker: destroyed guests reported as residue, an ssh stdin swallow fixed, snapshot age from the name; residue pruned (Prime); pbs-nh3 mirror keeps pbs-ana's history (Prime "match history"), first prune 10-07 00:00.** → `persistent-memory.d/2026-10-06-backup-checker-fixes-and-mirror-retention.md` +- `[2026-10-07]` **nh3-svos refreshed to SVOS a737a06 (schema 11) at svos-dev's ask (Prime's 2240 decision).** Live: app-knowledge (Albok token per A111), plus app-terminal, degraded by design (no seat on the box), on its own origin `svos-terminal.phasefinal.com` (no Basic auth, mesh-only). app-postoffice is declared and stopped (⚠ the bridge handle is Prime's to declare); Miranda's packages are only staged, for U13. Acceptance: 17/17 HTTPS checks and the Chromium phone/desk checks passed. ⚠ The interface fails once on thaw (it starts before the kernel listens); the restart recovers it, but the hook's single status check can race it. Reported to svos-dev. → `servers/nh3-svos/README.md` - `[2026-10-07]` **Gateway: glm-5.3 added; gen-frontier / gen-frontier-reasoning moved 5.2 → 5.3 (Prime); every GLM 5.2/5.3 entry advertises 1,048,576 in / 131,072 out with max_tokens 131,072 as the default.** 5.3 cannot turn reasoning off, so the gen-frontier pair is now effort low / high (measured ~260 vs ~1,490 reasoning tokens). ⚠ z.ai's `thinking: disabled` is light thinking, not zero, on 5.1/5.2/5.3 alike (220–340 reasoning tokens on a hard prompt). Restarted 1725 (61 s), held 2 h for albok-dev's load A/B. The gate script's first two runs could never pass: `grep -c` exits 1 on a zero count. Images: glm-5.x is text only (z.ai rejects image parts); glm-4.6v and glm-5.3-flash read images on our plan, but neither is on the gateway. → `stacks/litellm/conf/config.yaml` - `[2026-10-07]` **Hermes launchers repaired (Miranda relay 8462):** svos-dev's scratch probe had rewritten `~/.hermes/hermes-agent/.hermes/bin/{hermes,hermes-acp}` to exec a /tmp scratch Python. Repointed to `~/.hermes/tools/python-3.14.7…`; the gateway was not restarted. ⚠ Don't run `git` in `~/.hermes/hermes-agent`: a `status` there looped on auto-gc. - `[2026-10-07]` **albok-service 0.1.14 (unit 4.3: sidecar listing, byte-less ingest, a `.browse/` view; detector 2) deployed at 1804 at albok-dev's ask 8524.** `leaks` went from 8 to 6 when the placeholder false positives cleared. Reindexes ran one per wing afterwards to render the view. ⚠ **OWED: the `.browse/` exposure test.** Pick a share surface (sshfs `follow_symlinks` is the lead), rename `.browse/` on the server, and check that a fresh open through the client's cached listing FAILS. Nothing is shared until it passes; report to albok-dev. → `stacks/albok-service/README.md` diff --git a/servers/nh3-svos/README.md b/servers/nh3-svos/README.md index cbc9192..07be179 100644 --- a/servers/nh3-svos/README.md +++ b/servers/nh3-svos/README.md @@ -23,6 +23,37 @@ configuration, enrollment and acceptance. | App-consistent hook | svos-dev's `/etc/qemu/fsfreeze-hook.d/50-svos` stops the SVOS services on freeze and restarts the ones that were running on thaw. ⚠ **Debian's qemu-ga does NOT run fsfreeze hooks unless configured** (no `-F` in the unit, no `qemu-ga.conf` by default). Here `/etc/qemu/qemu-ga.conf` points it at **`/usr/local/sbin/fsfreeze-dispatch`** (`configs/qemu-ga/fsfreeze-dispatch`), which unlike the stock `/etc/qemu/fsfreeze-hook` propagates failure: if a freeze hook fails, it thaws that hook and the completed ones and exits 1, so qemu-ga refuses the freeze. **Live drill 2026-10-05 1952–1953 (services running):** quiesced backup `vm/111/2026-10-06T02:52:11Z` → kernel+interface down ~1–2 s, back by themselves, health ok. Refused freeze (temporary `60-drill-refuse` after 50-svos, since removed) `vm/111/2026-10-06T02:53:03Z` → the dispatcher unwound and restarted the services, and then **⚠ vzdump did NOT abort: it logged `ERROR: unable to freeze guest fs`, backed up UNFROZEN (crash-consistent, app live), and reported `Backup job finished successfully`, rc 0.** Both images mapped from PBS and mounted read-only (`ro,noload`) on nh3-pve-2, never booted: `svos.db` integrity ok (each image still carries its ~0.9 MB WAL: keep `-wal` with the db when restoring), interface `pins.sqlite3` integrity ok, config/key/config-generation hashes equal live, table counts equal live except `events`. **Monitoring:** `scripts/check-backup-freshness.sh` now reads the LOG of every OK vzdump task and reports ERROR lines; a freeze/thaw error is an APPLICATION-CONSISTENCY FAILURE, verdict **DEGRADED, exit 4** (alerted by the daily wrapper to infra-hermes). nh3-pve-2 was not in the check's node lists at all before 2026-10-05; it is now. Drill B is acknowledged by UPID in the script | | Monitoring | Beszel native agent 0.18.7 (`playbooks/beszel-native.yaml`, `configs/beszel-agent/hosts/nh3-svos.env`), hub system `t6y646ifa3x7xxv`, alerts CPU>95%/15m, Disk>85%/5m, Memory>90%/10m, offline/2m. Up 2026-10-05 | +## Release a737a06 + apps (2026-10-07, svos-dev thread `01M4D1GNYBW7QVY7RF05R412V2`, Prime's 2240 decision) + +- **Release:** `/opt/svos/current` → `releases/a737a06` (previous `6c6abbe` kept: it cannot read schema 11, and the + front door's previous-asset handle still points at it). Built by infra-ops from a clean `git archive`: 9 wheels, + the `web/` 0.0.6 static build, `kernel-requirements.txt` (uv export) and `apps-requirements.txt` (uv pip compile + against the kernel pins, hashed), `vendor/bifrost-1.2.1` (gitea vh index, sha256-verified, because PyPI has an + unrelated `bifrost`), and `hermes-plugin/svos` (staged for U13). Kernel 0.0.5 migrated schema 9 → 11 on first start. +- **Pre-change backups:** vzdump `vm/111/2026-10-08T06:02:14Z` (freeze/thaw clean) and an online sqlite `.backup` + plus config, keys and app state at `/var/lib/svos/backup/pre-a737a06/` (integrity ok, user_version 9). +- **Apps:** + | slug | listener | unit | state | + |---|---|---|---| + | app-interface | 127.0.0.1:8403 | svos-interface | up | + | app-knowledge | 127.0.0.1:8405 | svos-knowledge | up; Albok token `01M4D22HHJ7AJHX30WPDE02DFC`, vault `nh3-svos/albok-token-knowledge`, grants per A111 `read:fleet/memory`, `read:fleet/stash`, `read:personal/notes` (no wildcard, no restricted wing, no class grant) | + | app-terminal | 127.0.0.1:8404, base_url **https://svos-terminal.phasefinal.com** | svos-terminal | **degraded by design**: no zellij and no fleet session on this box | + | app-postoffice | 127.0.0.1:8406 | svos-postoffice (installed, NOT enabled) | declared + enrolled, stopped until Prime declares the bridge handle; `handle = "svos"` in its config is the example's placeholder | + | app-miranda, svos-agent | — | — | packages in apps-env only (U13) | + Credentials `/etc/svos/.env` (root 0600); units in `configs/svos-apps/`; the quiesce hook lists all three new units. +- **Terminal origin:** `svos-terminal.phasefinal.com` = headscale MagicDNS extra record → 100.64.0.9, its own Caddy + site (LE via DNS-01). **No Basic auth** (Chromium cancels Basic prompts in cross-origin frames; the lease is the + page's auth). `/verbs/*` and `/health` are refused unless they come from the box itself. The kernel's signed calls + reach the app through Caddy, so nh3-svos resolves the name via `/etc/hosts` (`100.64.0.9 svos-terminal.phasefinal.com + svos.phasefinal.com`): it has no MagicDNS (`accept-dns=false`). The terminal is `up` only while Caddy is. +- **Acceptance 2026-10-07 ~2315:** 17/17 HTTPS controls through an SSH forward to the real listener (the 12 from 10-05, + plus: health schema 11 with 2 up and 1 degraded; the terminal registered on its own origin; knowledge `wings` + = exactly the 3 token wings; knowledge `search` hits; terminal `attach` refused `404 unknown seat`). Chromium + phone + desk: shell, inbox, stream, reload, `/status`, `/graph`, `/feed`, no overflow, no page errors; desk + "Open Terminal here" → pane open, framed from `https://svos-terminal.phasefinal.com/`, body `waiting` (no lease). + From mesh peer fv-ml1, the terminal origin returns: `/` 200 with `frame-ancestors https://svos.phasefinal.com`; + lease routes 401; `/verbs/*` and `/health` 403; anything else 404. + ## Build (reproducible) ```bash