feat(curator): vendor the application backend as the scenario's tracked source

The curator Python backend (package, tests, systemd units, config templates, scripts) now lives under scenarios/curator/backend and is the single source of truth; the live checkout at the workspace path is a runtime copy. Exported from the app repo's tracked tree via git archive (no history, .pi/venv/caches excluded). 149 unit tests pass from the new location.

profile.toml backend is now repo-relative (scenarios/curator/backend); verify-generated.sh resolves a relative backend against REPO_ROOT. verify-no-secrets ASSIGN heuristic now requires value entropy so vendored kwargs like token=extraction_token no longer false-positive. README documents the backend/ layout and the operator-owned app rollout step.
This commit is contained in:
Kai
2026-08-30 18:49:10 -07:00
parent 14c97d88cf
commit 88b06d782f
57 changed files with 14236 additions and 9 deletions
@@ -0,0 +1,30 @@
[Unit]
Description=Curator database backup and retention
[Service]
Type=oneshot
WorkingDirectory=/home/claw/pi-workspaces/curator
Environment=PYTHONPATH=/home/claw/pi-workspaces/curator
EnvironmentFile=/home/claw/.config/curator/curator.env
ExecStart=/usr/bin/python3 -m curator backup
TimeoutStartSec=15m
# Local disk only; no network is needed, so egress stays closed.
IPAddressDeny=any
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/claw/.local/share/curator
ReadWritePaths=/mnt/truenas/multimedia/curator
PrivateTmp=true
UMask=0077
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
MemoryMax=1G
TasksMax=64
@@ -0,0 +1,10 @@
[Unit]
Description=Run Curator database backup daily
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
@@ -0,0 +1,30 @@
[Unit]
Description=Curator cover image refresh
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/home/claw/pi-workspaces/curator
Environment=PYTHONPATH=/home/claw/pi-workspaces/curator
EnvironmentFile=/home/claw/.config/curator/curator.env
ExecStart=/usr/bin/python3 -m curator refresh-covers
# Network-bound and safe to fail. A stall here used to delay the backup, which
# shared the same oneshot unit and ran first.
TimeoutStartSec=30m
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/claw/.local/share/curator
PrivateTmp=true
UMask=0077
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
MemoryMax=1G
TasksMax=64
@@ -0,0 +1,11 @@
[Unit]
Description=Run Curator cover refresh daily
[Timer]
# After the backup window, so the two never contend for the database.
OnCalendar=*-*-* 04:15:00
Persistent=true
RandomizedDelaySec=20m
[Install]
WantedBy=timers.target
@@ -0,0 +1,13 @@
[Unit]
Description=Curator database backup and retention
[Service]
Type=oneshot
WorkingDirectory=/home/claw/pi-workspaces/curator
Environment=PYTHONPATH=/home/claw/pi-workspaces/curator
EnvironmentFile=/home/claw/.config/curator/curator.env
ExecStart=/usr/bin/python3 -m curator maintain
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
@@ -0,0 +1,11 @@
[Unit]
Description=Run Curator maintenance daily
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
@@ -0,0 +1,77 @@
[Unit]
Description=Curator personal media library
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/claw/pi-workspaces/curator
Environment=PYTHONPATH=/home/claw/pi-workspaces/curator
Environment=CURATOR_PI_WORKSPACE=/home/claw/pi-workspaces/curator
Environment=CURATOR_PI_SESSION_DIR=/home/claw/.local/share/pi-curator/sessions
Environment=CURATOR_PI_MODEL=zenmux/openai/gpt-5.6-luna
Environment=CURATOR_PI_THINKING=high
Environment=CURATOR_PI_FALLBACK_MODEL=zenmux/x-ai/grok-4.6
Environment=CURATOR_PI_TIMEOUT_SECONDS=120
Environment=CURATOR_WECHAT_ARTICLE_BASE_URL=http://192.168.50.145:8091
EnvironmentFile=/home/claw/.config/curator/curator.env
ExecStart=/usr/bin/python3 -m curator serve
Restart=on-failure
RestartSec=5
TimeoutStartSec=60
TimeoutStopSec=20
# --- filesystem -------------------------------------------------------------
# The whole hierarchy is read-only apart from the paths listed below. Verified
# with systemd-run before being applied: the database, library, staging, backup
# and cover directories are writable and pi starts cleanly.
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/claw/.local/share/curator
ReadWritePaths=/mnt/truenas/multimedia/books
ReadWritePaths=/mnt/truenas/multimedia/curator
ReadWritePaths=/home/claw/.local/share/pi-curator
# pi takes a lock beside ~/.pi/agent/settings.json on startup. With a read-only
# home it cannot, and then reports the settings file as invalid and ignores it --
# which would silently discard the agent's configuration.
ReadWritePaths=/home/claw/.pi
# The agent's own prompt and launch contract are read-only to the service that
# runs it, so a compromised agent cannot rewrite the rules it runs under.
ReadOnlyPaths=/home/claw/pi-workspaces/curator
PrivateTmp=true
UMask=0077
# --- privileges -------------------------------------------------------------
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectProc=invisible
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=false
# node's JIT needs writable-executable pages, so MemoryDenyWriteExecute cannot
# be enabled while pi runs as a child of this service.
# --- resources --------------------------------------------------------------
# pi is a node process and the service may run several sequentially. These are
# ceilings that turn a runaway into a restart instead of host memory pressure.
MemoryMax=3G
MemoryHigh=2G
TasksMax=512
# NOTE: the listener is still on CURATOR_HOST=0.0.0.0, which reaches every
# interface including six docker bridges, and access genuinely arrives from both
# the LAN and 127.0.0.1. The defence is now authentication (plan P2-6): every
# route except /api/health and /login requires CURATOR_WEB_TOKEN, which is what
# makes an unaudited LAN host into a read-only observer instead of a write
# primitive. Do not unset CURATOR_WEB_TOKEN while the port is not loopback.
[Install]
WantedBy=default.target