All 29 gate scripts across the four repos were read and DECOYED - the label constructed without the fact, the gate run, the verdict recorded. 16 were fooled. None of them were in this repo. A decoy that nobody would write proves nothing, so the attempts that turned out illegitimate were WITHDRAWN rather than counted. Both of this repo were withdrawn, and both are named in the audit. The gates here that could not be given a plausible decoy are listed BY NAME in felhom.eu/scripts/decoy_coverage_gate.py EXEMPT (R-426) as UNTESTED - not as sound. A gate nobody tried to fool is UNKNOWN, and calling it sound would be the same confident guess this sweep exists to find. Survey table: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
6.7 KiB
CLAUDE.md — felhom-agent
Stable orientation only — current state lives in
CONTEXT.mdand the top ofCHANGELOG.md, never here. Cross-repo conventions (artifact taxonomy, access, clean-tree gate, secrets, CHANGELOG/REPORT): workspace-root/mnt/5_hdd/felhom.eu/git/CLAUDE.md. Path-scoped detail:.claude/rules/.
What this repo is
The operator-tier host agent, one per Proxmox host, owning all Proxmox interaction: provision/restore guests, host storage, backup/restore orchestration, the hub control loop, and a narrow per-guest local API. It is the most privilege-sensitive component in the system.
- Renamed from
proxmox-controller. - Distinct from
felhom-controller— that is the in-guest controller, Docker-only, holding no Proxmox credentials. Do not confuse them. - Control plane, not data plane: if the agent dies, apps keep serving; only management degrades.
- Pure Go stdlib +
golang.org/x/crypto. No web frameworks.
Doing X → read Y
| Doing | Read |
|---|---|
| writing any new code | REUSE.md — helpers, format-safety guards, traps, seams |
| needing current state / open threads | CONTEXT.md + the top CHANGELOG.md entry |
| Proxmox, reconcile or signed jobs | loads itself: .claude/rules/proxmox.md |
| local API, authz or guest hooks | loads itself: .claude/rules/localapi.md |
| backup, PBS or DR | loads itself: .claude/rules/backup.md |
| storage or escrow | loads itself: .claude/rules/storage.md |
| writing a health check | loads itself: .claude/rules/health-checks.md |
| release, build, publish, deploy, verify a version | the felhom-build-deploy skill — never hand-roll it |
| writing or reviewing a test, fixing a bug | the felhom-testing skill |
| host addresses, break-glass, node facts | felhom.eu/documentation/operations/nodes.md — never restate them |
| which box may I break | felhom.eu/documentation/runbooks/target-selection.md |
| what version is live anywhere | ask the hub (/hosts, /configs) or the box — never a doc |
| the authoritative design | felhom.eu/documentation/architecture/03-host-agent.md (locked) |
The root-CLI fence — API-first, exactly three exceptions
This is in the core because breaching it is how this component stops being auditable.
- API-first via a scoped
FelhomAgenttoken. Raw root-CLI is fenced to exactly 3 exceptions: keyctlpct create(golden image), USB mount/fstab, SMART/sensors. Clientnever shells out;Privilegednever makes HTTP calls — asserted byrouting_test.go. Adding a method toproxmox.Privilegedbreaks the fence; useproxmox.Runnerplus a new sudoersCmnd_Aliasandvalidate.go-style checks (REUSE.md§3).- Destructive ops go through the reconcile gate / signed-jobs path. Never call
Client.DestroyLXC/Vzdump/SetConfigad-hoc — that skips classification, signature, per-guest serialization and crash recovery. - Ownership must be PROVEN, never assumed. A raw
ListLXClist is not "guests the agent owns"; intersect withClient.Poolmembership and fail safe on a read failure (audit A1).
Gates — ONE entry point
Run python3 scripts/agent_gates.py from the repo root after ANY change here. It runs this
repo's gates — reuse_refs_check and instructions_gate, both the shared copies in
felhom.eu/scripts/, never copied into this repo (a copy recreates the drift they detect; an absent
sibling clone FAILS). --fast selects the gates touching no network and no container runtime; today
that is all of them. A missing gate is a FAILURE, never a skip.
The pre-push hook (.githooks/pre-push) runs it with --fast and refuses a failing push. It is
per-clone — switch it on once with git config core.hooksPath .githooks, and a manual run WARNS
when this clone is unarmed. git push --no-verify bypasses it deliberately; say so in the session
report when you use it — CI re-runs the same entry point on every push and emails the operator on
failure, so a bypass is noticed even though it is not blocked (R-168, CLOSED 2026-08-02).
Live validation — the fence
Exercise the SERVER-SIDE PIPELINE a real user triggers, end-to-end. The forbidden shortcut is BYPASSING it — the F9 episode was a raw guest-attach with hand-set state, and it proved nothing.
claude-in-chrome is NOT available on DooPlex. Invoking the exact endpoint the UI invokes is an
acceptable proxy — say which method was used. Low-level mechanism tests where the direct call IS
the mechanism are exempt.
Conventions
- Trunk-based — no branches. All shippable work commits directly to
main;mainequals what is deployed. Report-only artifacts (audits, findings, fixspecs) go tofelhom.eu/documentation/. - Unattended escape hatch: if a fix cannot be cleanly verified and shipped, revert and report — never park it on a branch.
- Logging: the slog logger fans out to journald (configured level) plus the always-DEBUG
applog.Ring(remote pulls). English, keys-never-values, durations on outcomes. Full rules:felhom.eu/documentation/runbooks/logging-conventions.md. - Update
REUSE.mdin the same commit that adds, changes or deprecates a shared helper or pattern.
End-of-session checklist
CHANGELOG.md(cumulative, newest on top) andREPORT.md(overwritten with this run only) — in every repo touched.CONTEXT.md— decisions, state, what is next.REUSE.md— if a shared helper or pattern moved.- A finding goes in
felhom.eu/documentation/backlog/OPEN-ITEMS.mdfirst, never only in a report or an audit. - Confirm your own last push's CI run went green, by run ID — CI mails on failure, which is a PUSH signal; this is the PULL check that catches a lost or unread mail. An unchecked green is an assumption, not an observation.
A gate ships with a decoy test that has been seen to fail (R-421). A decoy is the LABEL without
the FACT — a directory with the right name and no bake log, a note whose prose mentions the marker it
lacks. scripts/decoy_coverage_gate.py refuses a new gate that has neither a decoy nor a named
exemption carrying its row. The four shapes, the 2026-09-01 sweep that fooled 16 of 29 gates, and the
decoys withdrawn as illegitimate: documentation/audits/AUDIT-gate-decoys-2026-09-01.md and
felhom-controller/.claude/rules/gates.md. Scope is a fact too — prefer os.walk over
os.listdir, and a glob over a hand-maintained list.