32 Commits

Author SHA1 Message Date
admin dd2d1feb6e release path publishes, and an unreleasable version fails CI (R-115, R-183)
gates / gates (push) Failing after 7s
NO VERSION BUMP and nothing built: no Go code changed. The agent stays v0.120.0.

scripts/release-agent.sh — THE way to release. build -> tag -> publish -> verify
by INDEPENDENT download. Publishing was a separate remembered step and was
forgotten three times in five days (R-111's 17 stranded releases, 0.114.0, and
0.120.0 — deployed to both demo hosts and undownloadable, so a documented-path
reinstall would have silently downgraded them WHILE REPORTING SUCCESS). R-111's
own closing line named this leg and closed SHIPPED without it; it recurred the
same afternoon, which is the evidence that a note is not a mechanism.

It tags because felhom-host-install.sh now fetches the sixteen agent config
files from raw/tag/v<version>/ (R-183): a released version with no tag 404s a
box mid-install, as root, on a virgin machine. It verifies by downloading what
it just published and comparing the sha to what it built — the publish step's
own success is a report on its own write; a fetch returning the right bytes is
a different claim. It refuses a dirty/unpushed tree and refuses to re-release an
existing version. It does NOT vouch: that points machines at a version and stays
the operator's act.

scripts/check-published-versions.py — the gate. Every v<semver> tag must have a
downloadable package AND a tag tree serving the agent's configs. Registered as
NOT --fast (needs network; a push must not fail because Gitea blinked), and the
CI workflow now runs the FULL gate set instead of --fast — otherwise the gate
would have been registered and never run, the built-but-never-wired failure this
project has shipped four times.

The invariant is not the one specified, and the reason was measured, not assumed:
the hub artifact manifest is 401 without a per-customer passphrase and Gitea's
package LISTING api is 401 without a token, while the package DOWNLOAD url and
the git TAGS api are anonymous. So CI cannot ask "what is vouched" without an
operator credential — whose addition is the operator's call. The tag-based
invariant needs none and catches all three recorded instances. What it does not
catch (the hub vouching a version never released at all) is filed as R-184.
2026-08-03 12:34:20 +02:00
admin 054e85a2bf gates: one entry point (scripts/agent_gates.py) + pre-push hook
A census of all thirteen gate scripts across the four felhom repos on 2026-08-02 found that
every check a CLAUDE.md names was passing and two of the four nobody is told to run were
failing. This repo was the extreme case: nothing ran against it at all, and its REUSE.md — 90
cited paths — was checked by no one.

agent_gates.py exists at ONE gate on purpose, so the agent is not the one repo with nowhere to
put a check and so the pre-push hook has the same entry point in all four repos. It grows when
the agent grows a second gate. The shared reuse checker stays in felhom.eu/scripts/ and is
invoked across the workspace — never copied here; an absent sibling clone FAILS the gate and
prints the path tried, which test_agent_gates.py pins by running the entry point from a lone
directory with no sibling.

.githooks/pre-push runs it with --fast and refuses the push. Per-clone and --no-verify-able,
both stated in the hook itself; a manual run WARNS when the clone is unarmed.

Tooling only: no Go change, no build, no deploy, no version bump.
2026-08-02 15:22:58 +02:00
admin 6be168d1a0 docs(R-117 Part 1): repeat the no-block-I/O health-check rule where it binds
felhom.eu/CLAUDE.md now carries the standing rule (with the measurement), but
that file does not load in an agent-only session — and health checks are written
in this repo. A standing rule that does not load where it binds is the inert-seam
shape applied to a rule, so the constraint is repeated here as a one-liner with a
pointer to the full text.

No code change, no version bump.
2026-07-30 12:10:24 +02:00
admin 2f4ccab166 docs: correct two stale claims in CLAUDE.md that misdirect live work
Both found while writing felhom.eu runbooks/target-selection.md.

1. The demo host block said the t740 is the designated drill+build VM host "but no
   drill VM is provisioned there yet". Stale since 2026-07-25 -- VM 300 (drill-r50)
   has been there since. The sentence read as discouragement from the very box the
   operator ruling designates, which is part of why a drill went to DooPlex instead.
   Now says the ruling is realized and to start there, points at the new
   target-selection runbook, and notes drill.qcow2 on DooPlex is a BAKE fixture, not
   a drill target. Agent version dropped (it changes several times a day; the hub
   host list and --version are the authorities) and the t740's PVE node name added.

2. RETRACTED the block's claim that the agent "does not run at all" at the remote
   site because localapi binds the LAN literal 192.168.0.162 and the service has
   "never started" -- with an outstanding config edit needing Viktor GO. That was
   true before R-50 and is false now: since the island migration (2026-07-25)
   localapi binds 169.254.253.1:8443 on vmbr9, which is location-independent by
   design, and proxmox.endpoint is https://127.0.0.1:8006. Verified live 2026-07-30:
   service active, version 0.115.0, and GET /disks answered over the island -- the
   whole R-116 payload capture went through it. A session trusting the old text
   would not have attempted the read that worked. The recorded remote-site address
   was also wrong (.162, not .147), so it now says re-check instead of asserting one.

No code, no version bump, so no CHANGELOG entry (that file is version-keyed) and
REPORT.md is left holding the v0.115.0 record rather than being overwritten by a
docs fix.
2026-07-30 08:25:41 +02:00
admin 36ed6594d4 docs(CLAUDE.md): note demo-hp (t740) as designated drill/build VM host (no drill VM yet) 2026-07-25 09:59:12 +02:00
admin 8c55ac7fda docs: TEMPORARY remote-site access for felhom-pve (tailnet) — see AUDIT-vacation-remote-ops-2026-07-20
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nhk3eBHT8Mg5L8c2aj57aU
2026-07-20 08:07:43 +02:00
admin d81fddc7b1 docs: migrate workflow to DooPlex-local execution 2026-07-19 12:16:13 +02:00
admin f222a7b366 docs: repoint build path to /mnt/5_hdd/felhom.eu (DooPlex build root moved off SSD 2026-07-18) 2026-07-18 09:55:47 +02:00
admin 0df72ea643 docs: REPORT + CONTEXT + CLAUDE logging rule for v0.83.0 (deployed felhom-pve)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 17:18:55 +02:00
admin 3e760a559a docs: CLAUDE.md refresh — version narrative removed, layout completed (17 pkgs + opsign), skill pointers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 11:25:56 +02:00
admin 2a024b6039 docs: REUSE.md introduced — cross-repo reuse map (exec/format-safety guards, durable-id seams, traps; docs-only, no version bump)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 09:39:32 +02:00
admin 2a4affc3a8 docs(claude): refine A4 live-test rule (pipeline-bypass is the line, not API-vs-UI)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:07:55 +02:00
admin 8e6d00a57f docs(claude): require real UI/user-flow live validation (A4)
Per the F9 storage episode — live validation of a user-facing feature
must exercise the real flow end-to-end, not shortcut via direct
API/agent/CLI calls. Low-level mechanism tests are exempt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:33:33 +02:00
admin 20e03902c8 docs: v0.31.0 CHANGELOG + REPORT + CLAUDE banner (F9/F20-BUG2/BUG3 live-validated) 2026-06-14 15:41:59 +02:00
admin b566fbe57f docs(claude): codify trunk-based no-branches rule
Expands the 'push to main; no feature branches' convention into the full rule:
report-only artifacts → felhom.eu/documentation/; risky/supervised (agent/golden/
provisioning/destructive) fixes implemented on main during the supervised session,
not prepared on a branch; unattended escape hatch = revert+report. Supersedes the old
'prepared on fix/... branch, pending review' pattern.
2026-06-14 11:03:10 +02:00
admin 238ec02754 docs(claude): refresh stale version banner v0.13.0 -> v0.30.0 (current state summary)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:05:36 +02:00
admin 9a0e7e168b REPORT + CLAUDE: slice 8B.2 agent half (snapshotted phase, v0.13.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:02:16 +02:00
admin fe7b3c4ab7 REPORT + CLAUDE: slice 8C Phase A (agent disk endpoints + data-bearing gate, v0.12.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:07:21 +02:00
admin 4d9e76e66a REPORT + CLAUDE: slice 8B agent half (/backup/due cadence + /backup/status phases, v0.11.0)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:04:38 +02:00
admin 3fecf4c713 slice 8A (agent half): local-API server + provisioning back-half (v0.10.0)
internal/localapi: per-guest local-API server (doc 03 §6) — 7 self-scoped
endpoints, hashed per-guest token store, persisted self-signed leaf with stable
SHA-256 pin, optional 6th daemon goroutine. internal/provision: back-half —
mint token, render bootstrap.json (no registry cred), write 0600, chown
100000:100000, attach pct-set bind mount (host-side, F3, no pct exec).
--selftest=provision. build-golden.sh bakes the controller image + bootstrap
unit. sudoers FELHOM_PROVISION; firewall narrowing artifact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:47:42 +02:00
admin fae11020a5 slice 7 escrow: agent contract test + CHANGELOG/REPORT/CLAUDE (v0.9.0)
Agent half of slice-7 close-out finalized: cross-repo wire-contract test
(escrowUploadRequest mirrors the hub), v0.9.0 CHANGELOG entry + REPORT + CLAUDE
version line. No secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 07:45:38 +02:00
admin 57405c1a99 slice 7 Phase 1: unified bring-up reconcile job (provision + guest-loss DR) (v0.8.0)
The shared front half of provision and guest-loss DR as a journaled reconcile job
(internal/reconcile/bringup.go), mirroring the restore-test's crash-safety but keeping
the guest on success and applying a scenario-specific identity policy. Agent-only; no
hub/wire change. Grounded by the slice-7 bring-up spike (commit 3342993): F1/F3/F4.

- RunBringUp: restore -> reset identity -> size -> attach mounts -> start link-up;
  verdict is liveness (waitRunning), success KEEPS the guest.
- identity policy: provision = fresh MAC (net0 sans hwaddr -> PVE regen) + hostname,
  host-side; machine-id/host-keys regenerate guest-side (systemd + baked golden unit).
  dr_guest_loss = preserve continuity (keep hostname; keep MAC unless KeepMAC=false).
- compensating rollback: mid-flight failure destroys the just-created guest
  (SameTxnCreated provenance, gated); new Rollback journal flag + Recover.recoverBringUp
  reap a half-built guest from a crash.
- F4: coalesced config PUT + bounded retry on the transient PVE config-lock 500 only.
- --selftest=bring-up (mode/archive/vmid/hostname/keep).
- configs/build-golden.sh: validated golden recipe incl. the F3 first-boot host-key unit.
- doc-03 §9 + identity-reset settled/implemented.

Deferred (stated): provisioning back half -> slice 8; host-loss DR + escrow consumption
and the BringUpSpec source (hub desired-state) -> slice 10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:27:49 +02:00
admin 6e86483185 restore-test: verdict is liveness, not start-task exitstatus (v0.7.0)
Fixes the crying-wolf false-fail surfaced by the live hub-enrollment runbook:
PVE's guest-start task exits "WARNINGS: 1" for the benign systemd-nesting
advisory, and WaitTask treated any non-OK exitstatus as failure, so the verdict
was decided by an advisory exit code before the real boot check ran. Every
modern-distro restore-test reported pass:false.

- proxmox.WaitOptions.AllowWarnings (opt-in; default keeps all callers strict)
- restore-test start step accepts warnings, surfaces them, verdict stays waitRunning
- RestoreTestResult.StartWarnings/.WarningsRecognized + version-free "enable
  nesting" recognizer (can't rot back at systemd 258+); GuestAPI.TaskLogTail
- hub.RestoreTest.warnings/.warnings_recognized wire fields (consumed by hub v0.7.5)
- scheduler logs clean / passed-with-recognized / passed-with-unrecognized warnings
- tests: WaitTask warnings matrix; restore-test pass/fail-on-liveness; version-free
  regression guard (systemd 256-300)

Single agent bump 0.6.0 -> 0.7.0 covering the agent half of both task phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:30:03 +02:00
admin 766500dfc3 v0.6.0: slice 6 Phase B — PBS offsite tier (verify + PBS-API client + reporting)
Spike-proven that backup/restore-to-PBS reuse Phase A unchanged; the only new code is
the verify capability, a small PBS-API client, and PBSSnapshot reporting.

- internal/pbs: fingerprint-pinned, token-authed PBS-API client (Verify/Snapshots/
  TaskStatus, node-from-UPID; secret read from /etc/pve/priv/storage/<id>.pw at runtime,
  never logged) + the verify maintenance loop (own cadence, default 6h, NOT gated/journaled,
  like the watchdog) + SnapshotStore.
- hub: PBSSnapshot filled (namespace/type/id/time/size/owner/protected/encrypted/
  verify_state/verify_upid); PBSReporter collector seam; cross-repo golden + bidirectional
  key-set tests; hub handler parses pbs_snapshots + logs a failed-verify WARN.
- backup: report the ACTUAL vzdump mode (parsed from the task log; PVE may downgrade
  snapshot->stop). proxmox.Storage.Username. config PBSVerifyCadence/secret-dir.
  --selftest=pbs-verify. Backup/restore-to-PBS unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:53:04 +02:00
admin b527430ec7 v0.6.0-rc1: slice 6 Phase A — backup + the self-restore-test (local target)
The guest-level backup layer + the journaled self-restore-test (restore→boot→verify→
teardown) that closes "a backup you haven't restored isn't a backup". All benign
(reuses the slice-4 classifier/gate/journal; no new destructive class/crypto). Local
target only; PBS = Phase B. Restore to a NEW guest only. Backups crash-consistent.

- proxmox: DestroyLXC, VzdumpOptions.Notes (notes-template), LatestBackupVolID.
- reconcile: Engine.RunRestoreTest (journal Scratch entry BEFORE mutation; net link-down
  pre-boot; defer teardown always; benign gated destroy) + Recover extended to reap a
  leaked scratch guest (Scratch flag, special-cased before the UPID path; idempotent).
- internal/backup: runner (vzdump + archive resolve + bulk-gap = backup!=1) + cadence
  scheduler (4th daemon goroutine, default 24h) + in-memory report store.
- hub: Backup/RestoreTest filled; collector seams; cross-repo golden byte-identical +
  bidirectional key-set tests; hub handler logs a FAILED restore-test prominently.
- config BackupConfig (band 990000-990009 default); --selftest=backup / restore-test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:49:39 +02:00
admin 9d6e49236c v0.5.0: slice 5 Phase B — the host-root surface (mounts + SMART + grow + destructive gate)
The privileged write surface, isolated behind a narrow, arg-validated, adversarially-
tested seam (HostOps), the same discipline as the slice-4 gate. Completes slice 5.

- internal/storage: HostOps seam + SudoHostOps (systemd .mount units by fs-UUID, detach,
  SMART, lvs) via sudoers allowlist + fixed arg vectors, no shell; NoopHostOps fallback.
- validate.go: strict UUID/mount-path/device/LVM validators + in-process systemd-escape.
  Headline test: adversarial matrix (metacharacters/traversal/malformed) refused with
  zero exec.
- smart.go: smartctl SATA + NVMe parse, UNKNOWN-degrade; lvs thin-pool metadata fill.
- observer enrichment (Observe only): fills smart + thin-pool metadata.
- watchdog: benign re-mount response off the poll path (DevicePresent probe, rate-limited).
- reconcile: ActionResize (benign, grow-only) + proxmox.ResizeLXC; destructive storage ops
  (ClassStorageWipe/Decommission) through the slice-4 gate, target-scoped; built+tested,
  inert live.
- --selftest=storage [-watch] live harness; configs/felhom-agent.sudoers; privileged.* knobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:53:38 +02:00
admin 27b68f043b v0.5.0-rc1: slice 5 Phase A — storage observe/report + watchdog (read-only, live)
Fill the slice-3 storage_targets stub and add the fast-poll storage watchdog.
Read-only this phase; the host-root surface (mounts/SMART/grow/destructive gate)
is Phase B. Hub-owned desired manifest is slice 10, so reconcile against it is
built-but-unfed.

- internal/storage: StorageTarget wire contract, durable_id derivation per type,
  HostReader seam (procfs/sysfs, root-free), Observer (storage_targets from
  ListStorage/NodeStorage + host reads, lvmthin thin-pool fill), and the watchdog
  (third daemon goroutine; debounced out-of-band report on a known target's
  attach/disconnect transition).
- proxmox.Storage: additive parse-only config fields (durable_id sources).
- collector StorageObserver seam; Loop.SetTrigger out-of-band report; daemon runs
  the watchdog as a third goroutine; StorageConfig knobs.
- cross-repo golden kept byte-identical with felhom.eu/hub; bidirectional key-set test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:59:05 +02:00
admin 1af21a6cac v0.4.0: slice 4 Phase B — reversibility gate + signed-op consuming layer
The security core of slice 4: hub-supplied intent is no longer trusted for
destructive change. The gate fronts the per-guest queue's executor, so every
mutation passes it. Reuses internal/authz for all crypto (surface untouched).

- Classifier (doc 03 §4): benign vs destructive by provenance + data-bearing-
  ness, NOT by verb. Destroy/overwrite of customer data is destructive unless
  agent-internal provenance (same-journaled-txn create, or agent-tagged scratch)
  makes it benign — and that provenance is journal-recorded, NEVER hub-sourced.
  Unknown op class fails safe to destructive.
- Reversibility gate: benign -> allowed unsigned; destructive -> requires a
  verified, role-scoped, action-bound operator signature, else pending_signature
  and never executed. Every decision audited (signal, never the guard).
- Signed-op consuming layer over authz.Verifier.Verify (locked pipeline
  untouched): role-scoping (doc 04 §4 — recovery=rotation only, operational=
  ordinary destructive + planned rotation) + op-to-action binding (op+host+
  guest+params must match the gated action).
- Signed-job orchestration: idempotency dedupe by nonce + journal-wrapped
  execution via an injected DestructiveExecutor (nil this slice — inert).
- Crash recovery (Note 1): Engine.Recover consumes the journal InFlight() set at
  startup (resume-or-rollback) — covers an op that crashed after the POST and
  before its terminal record, which idempotency dedupe alone cannot. Added
  TaskStatusOnce to the GuestAPI seam. Wired into daemon startup.
- Note 2: memory comparison canonicalized to MiB (desiredMemoryMiB) so a
  non-MiB-aligned MemoryBytes converges in one pass, not perpetual drift.
- Daemon: builds the verifier from config signers (none = nil verifier, the
  common slice-4 state), the gate (+SlogAudit), runs Recover before mutating.

Adversarial matrix proven against the REAL authz.Verifier with in-test-minted
SSHSIGs (framing replicated in reconcile's test binary; authz untouched, no
signing added to the verify-only package): unsigned job + unsigned desired-state
delta -> pending_signature; unknown signer/expired/replay-across-restart/wrong
host -> typed authz rejections; wrong guest/op/params -> binding_mismatch;
recovery key on ordinary destructive -> role_denied; hub-supplied scratch tag
ignored -> refused; valid+role+target+fresh nonce -> accepted then replay
rejected. Full module race-clean + vet-clean on the Linux build server.

Inert this slice: no destructive deltas served until slice 10; the destructive
path is classified, gated, and tested but not wired to live execution.

CHECKPOINT: Phase B complete (slice 4 done). Awaiting validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:56:20 +02:00
admin 05c450147c v0.4.0-rc1: slice 4 Phase A — reconcile engine (structural, runs live unfed)
New internal/reconcile package: the agent-side control core's structural half.

- Per-guest serializer Queue (doc 03 §10): the single choke point all mutation
  sources funnel through; same-vmid serial in submit order, different vmids
  parallel (cond-var FIFO lanes).
- Desired-state model + DesiredProvider seam; EmptyProvider is the only live
  source at slice 4 (no hub serving until slice 10) so the live engine computes
  an empty action set and performs zero mutations.
- Normalization layer (FieldNormalizers): normalized desired-vs-actual so
  Proxmox round-trip quirks don't read as drift. normDesc promoted out of
  main.go to reconcile.NormDescription; selftest uses the shared helper.
- Plan (pure diff): minimal benign action set (Start/Stop/SetConfig) for guests
  in both desired and actual; provision/destroy out of scope here.
- Engine: dispatches onto the shared queue; honors the dual-mode SetConfig
  contract (UPID -> WaitTask; empty UPID -> synchronous success).
- Durable op journal + idempotency store (mirrors authz.FileNonceStore):
  in-flight task ids for crash detection + AlreadyApplied dedupe across restart.
- Wired into runDaemon alongside the hub loop, sharing the queue; runs cleanly
  with no desired state and no signers.

Full module race-clean and vet-clean on the Linux build server.

CHECKPOINT: Phase A only. Awaiting validation before Phase B (the reversibility
gate + signed-op consuming layer, landing v0.4.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:21:55 +02:00
admin 605ce25f58 v0.3.2: reversible SetConfig step in --selftest=task (slice-4 pre-check)
Append a reversible SetConfig write+revert to runSelftestTask: read
GuestConfig, write a `description` marker, verify it landed, restore the
original (or delete if absent), verify the restore. Handles PVE's dual-mode
SetConfig return (empty UPID = synchronous; UPID = WaitTask+assert OK).

Live self-gate PASSED on demo-felhom / guest 9999. Findings:
- LXC `description` write is synchronous (empty UPID) — dual-mode modeling
  confirmed; empty string is success, not an error.
- PVE appends a trailing newline to `description` on read; slice-4 reconcile
  must normalize description comparisons (hence normDesc helper).

First live exercise of the VM.Config.* privilege cluster. Standing operator
token rotated during the run; new secret stored out-of-band, not in the repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:13:04 +02:00
admin 237452c8c6 docs: reflow CLAUDE.md; unify REPORT/CHANGELOG convention; add no-secrets rule
Also overwrite REPORT.md with the live --selftest=task validation on demo-felhom
(snapshot/rollback/delete on guest 9999, exitstatus=OK under the felhom-agent@pve
privsep token; slice-1 mutating-ops gap closed, slice 4 unblocked). No version bump.
Token secret stored out-of-band, not committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:54:18 +02:00
admin 11c91a0dde upodate 2026-06-08 20:07:52 +02:00