feat(nh3-svos): SVOS a737a06 refresh — app-terminal on its own mesh origin (svos-terminal.phasefinal.com), app-knowledge, app-postoffice units; quiesce hook covers the new units

This commit is contained in:
vh
2026-10-07 23:17:18 -07:00
parent 9e972872e6
commit 469b3ba3cf
9 changed files with 216 additions and 2 deletions
+2 -1
View File
@@ -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):
+3
View File
@@ -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.
+2 -1
View File
@@ -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)
+30
View File
@@ -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
+30
View File
@@ -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
+30
View File
@@ -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
+87
View File
@@ -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
}
}
}