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:
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user