From 69fa9cf784312a5d3c27bba492d4c62568958126 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 9 Oct 2026 07:30:08 +0200 Subject: [PATCH] ep0-copy-gc: read ep0's real {"data": [...]} namespace answer; any other shape aborts (found on the first live dry run, guard held) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- .../delivery/hub-0.144.0-deploy.txt | 5 +++++ .../delivery/sign-bundle-0.154.0.txt | 10 ++++++++++ .../release-2026-10-09/ep0-copy-gc/dry-run.txt | 6 ++++++ scripts/CHANGELOG.md | 10 ++++++++++ scripts/ep0-copy-gc/felhom-ep0-copy-gc | 13 ++++++++++--- scripts/ep0-copy-gc/test_ep0_copy_gc.py | 15 ++++++++++++++- 6 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 documentation/audits/release-2026-10-09/delivery/hub-0.144.0-deploy.txt create mode 100644 documentation/audits/release-2026-10-09/delivery/sign-bundle-0.154.0.txt create mode 100644 documentation/audits/release-2026-10-09/ep0-copy-gc/dry-run.txt diff --git a/documentation/audits/release-2026-10-09/delivery/hub-0.144.0-deploy.txt b/documentation/audits/release-2026-10-09/delivery/hub-0.144.0-deploy.txt new file mode 100644 index 00000000..7c6eff82 --- /dev/null +++ b/documentation/audits/release-2026-10-09/delivery/hub-0.144.0-deploy.txt @@ -0,0 +1,5 @@ +2026-10-09T05:25:03Z +sync=Synced health=Healthy rev=702e19ee582f1883b3622388cef3717c4128eaf4 +image=gitea.dooplex.hu/admin/felhom-hub:0.144.0 +hub-6b44759c56-cvh6c gitea.dooplex.hu/admin/felhom-hub:0.144.0 ready=true +2026/10/09 07:24:22 [INFO] felhom-hub 0.144.0 starting diff --git a/documentation/audits/release-2026-10-09/delivery/sign-bundle-0.154.0.txt b/documentation/audits/release-2026-10-09/delivery/sign-bundle-0.154.0.txt new file mode 100644 index 00000000..fc0da625 --- /dev/null +++ b/documentation/audits/release-2026-10-09/delivery/sign-bundle-0.154.0.txt @@ -0,0 +1,10 @@ +== agent_config_update 0.154.0 (bundle 93487989…) signed with felhom-op-1, ttl 45m, 2026-10-09T05:17:56Z +-- demo-hp-bb76ea +signed: op=agent_config_update host=demo-hp-bb76ea guest="" key_id=felhom-op-1 nonce=4dd19f519d257d7b994015ddd9418138 expires=2026-10-09T06:02:56Z +uploaded signed op to the hub jobs queue +-- demo-felhom-8363b5 +signed: op=agent_config_update host=demo-felhom-8363b5 guest="" key_id=felhom-op-1 nonce=535a6c27b1161d683462a17a893d03ab expires=2026-10-09T06:02:56Z +uploaded signed op to the hub jobs queue +-- tester-1-d70be4 2026-10-09T05:25:17Z +signed: op=agent_config_update host=tester-1-d70be4 guest="" key_id=felhom-op-1 nonce=375fee9b045ba506074be0951d11f6ae expires=2026-10-09T06:10:17Z +uploaded signed op to the hub jobs queue diff --git a/documentation/audits/release-2026-10-09/ep0-copy-gc/dry-run.txt b/documentation/audits/release-2026-10-09/ep0-copy-gc/dry-run.txt new file mode 100644 index 00000000..288878da --- /dev/null +++ b/documentation/audits/release-2026-10-09/ep0-copy-gc/dry-run.txt @@ -0,0 +1,6 @@ +== DRY RUN 2026-10-09T05:26:53Z (installed from felhom.eu 702e19ee) +ep0-copy-gc: ABORT — 4 namespaces absent on ep0 at once (limit 2): ep0's list looks partial (a rebuild?), not like customer deletions; nothing recorded, nothing deleted. Absent: Tester-2, demo-felhom, demo-hp, tester-1 +rc=2 +-- copy namespaces on disk: Tester-2 demo-felhom demo-hp operator tester-1 +-- state file: +cat: /var/lib/felhom-ep0-copy-gc/absent.json: No such file or directory diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 4d49c917..0df0c6fc 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2026-10-09 — ep0-copy-gc reads ep0's real namespace answer (found on the first live dry run) + +- `scripts/ep0-copy-gc/felhom-ep0-copy-gc`: `proxmox-backup-client namespace list --output-format json` answers + `{"data": [{"ns": ""}, {"ns": "demo-hp"}, …]}`; the job iterated the wrapper object and read `data` as ep0's only + namespace. On DooPlex's first dry run (operator present) all four customer namespaces read „absent" and the + mass-absence guard stopped it — nothing recorded, nothing deleted. With two or fewer customers it would have started + their 7-day clock. Now any shape but `{"data": [{"ns": str}, …]}` aborts as „could not tell". The test fake printed a + bare list (a model of the tool, not the tool) and now prints the measured shape; `test_ep0_unknown_shape_aborts` + (red-proved: 4 failures with the real shape before the fix). + ## facebook — fb_probe: a deleted post answers (#10) „Object does not exist" (2026-10-08, fixed without a row) - The first write run proved a text post gone only on code 100; Meta v26.0 answers a deleted scheduled post with diff --git a/scripts/ep0-copy-gc/felhom-ep0-copy-gc b/scripts/ep0-copy-gc/felhom-ep0-copy-gc index 0143c16e..2fd33404 100755 --- a/scripts/ep0-copy-gc/felhom-ep0-copy-gc +++ b/scripts/ep0-copy-gc/felhom-ep0-copy-gc @@ -76,10 +76,17 @@ def ep0_namespaces(): r = pbc(["namespace", "list", "--repository", EP0_REPO, "--output-format", "json"], EP0_TOKEN_FILE, EP0_FINGERPRINT_FILE) if r.returncode != 0: raise RuntimeError("ep0 namespace list failed (rc %d): %s" % (r.returncode, r.stderr.strip()[-200:])) + # The real answer is {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} (measured on DooPlex 2026-10-09). Any other + # shape is „could not tell": the first version iterated the wrapper object, read „data" as ep0's only namespace, + # and was stopped on its first dry run only by the mass-absence guard. Pinned by test_ep0_unknown_shape_aborts and + # by the fake, which now prints the real shape. + doc = json.loads(r.stdout or "null") + items = doc.get("data") if isinstance(doc, dict) else None + if not isinstance(items, list) or not all(isinstance(i, dict) and isinstance(i.get("ns"), str) for i in items): + raise RuntimeError("ep0 namespace list has an unexpected shape: %s" % (r.stdout or "").strip()[:120]) out = set() - for item in json.loads(r.stdout or "[]"): - name = item.get("ns", "") if isinstance(item, dict) else str(item) - top = name.split("/")[0] + for item in items: + top = item["ns"].split("/")[0] if top: out.add(top) return out diff --git a/scripts/ep0-copy-gc/test_ep0_copy_gc.py b/scripts/ep0-copy-gc/test_ep0_copy_gc.py index 8c88434d..332c013d 100644 --- a/scripts/ep0-copy-gc/test_ep0_copy_gc.py +++ b/scripts/ep0-copy-gc/test_ep0_copy_gc.py @@ -24,7 +24,12 @@ with open(os.environ["FAKE_LOG"], "a") as f: if a[:2] == ["namespace", "list"]: if os.environ.get("FAKE_EP0_FAIL"): sys.stderr.write("connection refused\n"); sys.exit(255) - print(json.dumps([{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])])) + if os.environ.get("FAKE_EP0_RAW"): + print(os.environ["FAKE_EP0_RAW"]); sys.exit(0) + # the REAL shape, measured on DooPlex 2026-10-09: {"data": [{"ns": ""}, {"ns": "demo-hp"}, ...]} -- a root entry + # and a wrapper object. The first fake printed a bare list, so the tests passed while the job read „data" as + # ep0's only namespace on the real tool (stopped live only by the mass-absence guard). + print(json.dumps({"data": [{"ns": ""}] + [{"ns": n} for n in json.loads(os.environ["FAKE_EP0_NS"])]})) sys.exit(0) if a[:2] == ["namespace", "delete"]: sys.exit(0) @@ -112,6 +117,14 @@ class EndToEnd(unittest.TestCase): self.assertEqual(r.returncode, 2) self.assertEqual(self.deletes(), []) + def test_ep0_unknown_shape_aborts(self): + # an answer the job cannot read is „could not tell", never „these customers are gone" + self.seed("gone-cust", "2026-01-01") + for raw in ('{"result": [{"ns": "demo-hp"}]}', '["demo-hp"]', '{"data": "x"}'): + r = self.run_gc("--apply", FAKE_EP0_RAW=raw) + self.assertEqual(r.returncode, 2, raw) + self.assertEqual(self.deletes(), [], raw) + def test_ep0_unreachable_aborts(self): self.seed("gone-cust", "2026-01-01") r = self.run_gc("--apply", FAKE_EP0_FAIL="1")