Agent 0.159.1, the catalog allow-list (controller 0.328.0), break-glass rotation: report, register, design
gates / gates (push) Successful in 8m49s

Closed R-971, R-954, R-1124, R-1012 (174 -> 175 rows); opened R-1139..R-1143; R-895 READY with the design
and hub slice 1; R-972 waits on the vouch. [DESIGN] "The catalog allow-list" in 02; the catalog trust line in
01; FELHOM_THINCHECK and unit-forget in 03. Evidence audits/agent-allowlist-rotation-2026-10-17/.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-17 18:00:05 +02:00
parent d968f8490e
commit 5a5486104a
36 changed files with 816 additions and 16 deletions
@@ -727,6 +727,46 @@ Cloudflare edge served 0.315.0's first stylesheet under the address its third bu
---
## [DESIGN] The catalog allow-list — the container rights the box gives (controller v0.328.0, R-971, operator decision D2 = A, 2026-10-17)
**What.** Before ANY app compose is written — the install, a settings change, the 15-minute correction, a guarded update
or ladder step, a unit or off-site restore — the controller renders the compose with the app's own values (exactly as
`compose up` will see them) and asks `internal/privpolicy.Admit`. A setting in one of these classes is refused unless
this app's service has EXACTLY that setting on the allow-list: `privileged`, `cap_add`, `devices`,
`device_cgroup_rules`, `gpus`, deploy device reservations, `network_mode`/`pid`/`ipc`/`uts`/`userns_mode`/`cgroup`/
`cgroup_parent`/`runtime`/`isolation`, `security_opt` other than `no-new-privileges`, `sysctls`, `volumes_from`, `build`,
`include`, ANY `extends`, a `provider` service, `post_start`/`pre_stop` hooks, an `env_file`/`configs`/`secrets` file
outside the stack dir, a named volume with `driver`/`driver_opts`/`external`/`name`, a `${VAR}` in any of those keys,
and binds: the Docker socket anywhere, mount propagation, a `${VAR}` in a bind's options, and any source outside the
data roots the box itself gives — `${HDD_PATH}/appdata/`, `${USERDATA_PATH}`, `${IMPORT_PATH}` (a bind source may use
only those variables, as its leading token; each root must lie under `/mnt/felhom-drives/` or the system data path; on
the box a source is judged where it really points, through its nearest existing ancestor, so a symlink cannot carry it
out).
**Where, and why in code.** The list is `internal/privpolicy/allowlist.json`, compiled in with `go:embed`: a change in
Gitea — the threat R-971 names (whoever can push to the catalog can take every guest) — cannot add to it; a new entry
needs a controller release. An entry is app + service + exact setting, and holds only while that service runs the
entry's image REPOSITORY (a literal reference), with the image's own entrypoint and command, and the exact
`healthcheck.test` the entry names — so a push cannot keep the allowed right and change what runs with it. Today: one
entry, homepage's read-only Docker socket (the survey of all 123 catalog files found nothing else,
`audits/agent-allowlist-rotation-2026-10-17/P2/survey.md`). The catalog's CI gate `check-privileges.py` mirrors the
check with a byte-identical copy of the list (pinned from both sides); the BOX is the authority.
**What a refusal does.** Nothing is written. A refused correction or update leaves the app on its last good compose,
running; the controller raises the operator-only hub event `app_definition_refused` once per refused definition (hub ≥
v0.157.0) and the app page carries one line (`app_info.definition_refused`). A refused install, settings change or
restore is refused with a plain message („Semmi nem változott"); a restore asks BEFORE it stops the app. A check
that cannot read the compose refuses. The sync never copies a catalog template named like a controller infra stack.
**Out of scope, stated.** The controller's own infra stacks (traefik, cloudflared, filebrowser, samba) and its override
files (setup gate, family gate, install hold, media library) are rendered by the controller, not the catalog. Published
host ports are not a class (crafty-controller's game ports). Known limit: under `${HDD_PATH}/appdata/` one app can
name another app's folder (a per-app rule needs a name map — paperless-ngx keeps `appdata/paperless`), R-1142.
**Pinned by** `internal/privpolicy/privpolicy_test.go` (each class refused, with positive controls),
`stacks.TestAllowList_IsWiredIntoEveryComposeWriter` (AST: every compose writer and every env decision point asks
first), `sync.TestR971_*` (a refused correction writes neither file and is not reported updated).
## The app-definition seam — what happens to a deployed app when its compose file is rewritten
> **STATUS: MEASURED BEHAVIOUR, NOT A RECORDED DESIGN.** This section describes what the system was