From cd13c093a34619e31d7c412c9f95b8f01fd867df Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 1 Nov 2026 23:01:25 +0100 Subject: [PATCH] docs: preserve reviewed NetKingdom session artifacts --- .../workplan-manifest.json | 14 + .../final-integration-r1/workplan.patch | 129 ++ .../workplan.postimage.md | 1065 +++++++++++++++++ .../final-integration-r1/workplan.preimage.md | 996 +++++++++++++++ .../native-fixture/README.md | 125 ++ .../native-fixture/diagnostic_protocol.py | 88 ++ .../native-fixture/driver.py | 320 +++++ .../native-fixture/execution-result.json | 300 +++++ .../native-fixture/helper-selection-r2.json | 32 + .../native-fixture/manifest-delta-r2.json | 28 + .../native-fixture/manifest-delta-r3.json | 29 + .../native-fixture/manifest-delta-r4.json | 31 + .../native-fixture/manifest-delta-r5.json | 30 + .../native-fixture/manifest.json | 146 +++ .../native-fixture/preparation-r2.json | 13 + .../native-fixture/preparation-r3.json | 30 + .../native-fixture/preparation-r4.json | 17 + .../native-fixture/preparation-r5.json | 20 + .../group-postgres-r1/native-fixture/probe.py | 335 ++++++ .../native-fixture/producer-Containerfile.txt | 12 + .../native-fixture/revisions/r1/README.md | 51 + .../r1/coordinator-preflight-report.json | 20 + .../native-fixture/revisions/r1/driver.py | 214 ++++ .../native-fixture/revisions/r1/manifest.json | 123 ++ .../revisions/r1/preflight-r1-failure.json | 13 + .../native-fixture/revisions/r1/probe.py | 287 +++++ .../revisions/r1/source-manifest.json | 65 + .../native-fixture/revisions/r1/source.patch | 626 ++++++++++ .../revisions/r1/test_driver.py | 96 ++ .../revisions/r1/test_service_groups.py | 140 +++ .../native-fixture/revisions/r2/README.md | 72 ++ .../native-fixture/revisions/r2/driver.py | 239 ++++ .../revisions/r2/execution-result.json | 31 + .../revisions/r2/helper-selection-r2.json | 32 + .../revisions/r2/manifest-delta-r2.json | 28 + .../native-fixture/revisions/r2/manifest.json | 129 ++ .../native-fixture/revisions/r2/outcome.json | 13 + .../revisions/r2/preparation-r2.json | 13 + .../native-fixture/revisions/r2/probe.py | 287 +++++ .../revisions/r2/producer-Containerfile.txt | 12 + .../revisions/r2/test_driver.py | 107 ++ .../revisions/r2/test_service_groups.py | 140 +++ .../native-fixture/revisions/r3/README.md | 93 ++ .../revisions/r3/diagnostic_protocol.py | 86 ++ .../native-fixture/revisions/r3/driver.py | 320 +++++ .../revisions/r3/execution-result.json | 195 +++ .../revisions/r3/helper-selection-r2.json | 32 + .../revisions/r3/manifest-delta-r2.json | 28 + .../revisions/r3/manifest-delta-r3.json | 29 + .../native-fixture/revisions/r3/manifest.json | 140 +++ 50 files changed, 7421 insertions(+) create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan-manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.postimage.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.preimage.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/execution-result.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r3.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r4.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r5.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r3.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r4.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r5.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/coordinator-preflight-report.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/preflight-r1-failure.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/probe.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source-manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_service_groups.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/execution-result.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/helper-selection-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest-delta-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/outcome.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/preparation-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/probe.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/producer-Containerfile.txt create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_service_groups.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/README.md create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/diagnostic_protocol.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/driver.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/execution-result.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/helper-selection-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r2.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r3.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest.json diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan-manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan-manifest.json new file mode 100644 index 00000000..eaf1ee77 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan-manifest.json @@ -0,0 +1,14 @@ +{ + "path": "workplans/NK-WP-0048-security-case-types.md", + "preimage_sha256": "08215f6dc91a89706bf456a897d77de4032d47a21cd0252d202249619def135d", + "postimage_sha256": "6f595368aca545c974748b20ceb1caaddfa5162c4542f92a5fb3cb64586fe4cb", + "patch_sha256": "af9f720fba1b931679f866a29e8b3bc7d08f24a7f653db48daf485e35cb16eea", + "canonical_modified": false, + "T20_unchanged": true, + "T14_status": "progress", + "T14_depends_on": [ + "NK-WP-0048-T20", + "NK-WP-0048-T21" + ], + "T21_status": "done" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.patch b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.patch new file mode 100644 index 00000000..515da34b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.patch @@ -0,0 +1,129 @@ +diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md +--- a/workplans/NK-WP-0048-security-case-types.md ++++ b/workplans/NK-WP-0048-security-case-types.md +@@ -691,31 +691,30 @@ + ```task + id: NK-WP-0048-T14 + status: progress +-depends_on: [NK-WP-0048-T20] ++depends_on: [NK-WP-0048-T20, NK-WP-0048-T21] + priority: high + state_hub_task_id: "fce95440-a25b-5f27-8ef3-a3d182099113" + ``` + +-Owner: NetKingdom. Next action: specify a CNPG-backed journal through the +-existing `rapp-postgres` database-per-consumer contract, or obtain an explicit +-scoped governed exception to accepted ADR-0015 before any production SQLite +-proposal. The implemented SQLite backend supplies local/disposable proof only; +-a new PVC is not an admitted production path. Prepare the native writer-isolation +-contract for the existing identity-provisioner, and bind the +-existing protected owner callbacks to the accepted case/source/principal, +-per-attempt evidence and session revocation. Identify the actual deployment and +-custody owner through the existing ADR-0015 routes before proposing a rollout; +-an existing service token, caller-supplied JSON or provenance receipt establishes +-no new authorization. +- +-2026-10-17 refinement: T20 extracts the completed durable provenance source +-candidate and disposable LLDAP compatibility proof from this task. The source +-returns exact creation inventories, retains prestate/intents across process +-restart and refuses detected shared-member/group-identity drift. Its publication +-and projection are still pending the coordinator's separate integration receipt. +-This narrows T14 without closing its original externally referenced native gate. +-The task remains `todo` because preparation of the native contracts can proceed; +-no missing owner assent is represented as received. ++Owner: NetKingdom. Next action: bind the completed T21 PostgreSQL journal to ++an admitted dedicated CNPG consumer through the existing `rapp-postgres` ++database-per-consumer contract. Identify the actual deployment/package and ++custody owner, admitted placement/capacity and role/renewable lease/TLS target; ++implement the independently retained exact content checkpoint and authoritative ++tenant/case/source/principal callbacks before enabling governed group operations. ++The injection-only backend and fixture callbacks do not supply production scope ++or authorization. Existing service tokens, caller-supplied JSON and provenance ++receipts confer no new grant. The default production endpoint remains disabled ++without the trusted integration. SQLite remains local/disposable only; any ++production exception requires explicit scoped governance under ADR-0015. ++ ++2026-10-17 refinement: T20 retains the historical durable SQLite source and ++actual disposable LLDAP proof. T21 extracts the completed PostgreSQL source and ++bounded real disposable SQL compatibility proof. T14 remains `progress` for ++native CNPG admission/recovery, protected owner integration and exclusion of all ++external LDAP writers. Separate source publication/projection and independent ++cleanup receipts belong to coordinator follow-through; no missing native assent ++is represented as received. + + Completion still requires independently reviewed/admitted ADR-0015-conforming + CNPG storage (or an explicit governed exception), backup/lost-store recovery and +@@ -994,3 +993,73 @@ + decisions. No package, database, credential, rollout or owner assent was created. + Custodian evidence: group-provenance-r1/native-storage-proposal-r1; final source, + projection and cleanup review: resume-integration-r1/publication-verification.json. ++ ++ ++## T21 — Implement the PostgreSQL journal and prove disposable SQL compatibility ++ ++```task ++id: NK-WP-0048-T21 ++status: done ++depends_on: [NK-WP-0048-T20] ++priority: high ++``` ++ ++Completed 2026-10-17: bounded source preparation and actual disposable PostgreSQL ++compatibility only. T14 retains native storage, custody, callbacks, authorization, ++all-writer isolation and production admission. Coordinator source publication and ++projection remain separate follow-through; no source revision or task UUID is ++invented here. T20 remains the historical local SQLite/actual LLDAP proof. ++ ++The eight-path source candidate adds an injection-only PostgreSQL implementation ++behind the existing owner journal seam, with explicit trusted tenant/directory/ ++case/source/principal/epoch binding and independent continuity callbacks. It ++persists bounded intent/created-only receipts, exact-one version updates, durable ++active fences, and a content-bound journal chain. SQL commits precede independent ++exact-marker acknowledgment; lost replies/checkpoints poison continuation and ++retain uncertainty. A counter-only checkpoint is explicitly insufficient for ++divergent clones. Migration does not create database/schema/credential, admit an ++epoch or clear quarantine; production HTTP activation is absent pending trusted ++callbacks. Pinned psycopg package/image-copy closure is included, without claiming ++an image build or native dependency admission. ++ ++Candidate base `6cb43c0cc55212363a5406b02db046e2384a7c40`; final eight-path patch ++`sha256:5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d`. ++All 82 source tests pass, alongside 13 fixture plan/diagnostic checks and two ++source-backed snapshot regressions. Counts overlap the same source and fixture; ++they are not additive native acceptance or completion percentages. ++ ++The independently reviewed R5 fixture ran the actual SQL migration/backend and ++existing provisioner path against cached pinned disposable PostgreSQL. Directory ++operations deliberately used the existing synthetic LLDAP test double. All ++manifest-declared phase oracles passed: creation-only inventory and DDL refusal; ++separate-process replay/revoke with complete preexisting/shared visible-state ++preservation; foreign-context refusal; concurrent exactly-once reservation and ++stale version refusal; actual database fork with divergent bodies at the same ++counter and fresh-object rejection of the losing exact checkpoint; retained ++uncertainty after checkpoint/directory/SQL response loss; and actual pg_dump/ ++pg_restore rollback refusal against the independently retained fixture checkpoint. ++The checkpoint is fixture-only, not an admitted native owner evidence holder. ++ ++Only generated credentials/state were used, with two nonroot/read-only containers ++on private loopback, bounded tmpfs/CPU/memory/PIDs/lifetimes, no host ports/mounts, ++no pulls and no production contact. The safe receipt reports both exact owned ++containers absent, with no daemon uncertainty. Independent final pinned-daemon ++cleanup verification remains coordinator follow-through. ++ ++Failed R2/R3/R4 receipts and source revisions remain retained. R2's helper failure ++subphase is unknown; R3 exposed a real backend bug where replacing nested receipt ++objects lost the created ID held by ensure, fixed by acknowledging only the backend ++version after commit/checkpoint. R4 verified that fix through actual SQL bootstrap, ++then exposed fixture snapshot aliasing and empty deleted-group backing entries; ++source-backed regressions corrected the snapshot without weakening complete ++visible-prestate equality, and reject nonempty orphan membership. These refinements ++belong to one trial, not extra native acceptance successes. ++ ++Custodian evidence: `docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/`; ++exact source/pre/postimages and retained predecessors; native R5 manifest ++`sha256:d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051`; ++`final-integration-r1/typed-execution-evidence.json` and byte-exact safe receipt ++(original `/tmp/nk48-postgres-native-review-sguj7t8p/result.json`). No CNPG native ++storage/HA/backup/lease/TLS/capacity/restore admission, authoritative protected ++callbacks, real LDAP writer fencing/CAS/ABA exclusion, owner assent or production ++endpoint activation is claimed. Those remain live T14 work. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.postimage.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.postimage.md new file mode 100644 index 00000000..1211b25a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.postimage.md @@ -0,0 +1,1065 @@ +--- +id: NK-WP-0048 +type: workplan +title: "Establish SecurityCaseTypes so that known security cases are light" +domain: infotech +repo: net-kingdom +status: active +flavor: implementation +owner: claude-code +topic_slug: netkingdom +created: "2026-10-08" +updated: "2026-10-17" +depends_on: [] +related_repos: + - ops-mason + - ops-warden + - key-cape + - railiance-platform + - informed-decision +state_hub_workstream_id: "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb" +--- + +# NK-WP-0048 - Establish SecurityCaseTypes so that known security cases are light + +## Why + +The founder's direction (2026-10-08): setting up a security integration of a +kind that has been done before should be easy, meaning accept the case, then +ops-mason arranges it and ops-warden follows through. Setting up a new kind +needs ample documentation and tests and an informed-decision, because its shape +is part of the security architecture. + +Today every integration is hand-built: per-client helper scripts, per-lane +custody, founder checklists. The trigger was KEY-WP-0038 (Forgejo as an OIDC +client), whose next step was going to be yet another hand-written rollout +script. + +This plan continues the pattern work of NK-WP-0008 (pattern infospace), +NK-WP-0009 (tutorials) and NK-WP-0030 (scenario composition). It turns patterns +into **SecurityCaseTypes** that ops-mason and ops-warden can execute. Canon: +`canon/standards/security-case-types_v0.1.md` (proposed). + +Companion plans: ops-mason `MASON-WP-0010` (materialization) and ops-warden +`WARDEN-WP-0044` (follow-through). + +## Tasks + +```task +id: NK-WP-0048-T01 +status: done +priority: high +state_hub_task_id: "930ac3e9-20ec-5473-98cd-3fe8fddb730f" +``` + +### T01 — Accept the terminology and the canon + +Founder review of `canon/standards/security-case-types_v0.1.md`, including its +section 10 open questions. On acceptance the canon status goes from `proposed` +to `accepted`. + +2026-10-08: accepted by the founder (in session). Canon status `accepted`; +section 10's open questions are carried forward to the first type (T04, T05). + +```task +id: NK-WP-0048-T02 +status: done +priority: high +state_hub_task_id: "19c25d96-a55c-58f0-9168-3fdbc4955fc2" +``` + +### T02 — Inventory the de-facto types already built by hand + +Read-only. List every security integration built so far and group them by shape. +Known starting points: + +- browser OIDC relying parties on key-cape: informed-decision-approver, + coulomb-social, user-engine portal, openbao-admin, and Forgejo (pending) +- confidential service clients with `client_credentials`: secrets-engine + approval, requester and openbao clients, approval-engine operator +- workload KV read lanes on OpenBao: the railiance-platform credential lane + designs and CCRs, for example `railiance-backup-offsite-lane` +- short-lived SSH certificates through ops-warden (`warden sign`) +- tunnelled operator access (ops-bridge) + +For each: the repositories involved, the hand-built steps, attended human steps, +custody paths, verification evidence, and rollback (if any). Output +`docs/security-case-type-inventory.md`, ranking candidate types by how often the +shape recurs and how much manual work each instance costs today. + +2026-10-08: done, `docs/security-case-type-inventory.md`. 36 of 37 CCRs are +already `request_type: workload-kv-read`, a de-facto type reviewed each time as +if it were new. Browser clients: 5 hand-built plus Forgejo, each with its own +rollout script. Confidential service clients need a registration plus two or +three KV-read CCRs each. A forge read identity is needed in bulk by +ORGREF-WP-0002-T06. Recommendation: oidc-browser-relying-party first (unblocks +the forge), workload-kv-read second (largest payoff), then forge-read-identity. + +```task +id: NK-WP-0048-T03 +status: done +depends_on: [NK-WP-0048-T01] +priority: high +state_hub_task_id: "e487975b-73b1-516c-971d-da4cbbb93063" +``` + +### T03 — Schema, template and validator + +- `canon/schemas/security-case-type_v0.1.schema.json` and + `security-case_v0.1.schema.json` +- `security-case-types/_template/` with `type.yaml`, `README.md` and tests to copy +- a validator, extending `tools/security-scenario-composer/`, that checks a type, + checks a case against its type, and renders the case into the composer's + deterministic plan (still `execution.permitted: false`) + +Acceptance: the validator rejects missing sections, secret-shaped parameter +values, unpinned providers and cases against non-`implemented` types, with tests +for each. + +2026-10-08: done. `canon/schemas/security-case-type_v0.1.schema.json` and +`security-case_v0.1.schema.json`; `tools/security-case-types/security_case_validator.py` +(`validate-type`, `validate-case`, `render`, reusing the scenario composer); +`security-case-types/_template/` (a valid, runnable example type over +`net-kingdom.local-identity`, with README sections, an example case and tests that +travel with every copy). 31 tests pass, including the composer's existing suite. + +```task +id: NK-WP-0048-T04 +status: done +depends_on: [NK-WP-0048-T02, NK-WP-0048-T03] +priority: high +state_hub_task_id: "2fffe112-7d55-5680-9952-78d74f44f87f" +``` + +### T04 — Define the first type: browser OIDC relying party + +`security-case-types/oidc-browser-relying-party/`, version 1.0: + +- parameters: client id, display name, exact redirect URIs, scopes, client type + (confidential or public), MFA pin, the groups the consuming service maps, + custody path for the secret (confidential only), consuming service and its + owner +- composition: key-cape client registration (key-cape declares it as a playbook + capability), secret minting into custody (railiance-platform), LLDAP group + existence and membership (net-kingdom identity-provisioner), handover to the + consuming service +- materialization (ops-mason) and follow-through (ops-warden) sections +- verification: discovery lists the redirect URI; a test sign-in returns the + expected claims for one human; anonymous access to the custody path is denied +- rollback: remove exactly this client and its custody path +- README: threat model, MFA rationale, why redirect URIs are exact, which steps + still need an attended login and how that is minimised +- reference instances: informed-decision-approver, coulomb-social, user-engine + portal, openbao-admin + +Acceptance: complete per canon section 3, tests green, status `proposed`. + +2026-10-08: done. `security-case-types/oidc-browser-relying-party/` 1.0 (proposed): +`type.yaml` with 12 parameters (the custody reference is required only for +confidential clients), composition over key-cape's new +`key-cape.oidc-client-registration` declaration (contract PASS, `maturity: +planned`), materialization and follow-through, verification and rollback, all +conditional on client type where it matters. One attended step, +`register-client`, justified by the live registry being a Kubernetes Secret. +README with threat model, alternatives, an honest fit table (portal and vergabe +fit; Forgejo is the first confidential case; openbao-admin, coulomb.social and +informed-decision-approver do not fit 1.0), and known limits. Examples: Forgejo +(checked against the accepted request) and a public portal. The validator gained +`when` / `required_when` conditions and provider discovery. 43 tests pass. +ops-mason's and ops-warden's sections are drafted in the type. Their owners +review them in MASON-WP-0010-T01 and WARDEN-WP-0044-T01 before acknowledgment +(T05). + +```task +id: NK-WP-0048-T05 +status: done +depends_on: [NK-WP-0048-T04, MASON-WP-0010-T01, WARDEN-WP-0044-T01] +priority: high +state_hub_task_id: "3d987d3d-0797-538b-86e4-51185a327ae2" +``` + +### T05 — Acknowledge the type through informed-decision + +Prepare the Decision Memo from the type's README and `type.yaml` and submit it +through informed-decision's production intake. On acknowledgment the type +moves to `acknowledged`. + +2026-10-08: Decision Memo prepared. Hub decision `e418263e-a71f-4f01-a970-c87c9e187c09`. +`security-case-types/oidc-browser-relying-party/decision/`: `packet.md` (question, +one act, what acknowledging does not do, the shape, alternatives, attended +steps, limits, evidence), `build_intake.py` and the built `intake.json` (memo +class `decision-record`, review group `net-kingdom-admins`, resolves the hub +decision on answer). The envelope passes informed-decision's own +`validate_submission`. Queuing it is an attended operator step, a write inside +the production dispatcher pod: + + kubectl -n state-hub exec -i deploy/state-hub-memo-dispatch -- \ + python -m api.services.memo_dispatch --queue /data/private enqueue --envelope /dev/stdin \ + < security-case-types/oidc-browser-relying-party/decision/intake.json + +The answer then appears for review in informed-decision. + +2026-10-08: queued by the founder (`pending`, `queued`) after one refusal: the +first envelope's idempotency key was not State Hub's stable +`state-hub..` form, and the builder now validates against both +sides. The dispatcher delivered it to informed-decision (ledger 7 → 8 +delivered). T05 waits for the founder's answer in informed-decision, which +resolves hub decision `e418263e`. + +2026-10-08: ops-mason's review (MASON-WP-0010-T01) folded into the type, still +1.0 `proposed`: `secret_ref` is no longer a parameter, the type derives +`platform/workloads/keycape/{client_id}`; policies are +`workload-kv-read-keycape-{client_id}-eso` and +`workload-kv-read-keycape-{client_id}-{consuming_service}`, each with a templated +body (`read` on that one data path); new `openbao_role` +`external-secrets-keycape-{client_id}` (Kubernetes auth, ESO service account, +15m, no default policy); catalog entry `{consuming_service}-keycape-client`; +the two rollback steps on the KV path collapse into one metadata delete. The +schema and validator now require a `body` on policy and role items. 105 tests +pass. Two consequences: the delivered memo describes the type before these +changes (a case-set custody reference, two differently named policies, no ESO +role), so the founder answers on a superseded shape unless the memo is +re-sent; and Forgejo's custody path moves from the accepted request's proposed +`platform/workloads/forgejo/netkingdom-oidc-client` to +`platform/workloads/keycape/forgejo`, which key-cape (KEY-WP-0038) and +railiance-apps (RAPPS-WP-0016-T02) need to adopt in T06. ops-warden's review +(WARDEN-WP-0044-T01) is still in progress. + +2026-10-08: revised memo `nk-wp-0048-t05-oidc-browser-relying-party-r2` built +(`packet.md`, `intake.json`, packet `sha256:af6435e2…`); it states that it +supersedes the first memo and passes informed-decision's and the dispatcher's +validation. It needs a new memo id because the dispatcher refuses new content +under a delivered idempotency key (`idempotency_conflict`). Queuing it is the +same attended step as above. Both memos serve hub decision `e418263e`; the +first answer resolves it, so the first memo must not stay open: an accept on it +would acknowledge the superseded shape, and a decline would resolve the +decision against r2. Close it with `return` (a reviewer verb that requests a +new revision and does not resolve the hub decision; only accept and decline +do), commenting that r2 supersedes it, then answer r2. `withdraw` exists in +the vocabulary but is not offered to reviewers, and lifecycle `revise` only +applies to lifecycle-registered memos, not intake-delivered ones. + +2026-10-09: the founder reports r2 enqueued and the first memo returned. At +close, hub decision `e418263e` still showed `open` (not verified whether r2 +was answered or the answer is still in transit; the dispatcher ledger was not +read). ops-warden's review (WARDEN-WP-0044-T01, ops-warden +`history/2026-10-08-oidc-rp-follow-through-review.md`, d470f2e) arrived at +21:25, before r2 was built, and is not in r2. Required before WARDEN-WP-0044-T02: +R1 a per-step `lane` (missing: identity-provisioner caller credential, +railiance-platform broker grant), R2 canon meaning of `warden_executes: true` +(caller proxy, caller identity, no standing ops-warden token; ADR-0002), R3 a +per-step `value_handling`. Refinements R4 (one type-level catalog lane instead +of a per-case entry, which conflicts with ops-mason's item 5), R5 (ESO Ready gate +before bind-env), R6 (exact accepted memberships), R7 (mfa-pinned and +secret-delivered checks). Next: if r2 was accepted, fold R1-R3 and R5-R7 in as +a follow-up and settle R4 between ops-warden and ops-mason before T06; if not, +hold r2, fold them in and build r3. + key-cape and railiance-apps were told about Forgejo's new custody +path. + +2026-10-09: r2 was not answered (hub decision `e418263e` open; dispatcher 9 +delivered), so r2 is held and r3 replaces it. ops-warden withdrew R4 for R4′ +(message a66a9204): one catalog entry per case with its body templated in the +type. Folded in: + +- Canon section 3: a narrow meaning of `warden_executes: true` (caller proxy, + caller identity, no standing credential; ops-warden ADR-0002), plus per-step + `lane`, `value_handling` and an optional `ready_check`. A catalog entry body is + now required, like policy and role bodies. +- Schema and validator enforce all of these. +- The type: lanes `identity-provisioner-groups` and + `keycape-client-secret-generate` (both new) and `key-cape-oidc-login`; + `mint-secret` is `generated_in_custody`; a Ready gate on `deliver-secret`; + `ensure-groups` adds exactly the accepted case's memberships; new checks + `secret-delivered` and `mfa-pinned`; the catalog-entry body follows R4′. +- 110 tests pass. + +Memo r3 (`nk-wp-0048-t05-oidc-browser-relying-party-r3`, packet +`sha256:726dfc01…`) passes informed-decision's and the dispatcher's validation. +The founder queues it with the same command, returns r2 with a comment that r3 +supersedes it, then answers r3. + +Prerequisites for T06, not for T05: +- the two new lanes, each through its owner; +- a founder-approved rollout of an identity-provisioner image containing + 630ebfd (the deployed one is main-6c4fcaf from 2026-09-24, without + `/v1/groups/ensure`). + +ops-mason's request to create LLDAP group `fluid-telegram-operators` (message +259ec26d) was answered: not possible from here by that path; the founder creates +it in the LLDAP admin UI. + +2026-10-09: ops-warden finished its side of WARDEN-WP-0044-T01 (message +`58d33f40`, commit 0e7ceb8): the revised type conforms to `warden case +check-type`, with no further changes asked of the type. This acknowledges that +review. Its later addendum R8 (message `af274783`, ops-warden +`history/2026-10-08-oidc-rp-follow-through-review.md`) showed that lane +`identity-provisioner-groups` cannot exist as declared: the provisioner's only +HTTP credential is user-engine's runtime token, and its NetworkPolicy admits +only user-engine pods, so a caller-proxy lane would act as user-engine. Taken +option 1 (ops-warden's recommendation): `ensure-groups` and its rollback are +attended, in the `register-client` session on `key-cape-oidc-login`, run inside +the provisioner pod; no token is read. Option 2 (an operator credential of its +own on the provisioner) stays open for a later version. `warden case +check-type` now reports only `keycape-client-secret-generate` +(railiance-platform) as missing. 89 tests pass. + +Hub decision `e418263e` was still open, so memo r4 +(`nk-wp-0048-t05-oidc-browser-relying-party-r4`, packet `sha256:452ea421…`) +replaces r3; it passes informed-decision's and the dispatcher's validation. The +founder queues it with the same command, returns r2/r3 if still open with a +comment that r4 supersedes them, then answers r4. + +Prerequisites for T06 are now: lane `keycape-client-secret-generate` +(railiance-platform), and a founder-approved rollout of an identity-provisioner +image containing 630ebfd. + +2026-10-09: done. The founder queued r4 (`pending`, `queued`), returned r3 as +superseded and accepted r4 in informed-decision. informed-decision's hub +write-back is off in production (`INFD_HUB_WRITEBACK` unset), so net-kingdom +resolved hub decision `e418263e` from the founder's report, naming memo r4 in +the rationale. The type is `acknowledged`; it becomes `implemented` with T06. + +```task +id: NK-WP-0048-T06 +status: wait +depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, NK-WP-0048-T17, NK-WP-0048-T18, NK-WP-0048-T20, MASON-WP-0010-T03, WARDEN-WP-0044-T03] +blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status." +priority: high +state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" +``` + +### T06 — First KnownSecurityCase: Forgejo + +Write `security-cases/forgejo-netkingdom-oidc.yaml` from the parameters the +founder accepted in KEY-WP-0038-T01 (`key-cape/docs/forgejo-oidc-client-request.yaml`), +so that acceptance carries over as the case acceptance. ops-mason materializes it; +ops-warden follows through and verifies it. The type becomes `implemented` once +this reference case is live. + +This replaces the hand-built path proposed for KEY-WP-0038-T02. + +2026-10-09: case written, `security-cases/forgejo-netkingdom-oidc.yaml`, status +`accepted`: the example's parameters plus the founder's 2026-10-08 acceptance of +the key-cape request (owner and founder), with the custody-path derivation +covered by the type's acknowledgment (memo r4). It validates and renders (plan +`execution.permitted: false`; three attended steps on `key-cape-oidc-login`); +it needs `--allow-unimplemented` until it is live, since it is the case that +makes the type `implemented`. A test keeps it equal to the type's example. +Still open before it runs: ops-mason renders and builds it (MASON-WP-0010-T02, +T03, unblocked by T05), ops-warden plans and runs it (WARDEN-WP-0044-T02, T03), +railiance-platform's `keycape-client-secret-generate` lane (asked by ops-mason +`789feb39` and ops-warden `d9f889b1`, both unread), and a founder-approved +identity-provisioner rollout containing 630ebfd. + +2026-10-09: identity-provisioner rolled out, founder-approved in session: +`main-630ebfd` (`sha256:bafb4860…`, built by the image workflow) replaces +`main-6c4fcaf` (`sha256:ffacd5d7…`; only change: 630ebfd, additive). Applied +with `kubectl set image` on the Deployment only (generation 17 → 18, 1/1 Ready); +the file's Service, NetworkPolicies and Ingress were not reapplied. Verified in +the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz` +200; no token used, no group changed. Rollback: `kubectl -n sso set image +deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`. +The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows. + +2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan` +(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason +materializes it, and a materialized copy plans cleanly once +`keycape-client-secret-generate` exists. Its observation that `bind-env` carries +a literal `KEYCAPE__CLIENT_SECRET`: the validator now renders a +`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py` +derives the name) and rejects unknown specs, and follow-through and rollback +text is now checked for unknown placeholders too (patch-level, canon section 6). +The type keeps the literal until ops-warden's planner (`text.format`) renders the +spec as well; asked of ops-warden. 53 tests pass. + +2026-10-10: ops-warden rendered `{param:env}` in `warden case plan` +(23ba584, message `c1df14b7`; other specs and `!conversion` refused). The type +adopts it as a 1.0 patch (canon section 6): `bind-env` and its rollback now read +`KEYCAPE_{client_id:env}_CLIENT_SECRET`. ops-mason reports the Forgejo case +rendered and built (message `1ebddae9`; CCR drafts handed to railiance-platform), +so T06 now waits on the platform apply, the mint-secret lane and the attended +register-client session. + +2026-10-10: key-cape adopted the type's custody path (key-cape ab5cccd, message +`88a4559c`): `docs/forgejo-oidc-client-request.yaml` and KEY-WP-0038-T02 name +`platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, delivered as +`sso/keycape-forgejo-client` through `external-secrets-keycape-forgejo`, and the +`forgejo/netkingdom-oidc-client` proposal is withdrawn. This matches the type and +the Forgejo example; no change needed here. + +2026-10-10: key-cape's type-composition review (key-cape 6038483, message +`8e5ed5f6`) found the composition sound with two blocking findings, taken as a +1.0 patch (stricter validation and documentation, canon section 6): +R1, redirect URIs now refuse `?` and `#` in the type pattern and in +`keycape-client.py`'s `REDIRECT` (KeyCape refuses `?` at startup and the single +replica would stop); R2, `allowed_scopes` always includes `groups`, the renderer +refuses `access_groups` without it, and `access_groups` now says the consumer +enforces the gate because KeyCape has none; R3, `mfa_required` "false" means the +per-user privacyIDEA policy; R4, register-client's justification records the +restart for every client, the quiet window, and that the rendered registry must +pass KeyCape's client validation before it. The accepted Forgejo case and both +examples still validate; 116 tests pass. +Open, needs a decision: R2's verification step that the consumer enforces the +group gate is a new verification step, hence 1.1 (minor), and the validator pins +cases to the exact type version, so the accepted 1.0 Forgejo case would need +re-pinning. Also open with key-cape: a KeyCape entry point that validates a +rendered client registry without starting the server; until then +`keycape-client.py` mirrors the redirect rule. + +2026-10-10: founder go for 1.1, so no 1.0 run overlaps. The type adds +verification `consumer-enforces-groups`, and hand-over now requires the consumer +to enforce `access_groups` (minor, canon section 6; risk class unchanged, no new +acknowledgment; history in the type README). The accepted Forgejo case and both +examples are re-pinned to `oidc-browser-relying-party@1.1` before any apply; the +acknowledgment intake keeps `@1.0` as the record of what was acknowledged. +ops-mason's build and ops-warden's case-ref digest predate this and need a +re-render. + +2026-10-10: ops-mason re-rendered the Forgejo case from `@1.1` (message +`22c6bba6`; plan digest `af1f4f81…`, no collisions) and rebuilt the bundle. The +rendered OpenBao objects are identical to 1.0, so the already applied +CCR-2026-0041 stands; 1.1 changes only consumer-side steps. key-cape shipped +`keycape check-config` (c528daa, message `a7b0aeff`): KeyCape's startup checks +without secret values, including the new duplicate-clientId rule. +`keycape-client.py --check-config ` now runs it on the live and the +proposed registry, fed on stdin, and apply refuses a proposed registry that +fails; register-client's entry point names the option (1.1 patch). The deployed +image predates c528daa, so the binary is built from key-cape source for now. +Before the next KeyCape rollout (Forgejo register-client or the CCR-2026-0018 +window), the attended operator runs `inspect --check-config` so the live +registry is checked too. + +```task +id: NK-WP-0048-T07 +status: wait +depends_on: [NK-WP-0048-T06] +blocking_reason: "The next types follow once the first one is proven end to end." +priority: medium +state_hub_task_id: "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7" +``` + +### T07 — Queue the next types + +From the T02 ranking, open the next type drafts (likely the confidential service +client and the workload KV read lane), each to be acknowledged on its own. +Record which existing hand-built instances get adopted as cases, without +rewriting them in place. + +```task +id: NK-WP-0048-T08 +status: done +priority: high +state_hub_task_id: "8e519082-dc8b-5e46-b660-03d03e55dcf6" +``` + +### T08 — identity-provisioner: ensure service-access groups + +The type's `ensure-groups` step calls `identity-provisioner groups ensure --groups + --members `. The provisioner already has idempotent +`_ensure_group` and `_add_group`, but exposes only tenant-scoped operations. Add +the general operation (create if absent, add the listed memberships, never +remove others), its rollback (remove only this case's memberships), tests, and +the ops-warden route for its service identity. + +2026-10-08: done. `identity-provisioner/provisioner.py` gained +`ensure_service_groups` (`POST /v1/groups/ensure`) and `revoke_service_groups` +(`POST /v1/groups/revoke`), behind the existing service-token check. Names +matching `tenant*`, `netkingdom-*` and `lldap_*` are reserved; members must +already exist; everything is validated before any change; memberships are read +back before success; nothing else is ever removed; groups are deleted only when +asked and empty. 7 new tests (39 in the provisioner suite). The type's +`ensure-groups` step and its rollback now name these endpoints. The ops-warden +route for the provisioner's service token belongs to WARDEN-WP-0044-T01. + +```task +id: NK-WP-0048-T09 +status: done +priority: high +state_hub_task_id: "0ee12dfc-8260-5038-bd39-cc7fb4113be0" +``` + +### T09 — The `keycape-client` entry point and its declaration + +Planned first as key-cape KEY-WP-0038-T04. Moved here because the machinery a +live registration needs already lives in net-kingdom, where KeyCape is deployed +(`sso-mfa/k8s/keycape/`): the contained kubectl, the cluster-identity pin and the +issuer pin. A copy in key-cape would drift. + +2026-10-08: done. `sso-mfa/k8s/keycape/keycape-client.py` derives the KeyCape +client record from a case (`client-from-case`). It adds or removes exactly one +client with the existing safeguards (byte-preserving merge, exact match or +refuse, shared-reference refusal, compare-and-swap, read-back, contained errors), +requires the Deployment env binding first for confidential clients, and requires +the case acceptance record for `apply` instead of a per-client founder decision +id. 15 tests, including a regression showing that the vergabe case reproduces +its hand-built registration exactly; all 71 helper tests pass. Declaration +`capabilities/playbooks/net-kingdom.keycape-client-registration.yaml` (contract +PASS, `maturity: implemented`) replaces key-cape's draft. The type now pins it, +and for confidential clients adds `deliver-secret` (delivery from custody into +the sso namespace) and `bind-env` (Deployment env), attended in the same session +as `register-client`. The per-client helpers stay until their clients are +adopted as cases. + +2026-10-10 — NetKingdom owner review of the execution seam, source only: +ops-warden/docs/security-case-execution-proposal.md SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was reviewed against INTENT, SecurityCase canon, acknowledged type1.1, current +case and owner source. docs/security-case-execution-owner-review.md supports +the closed adapter/receipt direction with explicit versioned schema, structural +materialization provenance, unchanged caller/session boundaries, safe CAS +reconciliation and exact-created-object rollback requirements. This is owner +review, not approval. Supporting file digests are in the adjacent sources JSON. + +Current source exposes a circular wait: ops-mason T03 waits on mint while Warden +and the broker refuse a non-materialized case. Structural acceptance therefore +needs its own native pinned receipt before mint, not a successful mint before +materialized. The platform broker additionally pins type1.0 while Forgejo is +accepted at1.1; platform-owner alignment and tests are required, not an unchecked +version bypass. T06 remains wait. No case/type lifecycle status, grant, authority +or live runtime changed by this review. A proposed informed-decision would +adopt the source contract only; native materialization and execution acceptance +stay distinct. + +Final proposal re-review: staged Warden proposal SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +adds only the explicitly local reducer appendix, not an accepted wire protocol +or executor. The exact-final review and sources JSON supersede the earlier +1035f049... proposal review. Platform staged the exact type1.1 broker correction +with ten fake-backend subprocess checks (script SHA256 +9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d). +The next source work is the versioned closed adapter/receipt/materialization +contract, not another relaxation of the broker's status gate. Native work still +requires installed broker provenance, structural reader receipts, exact new +materialized-case bytes, caller grant/session revocation and verified execution. +No installed/native acceptance is inferred from that staged correction. + +2026-10-11 — new source-only type1.2 architecture acknowledgment: +Confirmed native founder ACCEPT of infd-20261011-forgejo-type12-source-r1@1 +at06:47:39Z, approval8e9332c8-754a-420c-b8b7-445cb4fe6765. Exact frozen packet +0b377d647ca748cd03c92f9792396412048055d4bde9f7bb06ff6d413f7acd68 and submission +585cee731de30d2b37883a57abfd9eb32f023ee694e8d911989109d5caf80569; native receipt +retained in docs/evidence/2026-10-11-forgejo-type12-source-acknowledgment/. +The complete acknowledged1.2 source package is isolated under +security-case-type-revisions/1.2/oidc-browser-relying-party; default loader and +current accepted1.1 case remain untouched because load_type is single-version. +The new proposed1.2 case is NOT accepted/materialized/live and has no acceptance +fields. Actual reference-migration design approval is a separate Apps owner +receipt; no1.1 approval is inherited. Source-recording review/test proof and exact +next gates are in docs/forgejo-type12-source-acknowledgment-followthrough.md. +T06 remains wait and T07 remains its existing dependent task; type is NOT +implemented and no first-case/native grant or broader workplan finish is asserted. + +2026-10-11 — bounded explicit version selection source preparation for T06: +The validator now offers optional exact version_roots/--version-root VERSION=PATH +selection with duplicate/mismatch/traversal/symlink refusal and no fallback. The +default1.1 source and historicalacceptedcase hashes/ordinaryrefusal/preparation +render stayunchanged; actual1.1 is acknowledged, not implemented. Acknowledged1.2 +ordinaryadmission also refuses; optionalpreparation remains nonexecuting.43local +validatorchecks pass and original1.1 CLIrenderbytes match exactly. Makerproof: +docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json. +FormalNetownerreview/reference-source integration and all existing native adapter, +provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no +new residual or firstcase/typeimplementation/native grant is inferred. + + +2026-10-16 — isolated source preparation for T06 (canonical integration pending): +Added `check-evidence` to the existing validator CLI. It reuses exact version +selection and ordinary type/case/render admission, checks actual Mason structural +request source/inventory/projection pins and reads actual Warden journal rows +read-only. Successful rows require exact type-declared descriptors and attempt/ +revocation fingerprints. Existing acknowledged types without machine descriptors +remain plan-only. The compatibility result never claims authenticated native +completion: Request and journal fingerprints are not portable native evidence. +Owner authentication/status binding and actual native gates remain unresolved; +T06/T07 statuses and both type/case lifecycles remain unchanged. No dispatch, +credential access or authority expansion is introduced. See tool README for the +existing owner protocol boundaries and source integration tests. + +Concrete next Warden owner integration: current `Executor.admit` refuses every +case parameter whose sensitivity is not public/operational, so browser +`redirect_uris`, `allowed_scopes`, `client_type`, `mfa_required` (and 1.2 +`consumer_namespace`) fail after otherwise prepared admission. The sensitive +classification is a security-change review level, not secret content. The +Warden owner must validate through NetKingdom's existing exact-version case/type +validator, then project only descriptor-declared safe target metadata; do not +remove the credential/value refusal or copy complete parameters into journals. +Actual owner descriptor acknowledgment and independent evidence admission remain +prerequisites, with normal admission still refusing unimplemented types. + + +## T10 — Source compatibility checker for Mason and Warden evidence + +```task +id: NK-WP-0048-T10 +status: done +priority: high +state_hub_task_id: "4b06ca97-c1e4-5680-bf22-ab75ab1646a4" +``` + +Completed 2026-10-16 in published source `c31d21ba`. The read-only +`check-evidence` command checks the actual Mason structural request and Warden +journal source protocols against an exact version/render. Independent source +review passed 103 tests at that checkpoint. It always reports native completion +false and execution prohibited. Fixtures and local journal fingerprints do not +provide authenticated owner/native evidence. This records completed source scope +previously described under T06; it does not duplicate native acceptance credit. + +## T11 — Validate the optional machine-descriptor pin contract + +```task +id: NK-WP-0048-T11 +status: done +priority: high +state_hub_task_id: "ef2a0119-f59e-5e45-9519-115da9df7b85" +``` + +Completed 2026-10-16. Reproduced the JSON schema rejecting a valid +`execution_descriptors` map while the Python validator accepted malformed maps. +The schema now supports the existing Warden optional exact-byte pin map; semantic +validation binds keys to declared steps, bounds the map and checks digest shape. +The direct evidence API refuses malformed maps before accessing them. Twenty-one +new checks and all 127 affected source checks passed. No current type/case bytes, +status, owner descriptors or native acceptance changed. Source evidence and +publication receipt: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-acceptance-next/`. + +## T12 — Prepare NetKingdom-owned attended step adapters + +```task +id: NK-WP-0048-T12 +status: todo +priority: high +state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd" +``` + +Owner: NetKingdom. Retained scope: prepare bounded attended Deployment env, +config verification and restart/readiness steps around the existing helpers. +Next action: identify exact current entry points and their revision/readback +contracts, then implement fixed source-pinned callbacks or receipt-only attended +steps. Completion requires positive/refusal checks, exact target/source pins and +uncertainty behavior without acquiring user-engine runtime identity or exposing +registry/client secrets. Existing caller/session, cluster/issuer, CAS/readback +and status-only output boundaries remain mandatory. + +2026-10-16 task refinement: the original broad adapter scope is split into T13 +(completed registration source candidate), T14 (group creation provenance), T15 +(durable registration recovery) and T16 (descriptor/artifact selection and owner +review). This preserves T12's task ID and Hub UUID while giving each remaining +outcome an owner and completion gate. T06 now names every source dependency; +its native/reference lifecycle acceptance scope remains unchanged. Completion +of any source task supplies no Warden, Platform or Apps assent and no native +execution authority. + +## T13 — Prepare the fixed registration adapter source candidate + +```task +id: NK-WP-0048-T13 +status: done +priority: high +state_hub_task_id: "b264c10b-565c-5d29-b788-03cf5fbd2883" +``` + +Completed 2026-10-16, source implementation and synthetic fixture proof only. +Added an injection-only adapter around actual `keycape-client.run`: fixed +case/principal/human/lane/source/acceptance binding, existing CAS/readback, +metadata-only owner evidence and per-attempt parent-session revocation boundary. +The descriptor source composition pins the exact checker executable digest +alongside adapter/helper source. Creation-only rollback authenticates its +creation receipt and refuses changed postcreation UID/resourceVersion; +pre-existing clients confer no rollback authority. Missing readback, evidence, +revocation or process-local creation provenance cannot become retry-safe success. + +Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/` +R3 candidate, explicit patch and postimages, retained R1 rejected custody finding +and R2 revision, and 36 passing source checks (16 new, 20 existing). Three checks +use actual Warden kernel admission/dispatch/rollback with published Warden +`d97652a3bb5fa733b063d71a1b74068efcccf572` producer pins unchanged. Owner trust, +descriptor/type/case and Kubernetes objects remain synthetic fixtures. This +records a completed source deliverable, not independent review, publication, +type acknowledgment, exercised procedure, identity admission or native browser +acceptance. Independent root source review: Custodian browser-adapter-next/coordinator-review-r3.json. Source integration and normal publication/synchronization receipts: Custodian browser-adapter-next/canonical-integration.json. These receipts supply no other-owner assent or native authority. + +Source publication: `0775c2bfa41af236301155bb5a69bbf088bdf823`, independently +reviewed by the Custodian coordinator before integration. The required initial +projection and ordinary UUID writeback both completed with zero failures and +zero automation errors. Generated-only brief commits were refused by the normal +hook; exact pre-existing staged/working brief bytes were restored. The separate +canonical integration receipt retains those refusals and projection evidence. +Native admission and all other-owner acceptance remain open. + +## T14 — Admit governed group rollback against native owner boundaries + +```task +id: NK-WP-0048-T14 +status: progress +depends_on: [NK-WP-0048-T20, NK-WP-0048-T21] +priority: high +state_hub_task_id: "fce95440-a25b-5f27-8ef3-a3d182099113" +``` + +Owner: NetKingdom. Next action: bind the completed T21 PostgreSQL journal to +an admitted dedicated CNPG consumer through the existing `rapp-postgres` +database-per-consumer contract. Identify the actual deployment/package and +custody owner, admitted placement/capacity and role/renewable lease/TLS target; +implement the independently retained exact content checkpoint and authoritative +tenant/case/source/principal callbacks before enabling governed group operations. +The injection-only backend and fixture callbacks do not supply production scope +or authorization. Existing service tokens, caller-supplied JSON and provenance +receipts confer no new grant. The default production endpoint remains disabled +without the trusted integration. SQLite remains local/disposable only; any +production exception requires explicit scoped governance under ADR-0015. + +2026-10-17 refinement: T20 retains the historical durable SQLite source and +actual disposable LLDAP proof. T21 extracts the completed PostgreSQL source and +bounded real disposable SQL compatibility proof. T14 remains `progress` for +native CNPG admission/recovery, protected owner integration and exclusion of all +external LDAP writers. Separate source publication/projection and independent +cleanup receipts belong to coordinator follow-through; no missing native assent +is represented as received. + +Completion still requires independently reviewed/admitted ADR-0015-conforming +CNPG storage (or an explicit governed exception), backup/lost-store recovery and +migration of legacy requests; a native +writer-isolation/fencing or provider CAS/version contract that closes external +writer races and remove/re-add ABA; exact case/source/principal and current +native object bindings for protected callbacks; and actual per-attempt evidence, +revocation and readback/refusal verification through the existing owner paths. +Only independently retained confirmed creations may be undone. Every +pre-existing/shared group and membership must remain preserved; unresolved +intents never expire into retry authority. The disposable fixture supplies none +of these native custody, authorization, owner-acceptance or production claims. + +## T15 — Define authenticated recovery for lost registration process state + +```task +id: NK-WP-0048-T15 +status: todo +priority: high +state_hub_task_id: "1b825705-4848-5edc-832f-b1622cd9bdb7" +``` + +Owner: NetKingdom for registration state, with Warden owning kernel evidence +admission. Next action: select the exact existing owner evidence holder/readback +contract and define authenticated lookup for the original creation attempt. +Completion requires restart/lost-receipt fixtures that bind case/source/principal, +original created-only provenance and current native revision before any undo; +uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses +post-restart rollback and installs no reconciliation callback. A new broker or +unverified JSON receipt is not a recovery contract. Actual evidence-holder +acceptance and attended recovery remain separately authorized native work. + +## T16 — Select and review the actual machine descriptor composition + +```task +id: NK-WP-0048-T16 +status: todo +priority: high +state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e" +``` + +Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden +independently owns global provenance/admission. Next action: obtain review of the +exact checker source/artifact proposed by T17 and identify native owner evidence/ +session callback contracts, then assemble the complete step descriptor set and +a proposed explicit type version selection. T13's historical descriptor retains +its synthetic checker; T17 supplies an artifact-bound proposal, not admission. +Completion requires separate owner source review of exact machine descriptors, +artifact/source/config pins, normal type/version admission and independent +evidence-verification boundaries. Warden d97652a refreshes producer validator +pins for NetKingdom 015694ee; it does not attest T13's new adapter. Pinning any +machine descriptor into an acknowledged type is a separately reviewed source +change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual +consumer login/group enforcement. Neither their assent nor native acceptance is +supplied by this task. + +2026-10-16 source checkpoint: T19 below extracts the completed per-attempt +executable snapshot implementation and reviewed fixture proof. T16 retains +native executable storage/custody, exact owner evidence/session callbacks, +complete descriptor composition and ordinary type/version admission. The +successor descriptor/source pins are proposed only; the earlier descriptor +history is preserved. T14 group provenance and T15 durable restart/recovery +remain independent open requirements. No native acceptance is counted here. + +## T17 — Build and exercise the exact offline config-checker artifact + +```task +id: NK-WP-0048-T17 +status: done +priority: high +state_hub_task_id: "8c3e4baa-a218-5996-af9e-fe1153a46dac" +``` + +Completed 2026-10-16, isolated artifact/source fixture proof only. Built the +existing `key-cape/src/cmd/keycape` command from published clean source revision +`3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, source tree +`f0010b9cab17dbc3a787628a29657d98b900320f`, with cached Go 1.23.0, +CGO disabled, Linux/amd64 v1, trimmed paths, disabled VCS stamping and offline +read-only module selection. The full retained closure pins 108 source files, +9876 toolchain files and ten used module source directories. Three isolated +build locations produced identical 13633409-byte artifacts: +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. + +Eight disposable checks use the actual executable and maintained helper/kernel, +with complete synthetic configs and a minimal environment: add/created-only +removal, normal pinned producer admission, rollback, lost-readback uncertainty, +stale revisions, substituted checker refusal, invalid startup config and missing +secret-env refusal. No checker stub or producer-pin override remains. The +artifact-bound descriptor proposal is +`sha256:76b1153d188062ece53fcd5e8d35290fdbfd02a69dd79d4b001fc42781afcdc3`. +Evidence: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-checker-artifact-next/`. +The first two build attempts succeeded but their closure collection failed; +both failures are retained, as is refusal of the older incomplete synthetic +fixture. No production config, credentials or cluster mutation was used. + +This extracts completed build/fixture scope from T16 without claiming KeyCape +owner endorsement, native checker selection/storage custody, acknowledged type +integration, native evidence/session callbacks or attended browser acceptance. +T16 retains exact owner review and composition/admission scope. The attended +registration procedure remains unexercised. + +## T18 — Make checker input and executable selection deterministic + +```task +id: NK-WP-0048-T18 +status: done +priority: high +state_hub_task_id: "9c45da71-fb4c-5f20-9065-90f448f37c64" +``` + +Completed 2026-10-16, isolated source correction and real-artifact fixture proof +only; independent integration/publication remain separately recorded. KeyCape +source review identified non-secret `KEYCAPE_ACCOUNT_PORTAL_URL` and +`KEYCAPE_BROWSER_LOGOUT_URL` overrides in config.Load. The original helper +inherited ambient environment, allowing valid supplied configuration to be +rejected or invalid supplied URL configuration to be admitted. A bare relative +checker filename could also be replaced by PATH lookup after the adapter had +hashed the selected local file. All three counterexamples are retained against +the unchanged predecessor. + +The existing helper now resolves the selected checker file to an absolute path +and passes an explicit empty subprocess environment. Three opt-in real-checker +regressions verify both URL-verdict directions and relative-file selection under +a non-checker PATH decoy. All 39 affected checks pass with the exact supplied +artifact, as do 11 real-artifact helper/kernel scenarios. Source evidence: +Custodian `docs/changes/netkingdom-target-2026-10-16/browser-checker-environment-r2/`. +Historical descriptors remain retained; the helper correction changes successor +source-composition pins and requires their separate review/admission. An absolute +path is not immutable custody and does not eliminate file replacement races; +T16 retains native executable storage/custody and callback/type admission. +No production configuration, credentials or cluster mutation was used. + +Source integration and publication: `916ba4b33e03179274d03b78bd898a4843e8653f`, following independent +Custodian source review in browser-checker-environment-r2/coordinator-review.json. +The ordinary protected projection pass assigned T17/T18 UUIDs with zero failures +and zero automation errors; generated-only brief commit refusal is retained. +The exact prior staged/working brief bytes were restored. Final source/publication/ +projection evidence is retained in Custodian browser-checker-environment-r2/ +canonical-integration.json. These source receipts do not admit native executable +custody, owner callbacks, acknowledged type changes or browser identities. + +## T19 — Bind checker execution to verified immutable attempt bytes + +```task +id: NK-WP-0048-T19 +status: done +priority: high +state_hub_task_id: "f50fdcd1-00be-5c3c-be0c-345aa2126032" +``` + +Completed 2026-10-16, independently reviewed source implementation and synthetic +fixture proof only. The registration adapter now opens a bounded regular checker +artifact without following its terminal symlink, verifies the reviewed digest, +and snapshots the exact bytes into a write/grow/shrink/seal-removal-sealed Linux +memfd before helper access. Live and proposed config checks execute that same fd +through procfs with the existing empty environment; the fd closes on completion +or failure. Unsupported snapshot/seal/exec policy refuses through the existing +parent-session revocation and uncertainty boundaries. The earlier unbounded +pre-snapshot path read was removed during independent review; actual adapter +symlink/FIFO/oversize refusal tests verify helper access never starts. + +Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-resume-r1/`, +including exact base/preimages/postimages/patch, proposed descriptor, coordinator +source review and canonical integration receipt. Independent Custodian checks +passed 49 affected source tests and 12 actual Go checker/helper/kernel scenarios; +the sets overlap and are not separate acceptance counts. Existing published +Warden d97652a producer pins remain unchanged. The exact KeyCape artifact remains +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. +The predecessor path-replacement refusal and the first expanded reopened-fd test +failure are retained; the latter fixture was corrected to restore write mode +before independently proving seals block reopened-fd writes and shared mmap. + +This source task adds no native owner custody, callback, evidence trust, type +acknowledgment, credential access, consumer sign-in or rollback acceptance. +Executable memfds, Linux seals and usable procfs are explicit runtime dependencies +and still require native admission. This is an extracted source outcome from +T16, not completion of T16; T12/T14/T15 and all external-owner gates remain open. + + +Publication verified by the coordinator after the source worker usage limit: +`8790dd18ae83c4651ef3d1d923312a118a5b436b` is the actual remote main revision. +All five canonical source hashes match the reviewed packet. Normal synchronization +completed with zero failures/automation errors; all 19 task statuses match the +projection. T19's assigned UUID is retained. The original staged and working brief +bytes were restored and independently compared. The exact source, projection and +publication checks are in Custodian browser-resume-r1/canonical-integration.json. +Native acceptance remains open under T16 and the other named owner tasks. + + +## T20 — Prepare durable group provenance and prove disposable LLDAP compatibility + +```task +id: NK-WP-0048-T20 +status: done +priority: high +state_hub_task_id: "863fcbdb-e246-51ba-87ca-a2f44d01d6f8" +``` + +Completed 2026-10-17, source candidate preparation and bounded synthetic +compatibility proof only. This extracts evidenced implementation/fixture scope +from T14; T14 retains native storage, writer isolation, protected callback and +owner-acceptance gates. Source integration/publication and State Hub projection +are pending the coordinator's separate receipts; no revision or task UUID is +invented here. + +The existing identity-provisioner group ensure/revoke interface now has a +reviewed candidate for a bounded owner-private SQLite journal, proven only in +local/disposable use. The initial storage instructions wrongly suggested a native +persistent-volume path; source review against accepted ADR-0015 corrected them +before publication. Production SQLite-on-a-PVC is not admitted: T14 retains +CNPG/rapp-postgres storage admission or a separately governed exception. +The local journal saves +complete prestate and intent before each attempted directory mutation, inventories +only confirmed new groups/memberships, binds original ensure idempotency and +correlation to the exact request digest, and refuses unsigned caller inventories +or legacy broad revoke requests. Replays never recreate removed state. Missing, +replaced or untrusted storage and missing intent rows stop execution; definite +before-mutation refusals retain evidence without stranding unrelated valid work. +Interrupted writes/readback remain unresolved across reopening. Revoke validates +all targeted native ids and complete member postimages before destructive work, +removes only confirmed creations and never deletes a preexisting empty group. +Server/configuration wiring, image-copy closure and owner storage/request +migration instructions are included. The existing endpoint's caller +authentication remains the existing boundary, not a new grant. + +The candidate was prepared against NetKingdom +`878abc93588e12b6da1bac055cfbb3abb8d7adf1`; exact source patch digest +`sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e`. +The independent Custodian review passed all 65 provisioner checks. The earlier +62-check candidate, two independent-review findings and subsequent fixes remain +retained rather than overwritten. Seven additional fixture-plan checks exercise +the preparation boundary; these counts overlap the same candidate and do not +measure task-completion percentage or independent acceptance. + +A separately reviewed bounded fixture then exercised the exact candidate against +cached pinned disposable LLDAP and Python images, through actual GraphQL. All 14 +closed compatibility/containment oracles passed: exact creation inventory, +separate-process SQLite reopening/replay/revoke, complete preexisting directory +preservation including empty/shared groups and memberships, refusal of shared +membership drift, retained uncertainty after a lost response following a real +addition, and refusal of unknown/cross-case receipt bindings. The fixture used +only newly generated synthetic users/passwords/key material in private tmpfs, +with a network-none anchor and shared loopback helper, no host ports/mounts, +no image pulls and no production or actual credential contact. The coordinator's +safe receipt reports both exact owned containers absent after cleanup; final +independent cleanup verification remains the coordinator's integration duty. + +Evidence: Custodian +`docs/changes/netkingdom-target-2026-10-16/group-provenance-r1/`, including exact +source base/pre/postimages, patch/manifest, independent review and retained +refinements. Native fixture manifest +`sha256:8a0514f5f14137355972400cd4852f48c892316d28e3abd3c86635d7cb81eef2`; +retained safe result `native-fixture/execution-result.json` (original +`/tmp/nk48-groups-native-review-5pywb19z/result.json`). The source task supplies +no native owner assent, production rollout, protected session/evidence callbacks, +ordinary type admission or consumer login proof. LLDAP has no conditional +membership mutation or generation token here: exact readback cannot exclude +ABA or a concurrent external writer, so T14 remains open. + +2026-10-17 verified source follow-through: source commit `4f65e41` passed normal +local scanning and all 65 canonical provisioner tests. The actual disposable +LLDAP receipt passed all 14 oracles; independent verification on the pinned +daemon found both exact names and the run label absent. Source integration +includes the ADR-0015 documentation correction; the journal remains local-only. + +T14 source preparation is underway. Twenty-three pinned public sources identify +NetKingdom implementation/reconciliation, Platform custody/database governance +and rapp-postgres consumer provisioning; the inspected runtime binding does not +assign identity-provisioner a package. The existing User Engine runtime token +and pod route are not a protected-case caller lane. The native proposal preserves +the same journal seam on a dedicated CNPG consumer and requires authoritative +scope binding, admitted role/lease/placement/recovery and all LDAP writers fenced. +The declared exclusive maintenance window is not mechanical exclusion of WebUI, +helper or lifecycle writers. Capacity placement and EUR cost/risk remain owner +decisions. No package, database, credential, rollout or owner assent was created. +Custodian evidence: group-provenance-r1/native-storage-proposal-r1; final source, +projection and cleanup review: resume-integration-r1/publication-verification.json. + + +## T21 — Implement the PostgreSQL journal and prove disposable SQL compatibility + +```task +id: NK-WP-0048-T21 +status: done +depends_on: [NK-WP-0048-T20] +priority: high +``` + +Completed 2026-10-17: bounded source preparation and actual disposable PostgreSQL +compatibility only. T14 retains native storage, custody, callbacks, authorization, +all-writer isolation and production admission. Coordinator source publication and +projection remain separate follow-through; no source revision or task UUID is +invented here. T20 remains the historical local SQLite/actual LLDAP proof. + +The eight-path source candidate adds an injection-only PostgreSQL implementation +behind the existing owner journal seam, with explicit trusted tenant/directory/ +case/source/principal/epoch binding and independent continuity callbacks. It +persists bounded intent/created-only receipts, exact-one version updates, durable +active fences, and a content-bound journal chain. SQL commits precede independent +exact-marker acknowledgment; lost replies/checkpoints poison continuation and +retain uncertainty. A counter-only checkpoint is explicitly insufficient for +divergent clones. Migration does not create database/schema/credential, admit an +epoch or clear quarantine; production HTTP activation is absent pending trusted +callbacks. Pinned psycopg package/image-copy closure is included, without claiming +an image build or native dependency admission. + +Candidate base `6cb43c0cc55212363a5406b02db046e2384a7c40`; final eight-path patch +`sha256:5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d`. +All 82 source tests pass, alongside 13 fixture plan/diagnostic checks and two +source-backed snapshot regressions. Counts overlap the same source and fixture; +they are not additive native acceptance or completion percentages. + +The independently reviewed R5 fixture ran the actual SQL migration/backend and +existing provisioner path against cached pinned disposable PostgreSQL. Directory +operations deliberately used the existing synthetic LLDAP test double. All +manifest-declared phase oracles passed: creation-only inventory and DDL refusal; +separate-process replay/revoke with complete preexisting/shared visible-state +preservation; foreign-context refusal; concurrent exactly-once reservation and +stale version refusal; actual database fork with divergent bodies at the same +counter and fresh-object rejection of the losing exact checkpoint; retained +uncertainty after checkpoint/directory/SQL response loss; and actual pg_dump/ +pg_restore rollback refusal against the independently retained fixture checkpoint. +The checkpoint is fixture-only, not an admitted native owner evidence holder. + +Only generated credentials/state were used, with two nonroot/read-only containers +on private loopback, bounded tmpfs/CPU/memory/PIDs/lifetimes, no host ports/mounts, +no pulls and no production contact. The safe receipt reports both exact owned +containers absent, with no daemon uncertainty. Independent final pinned-daemon +cleanup verification remains coordinator follow-through. + +Failed R2/R3/R4 receipts and source revisions remain retained. R2's helper failure +subphase is unknown; R3 exposed a real backend bug where replacing nested receipt +objects lost the created ID held by ensure, fixed by acknowledging only the backend +version after commit/checkpoint. R4 verified that fix through actual SQL bootstrap, +then exposed fixture snapshot aliasing and empty deleted-group backing entries; +source-backed regressions corrected the snapshot without weakening complete +visible-prestate equality, and reject nonempty orphan membership. These refinements +belong to one trial, not extra native acceptance successes. + +Custodian evidence: `docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/`; +exact source/pre/postimages and retained predecessors; native R5 manifest +`sha256:d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051`; +`final-integration-r1/typed-execution-evidence.json` and byte-exact safe receipt +(original `/tmp/nk48-postgres-native-review-sguj7t8p/result.json`). No CNPG native +storage/HA/backup/lease/TLS/capacity/restore admission, authoritative protected +callbacks, real LDAP writer fencing/CAS/ABA exclusion, owner assent or production +endpoint activation is claimed. Those remain live T14 work. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.preimage.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.preimage.md new file mode 100644 index 00000000..aee04735 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/final-integration-r1/workplan.preimage.md @@ -0,0 +1,996 @@ +--- +id: NK-WP-0048 +type: workplan +title: "Establish SecurityCaseTypes so that known security cases are light" +domain: infotech +repo: net-kingdom +status: active +flavor: implementation +owner: claude-code +topic_slug: netkingdom +created: "2026-10-08" +updated: "2026-10-17" +depends_on: [] +related_repos: + - ops-mason + - ops-warden + - key-cape + - railiance-platform + - informed-decision +state_hub_workstream_id: "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb" +--- + +# NK-WP-0048 - Establish SecurityCaseTypes so that known security cases are light + +## Why + +The founder's direction (2026-10-08): setting up a security integration of a +kind that has been done before should be easy, meaning accept the case, then +ops-mason arranges it and ops-warden follows through. Setting up a new kind +needs ample documentation and tests and an informed-decision, because its shape +is part of the security architecture. + +Today every integration is hand-built: per-client helper scripts, per-lane +custody, founder checklists. The trigger was KEY-WP-0038 (Forgejo as an OIDC +client), whose next step was going to be yet another hand-written rollout +script. + +This plan continues the pattern work of NK-WP-0008 (pattern infospace), +NK-WP-0009 (tutorials) and NK-WP-0030 (scenario composition). It turns patterns +into **SecurityCaseTypes** that ops-mason and ops-warden can execute. Canon: +`canon/standards/security-case-types_v0.1.md` (proposed). + +Companion plans: ops-mason `MASON-WP-0010` (materialization) and ops-warden +`WARDEN-WP-0044` (follow-through). + +## Tasks + +```task +id: NK-WP-0048-T01 +status: done +priority: high +state_hub_task_id: "930ac3e9-20ec-5473-98cd-3fe8fddb730f" +``` + +### T01 — Accept the terminology and the canon + +Founder review of `canon/standards/security-case-types_v0.1.md`, including its +section 10 open questions. On acceptance the canon status goes from `proposed` +to `accepted`. + +2026-10-08: accepted by the founder (in session). Canon status `accepted`; +section 10's open questions are carried forward to the first type (T04, T05). + +```task +id: NK-WP-0048-T02 +status: done +priority: high +state_hub_task_id: "19c25d96-a55c-58f0-9168-3fdbc4955fc2" +``` + +### T02 — Inventory the de-facto types already built by hand + +Read-only. List every security integration built so far and group them by shape. +Known starting points: + +- browser OIDC relying parties on key-cape: informed-decision-approver, + coulomb-social, user-engine portal, openbao-admin, and Forgejo (pending) +- confidential service clients with `client_credentials`: secrets-engine + approval, requester and openbao clients, approval-engine operator +- workload KV read lanes on OpenBao: the railiance-platform credential lane + designs and CCRs, for example `railiance-backup-offsite-lane` +- short-lived SSH certificates through ops-warden (`warden sign`) +- tunnelled operator access (ops-bridge) + +For each: the repositories involved, the hand-built steps, attended human steps, +custody paths, verification evidence, and rollback (if any). Output +`docs/security-case-type-inventory.md`, ranking candidate types by how often the +shape recurs and how much manual work each instance costs today. + +2026-10-08: done, `docs/security-case-type-inventory.md`. 36 of 37 CCRs are +already `request_type: workload-kv-read`, a de-facto type reviewed each time as +if it were new. Browser clients: 5 hand-built plus Forgejo, each with its own +rollout script. Confidential service clients need a registration plus two or +three KV-read CCRs each. A forge read identity is needed in bulk by +ORGREF-WP-0002-T06. Recommendation: oidc-browser-relying-party first (unblocks +the forge), workload-kv-read second (largest payoff), then forge-read-identity. + +```task +id: NK-WP-0048-T03 +status: done +depends_on: [NK-WP-0048-T01] +priority: high +state_hub_task_id: "e487975b-73b1-516c-971d-da4cbbb93063" +``` + +### T03 — Schema, template and validator + +- `canon/schemas/security-case-type_v0.1.schema.json` and + `security-case_v0.1.schema.json` +- `security-case-types/_template/` with `type.yaml`, `README.md` and tests to copy +- a validator, extending `tools/security-scenario-composer/`, that checks a type, + checks a case against its type, and renders the case into the composer's + deterministic plan (still `execution.permitted: false`) + +Acceptance: the validator rejects missing sections, secret-shaped parameter +values, unpinned providers and cases against non-`implemented` types, with tests +for each. + +2026-10-08: done. `canon/schemas/security-case-type_v0.1.schema.json` and +`security-case_v0.1.schema.json`; `tools/security-case-types/security_case_validator.py` +(`validate-type`, `validate-case`, `render`, reusing the scenario composer); +`security-case-types/_template/` (a valid, runnable example type over +`net-kingdom.local-identity`, with README sections, an example case and tests that +travel with every copy). 31 tests pass, including the composer's existing suite. + +```task +id: NK-WP-0048-T04 +status: done +depends_on: [NK-WP-0048-T02, NK-WP-0048-T03] +priority: high +state_hub_task_id: "2fffe112-7d55-5680-9952-78d74f44f87f" +``` + +### T04 — Define the first type: browser OIDC relying party + +`security-case-types/oidc-browser-relying-party/`, version 1.0: + +- parameters: client id, display name, exact redirect URIs, scopes, client type + (confidential or public), MFA pin, the groups the consuming service maps, + custody path for the secret (confidential only), consuming service and its + owner +- composition: key-cape client registration (key-cape declares it as a playbook + capability), secret minting into custody (railiance-platform), LLDAP group + existence and membership (net-kingdom identity-provisioner), handover to the + consuming service +- materialization (ops-mason) and follow-through (ops-warden) sections +- verification: discovery lists the redirect URI; a test sign-in returns the + expected claims for one human; anonymous access to the custody path is denied +- rollback: remove exactly this client and its custody path +- README: threat model, MFA rationale, why redirect URIs are exact, which steps + still need an attended login and how that is minimised +- reference instances: informed-decision-approver, coulomb-social, user-engine + portal, openbao-admin + +Acceptance: complete per canon section 3, tests green, status `proposed`. + +2026-10-08: done. `security-case-types/oidc-browser-relying-party/` 1.0 (proposed): +`type.yaml` with 12 parameters (the custody reference is required only for +confidential clients), composition over key-cape's new +`key-cape.oidc-client-registration` declaration (contract PASS, `maturity: +planned`), materialization and follow-through, verification and rollback, all +conditional on client type where it matters. One attended step, +`register-client`, justified by the live registry being a Kubernetes Secret. +README with threat model, alternatives, an honest fit table (portal and vergabe +fit; Forgejo is the first confidential case; openbao-admin, coulomb.social and +informed-decision-approver do not fit 1.0), and known limits. Examples: Forgejo +(checked against the accepted request) and a public portal. The validator gained +`when` / `required_when` conditions and provider discovery. 43 tests pass. +ops-mason's and ops-warden's sections are drafted in the type. Their owners +review them in MASON-WP-0010-T01 and WARDEN-WP-0044-T01 before acknowledgment +(T05). + +```task +id: NK-WP-0048-T05 +status: done +depends_on: [NK-WP-0048-T04, MASON-WP-0010-T01, WARDEN-WP-0044-T01] +priority: high +state_hub_task_id: "3d987d3d-0797-538b-86e4-51185a327ae2" +``` + +### T05 — Acknowledge the type through informed-decision + +Prepare the Decision Memo from the type's README and `type.yaml` and submit it +through informed-decision's production intake. On acknowledgment the type +moves to `acknowledged`. + +2026-10-08: Decision Memo prepared. Hub decision `e418263e-a71f-4f01-a970-c87c9e187c09`. +`security-case-types/oidc-browser-relying-party/decision/`: `packet.md` (question, +one act, what acknowledging does not do, the shape, alternatives, attended +steps, limits, evidence), `build_intake.py` and the built `intake.json` (memo +class `decision-record`, review group `net-kingdom-admins`, resolves the hub +decision on answer). The envelope passes informed-decision's own +`validate_submission`. Queuing it is an attended operator step, a write inside +the production dispatcher pod: + + kubectl -n state-hub exec -i deploy/state-hub-memo-dispatch -- \ + python -m api.services.memo_dispatch --queue /data/private enqueue --envelope /dev/stdin \ + < security-case-types/oidc-browser-relying-party/decision/intake.json + +The answer then appears for review in informed-decision. + +2026-10-08: queued by the founder (`pending`, `queued`) after one refusal: the +first envelope's idempotency key was not State Hub's stable +`state-hub..` form, and the builder now validates against both +sides. The dispatcher delivered it to informed-decision (ledger 7 → 8 +delivered). T05 waits for the founder's answer in informed-decision, which +resolves hub decision `e418263e`. + +2026-10-08: ops-mason's review (MASON-WP-0010-T01) folded into the type, still +1.0 `proposed`: `secret_ref` is no longer a parameter, the type derives +`platform/workloads/keycape/{client_id}`; policies are +`workload-kv-read-keycape-{client_id}-eso` and +`workload-kv-read-keycape-{client_id}-{consuming_service}`, each with a templated +body (`read` on that one data path); new `openbao_role` +`external-secrets-keycape-{client_id}` (Kubernetes auth, ESO service account, +15m, no default policy); catalog entry `{consuming_service}-keycape-client`; +the two rollback steps on the KV path collapse into one metadata delete. The +schema and validator now require a `body` on policy and role items. 105 tests +pass. Two consequences: the delivered memo describes the type before these +changes (a case-set custody reference, two differently named policies, no ESO +role), so the founder answers on a superseded shape unless the memo is +re-sent; and Forgejo's custody path moves from the accepted request's proposed +`platform/workloads/forgejo/netkingdom-oidc-client` to +`platform/workloads/keycape/forgejo`, which key-cape (KEY-WP-0038) and +railiance-apps (RAPPS-WP-0016-T02) need to adopt in T06. ops-warden's review +(WARDEN-WP-0044-T01) is still in progress. + +2026-10-08: revised memo `nk-wp-0048-t05-oidc-browser-relying-party-r2` built +(`packet.md`, `intake.json`, packet `sha256:af6435e2…`); it states that it +supersedes the first memo and passes informed-decision's and the dispatcher's +validation. It needs a new memo id because the dispatcher refuses new content +under a delivered idempotency key (`idempotency_conflict`). Queuing it is the +same attended step as above. Both memos serve hub decision `e418263e`; the +first answer resolves it, so the first memo must not stay open: an accept on it +would acknowledge the superseded shape, and a decline would resolve the +decision against r2. Close it with `return` (a reviewer verb that requests a +new revision and does not resolve the hub decision; only accept and decline +do), commenting that r2 supersedes it, then answer r2. `withdraw` exists in +the vocabulary but is not offered to reviewers, and lifecycle `revise` only +applies to lifecycle-registered memos, not intake-delivered ones. + +2026-10-09: the founder reports r2 enqueued and the first memo returned. At +close, hub decision `e418263e` still showed `open` (not verified whether r2 +was answered or the answer is still in transit; the dispatcher ledger was not +read). ops-warden's review (WARDEN-WP-0044-T01, ops-warden +`history/2026-10-08-oidc-rp-follow-through-review.md`, d470f2e) arrived at +21:25, before r2 was built, and is not in r2. Required before WARDEN-WP-0044-T02: +R1 a per-step `lane` (missing: identity-provisioner caller credential, +railiance-platform broker grant), R2 canon meaning of `warden_executes: true` +(caller proxy, caller identity, no standing ops-warden token; ADR-0002), R3 a +per-step `value_handling`. Refinements R4 (one type-level catalog lane instead +of a per-case entry, which conflicts with ops-mason's item 5), R5 (ESO Ready gate +before bind-env), R6 (exact accepted memberships), R7 (mfa-pinned and +secret-delivered checks). Next: if r2 was accepted, fold R1-R3 and R5-R7 in as +a follow-up and settle R4 between ops-warden and ops-mason before T06; if not, +hold r2, fold them in and build r3. + key-cape and railiance-apps were told about Forgejo's new custody +path. + +2026-10-09: r2 was not answered (hub decision `e418263e` open; dispatcher 9 +delivered), so r2 is held and r3 replaces it. ops-warden withdrew R4 for R4′ +(message a66a9204): one catalog entry per case with its body templated in the +type. Folded in: + +- Canon section 3: a narrow meaning of `warden_executes: true` (caller proxy, + caller identity, no standing credential; ops-warden ADR-0002), plus per-step + `lane`, `value_handling` and an optional `ready_check`. A catalog entry body is + now required, like policy and role bodies. +- Schema and validator enforce all of these. +- The type: lanes `identity-provisioner-groups` and + `keycape-client-secret-generate` (both new) and `key-cape-oidc-login`; + `mint-secret` is `generated_in_custody`; a Ready gate on `deliver-secret`; + `ensure-groups` adds exactly the accepted case's memberships; new checks + `secret-delivered` and `mfa-pinned`; the catalog-entry body follows R4′. +- 110 tests pass. + +Memo r3 (`nk-wp-0048-t05-oidc-browser-relying-party-r3`, packet +`sha256:726dfc01…`) passes informed-decision's and the dispatcher's validation. +The founder queues it with the same command, returns r2 with a comment that r3 +supersedes it, then answers r3. + +Prerequisites for T06, not for T05: +- the two new lanes, each through its owner; +- a founder-approved rollout of an identity-provisioner image containing + 630ebfd (the deployed one is main-6c4fcaf from 2026-09-24, without + `/v1/groups/ensure`). + +ops-mason's request to create LLDAP group `fluid-telegram-operators` (message +259ec26d) was answered: not possible from here by that path; the founder creates +it in the LLDAP admin UI. + +2026-10-09: ops-warden finished its side of WARDEN-WP-0044-T01 (message +`58d33f40`, commit 0e7ceb8): the revised type conforms to `warden case +check-type`, with no further changes asked of the type. This acknowledges that +review. Its later addendum R8 (message `af274783`, ops-warden +`history/2026-10-08-oidc-rp-follow-through-review.md`) showed that lane +`identity-provisioner-groups` cannot exist as declared: the provisioner's only +HTTP credential is user-engine's runtime token, and its NetworkPolicy admits +only user-engine pods, so a caller-proxy lane would act as user-engine. Taken +option 1 (ops-warden's recommendation): `ensure-groups` and its rollback are +attended, in the `register-client` session on `key-cape-oidc-login`, run inside +the provisioner pod; no token is read. Option 2 (an operator credential of its +own on the provisioner) stays open for a later version. `warden case +check-type` now reports only `keycape-client-secret-generate` +(railiance-platform) as missing. 89 tests pass. + +Hub decision `e418263e` was still open, so memo r4 +(`nk-wp-0048-t05-oidc-browser-relying-party-r4`, packet `sha256:452ea421…`) +replaces r3; it passes informed-decision's and the dispatcher's validation. The +founder queues it with the same command, returns r2/r3 if still open with a +comment that r4 supersedes them, then answers r4. + +Prerequisites for T06 are now: lane `keycape-client-secret-generate` +(railiance-platform), and a founder-approved rollout of an identity-provisioner +image containing 630ebfd. + +2026-10-09: done. The founder queued r4 (`pending`, `queued`), returned r3 as +superseded and accepted r4 in informed-decision. informed-decision's hub +write-back is off in production (`INFD_HUB_WRITEBACK` unset), so net-kingdom +resolved hub decision `e418263e` from the founder's report, naming memo r4 in +the rationale. The type is `acknowledged`; it becomes `implemented` with T06. + +```task +id: NK-WP-0048-T06 +status: wait +depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, NK-WP-0048-T17, NK-WP-0048-T18, NK-WP-0048-T20, MASON-WP-0010-T03, WARDEN-WP-0044-T03] +blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status." +priority: high +state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c" +``` + +### T06 — First KnownSecurityCase: Forgejo + +Write `security-cases/forgejo-netkingdom-oidc.yaml` from the parameters the +founder accepted in KEY-WP-0038-T01 (`key-cape/docs/forgejo-oidc-client-request.yaml`), +so that acceptance carries over as the case acceptance. ops-mason materializes it; +ops-warden follows through and verifies it. The type becomes `implemented` once +this reference case is live. + +This replaces the hand-built path proposed for KEY-WP-0038-T02. + +2026-10-09: case written, `security-cases/forgejo-netkingdom-oidc.yaml`, status +`accepted`: the example's parameters plus the founder's 2026-10-08 acceptance of +the key-cape request (owner and founder), with the custody-path derivation +covered by the type's acknowledgment (memo r4). It validates and renders (plan +`execution.permitted: false`; three attended steps on `key-cape-oidc-login`); +it needs `--allow-unimplemented` until it is live, since it is the case that +makes the type `implemented`. A test keeps it equal to the type's example. +Still open before it runs: ops-mason renders and builds it (MASON-WP-0010-T02, +T03, unblocked by T05), ops-warden plans and runs it (WARDEN-WP-0044-T02, T03), +railiance-platform's `keycape-client-secret-generate` lane (asked by ops-mason +`789feb39` and ops-warden `d9f889b1`, both unread), and a founder-approved +identity-provisioner rollout containing 630ebfd. + +2026-10-09: identity-provisioner rolled out, founder-approved in session: +`main-630ebfd` (`sha256:bafb4860…`, built by the image workflow) replaces +`main-6c4fcaf` (`sha256:ffacd5d7…`; only change: 630ebfd, additive). Applied +with `kubectl set image` on the Deployment only (generation 17 → 18, 1/1 Ready); +the file's Service, NetworkPolicies and Ingress were not reapplied. Verified in +the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz` +200; no token used, no group changed. Rollback: `kubectl -n sso set image +deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`. +The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows. + +2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan` +(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason +materializes it, and a materialized copy plans cleanly once +`keycape-client-secret-generate` exists. Its observation that `bind-env` carries +a literal `KEYCAPE__CLIENT_SECRET`: the validator now renders a +`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py` +derives the name) and rejects unknown specs, and follow-through and rollback +text is now checked for unknown placeholders too (patch-level, canon section 6). +The type keeps the literal until ops-warden's planner (`text.format`) renders the +spec as well; asked of ops-warden. 53 tests pass. + +2026-10-10: ops-warden rendered `{param:env}` in `warden case plan` +(23ba584, message `c1df14b7`; other specs and `!conversion` refused). The type +adopts it as a 1.0 patch (canon section 6): `bind-env` and its rollback now read +`KEYCAPE_{client_id:env}_CLIENT_SECRET`. ops-mason reports the Forgejo case +rendered and built (message `1ebddae9`; CCR drafts handed to railiance-platform), +so T06 now waits on the platform apply, the mint-secret lane and the attended +register-client session. + +2026-10-10: key-cape adopted the type's custody path (key-cape ab5cccd, message +`88a4559c`): `docs/forgejo-oidc-client-request.yaml` and KEY-WP-0038-T02 name +`platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, delivered as +`sso/keycape-forgejo-client` through `external-secrets-keycape-forgejo`, and the +`forgejo/netkingdom-oidc-client` proposal is withdrawn. This matches the type and +the Forgejo example; no change needed here. + +2026-10-10: key-cape's type-composition review (key-cape 6038483, message +`8e5ed5f6`) found the composition sound with two blocking findings, taken as a +1.0 patch (stricter validation and documentation, canon section 6): +R1, redirect URIs now refuse `?` and `#` in the type pattern and in +`keycape-client.py`'s `REDIRECT` (KeyCape refuses `?` at startup and the single +replica would stop); R2, `allowed_scopes` always includes `groups`, the renderer +refuses `access_groups` without it, and `access_groups` now says the consumer +enforces the gate because KeyCape has none; R3, `mfa_required` "false" means the +per-user privacyIDEA policy; R4, register-client's justification records the +restart for every client, the quiet window, and that the rendered registry must +pass KeyCape's client validation before it. The accepted Forgejo case and both +examples still validate; 116 tests pass. +Open, needs a decision: R2's verification step that the consumer enforces the +group gate is a new verification step, hence 1.1 (minor), and the validator pins +cases to the exact type version, so the accepted 1.0 Forgejo case would need +re-pinning. Also open with key-cape: a KeyCape entry point that validates a +rendered client registry without starting the server; until then +`keycape-client.py` mirrors the redirect rule. + +2026-10-10: founder go for 1.1, so no 1.0 run overlaps. The type adds +verification `consumer-enforces-groups`, and hand-over now requires the consumer +to enforce `access_groups` (minor, canon section 6; risk class unchanged, no new +acknowledgment; history in the type README). The accepted Forgejo case and both +examples are re-pinned to `oidc-browser-relying-party@1.1` before any apply; the +acknowledgment intake keeps `@1.0` as the record of what was acknowledged. +ops-mason's build and ops-warden's case-ref digest predate this and need a +re-render. + +2026-10-10: ops-mason re-rendered the Forgejo case from `@1.1` (message +`22c6bba6`; plan digest `af1f4f81…`, no collisions) and rebuilt the bundle. The +rendered OpenBao objects are identical to 1.0, so the already applied +CCR-2026-0041 stands; 1.1 changes only consumer-side steps. key-cape shipped +`keycape check-config` (c528daa, message `a7b0aeff`): KeyCape's startup checks +without secret values, including the new duplicate-clientId rule. +`keycape-client.py --check-config ` now runs it on the live and the +proposed registry, fed on stdin, and apply refuses a proposed registry that +fails; register-client's entry point names the option (1.1 patch). The deployed +image predates c528daa, so the binary is built from key-cape source for now. +Before the next KeyCape rollout (Forgejo register-client or the CCR-2026-0018 +window), the attended operator runs `inspect --check-config` so the live +registry is checked too. + +```task +id: NK-WP-0048-T07 +status: wait +depends_on: [NK-WP-0048-T06] +blocking_reason: "The next types follow once the first one is proven end to end." +priority: medium +state_hub_task_id: "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7" +``` + +### T07 — Queue the next types + +From the T02 ranking, open the next type drafts (likely the confidential service +client and the workload KV read lane), each to be acknowledged on its own. +Record which existing hand-built instances get adopted as cases, without +rewriting them in place. + +```task +id: NK-WP-0048-T08 +status: done +priority: high +state_hub_task_id: "8e519082-dc8b-5e46-b660-03d03e55dcf6" +``` + +### T08 — identity-provisioner: ensure service-access groups + +The type's `ensure-groups` step calls `identity-provisioner groups ensure --groups + --members `. The provisioner already has idempotent +`_ensure_group` and `_add_group`, but exposes only tenant-scoped operations. Add +the general operation (create if absent, add the listed memberships, never +remove others), its rollback (remove only this case's memberships), tests, and +the ops-warden route for its service identity. + +2026-10-08: done. `identity-provisioner/provisioner.py` gained +`ensure_service_groups` (`POST /v1/groups/ensure`) and `revoke_service_groups` +(`POST /v1/groups/revoke`), behind the existing service-token check. Names +matching `tenant*`, `netkingdom-*` and `lldap_*` are reserved; members must +already exist; everything is validated before any change; memberships are read +back before success; nothing else is ever removed; groups are deleted only when +asked and empty. 7 new tests (39 in the provisioner suite). The type's +`ensure-groups` step and its rollback now name these endpoints. The ops-warden +route for the provisioner's service token belongs to WARDEN-WP-0044-T01. + +```task +id: NK-WP-0048-T09 +status: done +priority: high +state_hub_task_id: "0ee12dfc-8260-5038-bd39-cc7fb4113be0" +``` + +### T09 — The `keycape-client` entry point and its declaration + +Planned first as key-cape KEY-WP-0038-T04. Moved here because the machinery a +live registration needs already lives in net-kingdom, where KeyCape is deployed +(`sso-mfa/k8s/keycape/`): the contained kubectl, the cluster-identity pin and the +issuer pin. A copy in key-cape would drift. + +2026-10-08: done. `sso-mfa/k8s/keycape/keycape-client.py` derives the KeyCape +client record from a case (`client-from-case`). It adds or removes exactly one +client with the existing safeguards (byte-preserving merge, exact match or +refuse, shared-reference refusal, compare-and-swap, read-back, contained errors), +requires the Deployment env binding first for confidential clients, and requires +the case acceptance record for `apply` instead of a per-client founder decision +id. 15 tests, including a regression showing that the vergabe case reproduces +its hand-built registration exactly; all 71 helper tests pass. Declaration +`capabilities/playbooks/net-kingdom.keycape-client-registration.yaml` (contract +PASS, `maturity: implemented`) replaces key-cape's draft. The type now pins it, +and for confidential clients adds `deliver-secret` (delivery from custody into +the sso namespace) and `bind-env` (Deployment env), attended in the same session +as `register-client`. The per-client helpers stay until their clients are +adopted as cases. + +2026-10-10 — NetKingdom owner review of the execution seam, source only: +ops-warden/docs/security-case-execution-proposal.md SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +was reviewed against INTENT, SecurityCase canon, acknowledged type1.1, current +case and owner source. docs/security-case-execution-owner-review.md supports +the closed adapter/receipt direction with explicit versioned schema, structural +materialization provenance, unchanged caller/session boundaries, safe CAS +reconciliation and exact-created-object rollback requirements. This is owner +review, not approval. Supporting file digests are in the adjacent sources JSON. + +Current source exposes a circular wait: ops-mason T03 waits on mint while Warden +and the broker refuse a non-materialized case. Structural acceptance therefore +needs its own native pinned receipt before mint, not a successful mint before +materialized. The platform broker additionally pins type1.0 while Forgejo is +accepted at1.1; platform-owner alignment and tests are required, not an unchecked +version bypass. T06 remains wait. No case/type lifecycle status, grant, authority +or live runtime changed by this review. A proposed informed-decision would +adopt the source contract only; native materialization and execution acceptance +stay distinct. + +Final proposal re-review: staged Warden proposal SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c +adds only the explicitly local reducer appendix, not an accepted wire protocol +or executor. The exact-final review and sources JSON supersede the earlier +1035f049... proposal review. Platform staged the exact type1.1 broker correction +with ten fake-backend subprocess checks (script SHA256 +9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d). +The next source work is the versioned closed adapter/receipt/materialization +contract, not another relaxation of the broker's status gate. Native work still +requires installed broker provenance, structural reader receipts, exact new +materialized-case bytes, caller grant/session revocation and verified execution. +No installed/native acceptance is inferred from that staged correction. + +2026-10-11 — new source-only type1.2 architecture acknowledgment: +Confirmed native founder ACCEPT of infd-20261011-forgejo-type12-source-r1@1 +at06:47:39Z, approval8e9332c8-754a-420c-b8b7-445cb4fe6765. Exact frozen packet +0b377d647ca748cd03c92f9792396412048055d4bde9f7bb06ff6d413f7acd68 and submission +585cee731de30d2b37883a57abfd9eb32f023ee694e8d911989109d5caf80569; native receipt +retained in docs/evidence/2026-10-11-forgejo-type12-source-acknowledgment/. +The complete acknowledged1.2 source package is isolated under +security-case-type-revisions/1.2/oidc-browser-relying-party; default loader and +current accepted1.1 case remain untouched because load_type is single-version. +The new proposed1.2 case is NOT accepted/materialized/live and has no acceptance +fields. Actual reference-migration design approval is a separate Apps owner +receipt; no1.1 approval is inherited. Source-recording review/test proof and exact +next gates are in docs/forgejo-type12-source-acknowledgment-followthrough.md. +T06 remains wait and T07 remains its existing dependent task; type is NOT +implemented and no first-case/native grant or broader workplan finish is asserted. + +2026-10-11 — bounded explicit version selection source preparation for T06: +The validator now offers optional exact version_roots/--version-root VERSION=PATH +selection with duplicate/mismatch/traversal/symlink refusal and no fallback. The +default1.1 source and historicalacceptedcase hashes/ordinaryrefusal/preparation +render stayunchanged; actual1.1 is acknowledged, not implemented. Acknowledged1.2 +ordinaryadmission also refuses; optionalpreparation remains nonexecuting.43local +validatorchecks pass and original1.1 CLIrenderbytes match exactly. Makerproof: +docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json. +FormalNetownerreview/reference-source integration and all existing native adapter, +provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no +new residual or firstcase/typeimplementation/native grant is inferred. + + +2026-10-16 — isolated source preparation for T06 (canonical integration pending): +Added `check-evidence` to the existing validator CLI. It reuses exact version +selection and ordinary type/case/render admission, checks actual Mason structural +request source/inventory/projection pins and reads actual Warden journal rows +read-only. Successful rows require exact type-declared descriptors and attempt/ +revocation fingerprints. Existing acknowledged types without machine descriptors +remain plan-only. The compatibility result never claims authenticated native +completion: Request and journal fingerprints are not portable native evidence. +Owner authentication/status binding and actual native gates remain unresolved; +T06/T07 statuses and both type/case lifecycles remain unchanged. No dispatch, +credential access or authority expansion is introduced. See tool README for the +existing owner protocol boundaries and source integration tests. + +Concrete next Warden owner integration: current `Executor.admit` refuses every +case parameter whose sensitivity is not public/operational, so browser +`redirect_uris`, `allowed_scopes`, `client_type`, `mfa_required` (and 1.2 +`consumer_namespace`) fail after otherwise prepared admission. The sensitive +classification is a security-change review level, not secret content. The +Warden owner must validate through NetKingdom's existing exact-version case/type +validator, then project only descriptor-declared safe target metadata; do not +remove the credential/value refusal or copy complete parameters into journals. +Actual owner descriptor acknowledgment and independent evidence admission remain +prerequisites, with normal admission still refusing unimplemented types. + + +## T10 — Source compatibility checker for Mason and Warden evidence + +```task +id: NK-WP-0048-T10 +status: done +priority: high +state_hub_task_id: "4b06ca97-c1e4-5680-bf22-ab75ab1646a4" +``` + +Completed 2026-10-16 in published source `c31d21ba`. The read-only +`check-evidence` command checks the actual Mason structural request and Warden +journal source protocols against an exact version/render. Independent source +review passed 103 tests at that checkpoint. It always reports native completion +false and execution prohibited. Fixtures and local journal fingerprints do not +provide authenticated owner/native evidence. This records completed source scope +previously described under T06; it does not duplicate native acceptance credit. + +## T11 — Validate the optional machine-descriptor pin contract + +```task +id: NK-WP-0048-T11 +status: done +priority: high +state_hub_task_id: "ef2a0119-f59e-5e45-9519-115da9df7b85" +``` + +Completed 2026-10-16. Reproduced the JSON schema rejecting a valid +`execution_descriptors` map while the Python validator accepted malformed maps. +The schema now supports the existing Warden optional exact-byte pin map; semantic +validation binds keys to declared steps, bounds the map and checks digest shape. +The direct evidence API refuses malformed maps before accessing them. Twenty-one +new checks and all 127 affected source checks passed. No current type/case bytes, +status, owner descriptors or native acceptance changed. Source evidence and +publication receipt: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-acceptance-next/`. + +## T12 — Prepare NetKingdom-owned attended step adapters + +```task +id: NK-WP-0048-T12 +status: todo +priority: high +state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd" +``` + +Owner: NetKingdom. Retained scope: prepare bounded attended Deployment env, +config verification and restart/readiness steps around the existing helpers. +Next action: identify exact current entry points and their revision/readback +contracts, then implement fixed source-pinned callbacks or receipt-only attended +steps. Completion requires positive/refusal checks, exact target/source pins and +uncertainty behavior without acquiring user-engine runtime identity or exposing +registry/client secrets. Existing caller/session, cluster/issuer, CAS/readback +and status-only output boundaries remain mandatory. + +2026-10-16 task refinement: the original broad adapter scope is split into T13 +(completed registration source candidate), T14 (group creation provenance), T15 +(durable registration recovery) and T16 (descriptor/artifact selection and owner +review). This preserves T12's task ID and Hub UUID while giving each remaining +outcome an owner and completion gate. T06 now names every source dependency; +its native/reference lifecycle acceptance scope remains unchanged. Completion +of any source task supplies no Warden, Platform or Apps assent and no native +execution authority. + +## T13 — Prepare the fixed registration adapter source candidate + +```task +id: NK-WP-0048-T13 +status: done +priority: high +state_hub_task_id: "b264c10b-565c-5d29-b788-03cf5fbd2883" +``` + +Completed 2026-10-16, source implementation and synthetic fixture proof only. +Added an injection-only adapter around actual `keycape-client.run`: fixed +case/principal/human/lane/source/acceptance binding, existing CAS/readback, +metadata-only owner evidence and per-attempt parent-session revocation boundary. +The descriptor source composition pins the exact checker executable digest +alongside adapter/helper source. Creation-only rollback authenticates its +creation receipt and refuses changed postcreation UID/resourceVersion; +pre-existing clients confer no rollback authority. Missing readback, evidence, +revocation or process-local creation provenance cannot become retry-safe success. + +Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/` +R3 candidate, explicit patch and postimages, retained R1 rejected custody finding +and R2 revision, and 36 passing source checks (16 new, 20 existing). Three checks +use actual Warden kernel admission/dispatch/rollback with published Warden +`d97652a3bb5fa733b063d71a1b74068efcccf572` producer pins unchanged. Owner trust, +descriptor/type/case and Kubernetes objects remain synthetic fixtures. This +records a completed source deliverable, not independent review, publication, +type acknowledgment, exercised procedure, identity admission or native browser +acceptance. Independent root source review: Custodian browser-adapter-next/coordinator-review-r3.json. Source integration and normal publication/synchronization receipts: Custodian browser-adapter-next/canonical-integration.json. These receipts supply no other-owner assent or native authority. + +Source publication: `0775c2bfa41af236301155bb5a69bbf088bdf823`, independently +reviewed by the Custodian coordinator before integration. The required initial +projection and ordinary UUID writeback both completed with zero failures and +zero automation errors. Generated-only brief commits were refused by the normal +hook; exact pre-existing staged/working brief bytes were restored. The separate +canonical integration receipt retains those refusals and projection evidence. +Native admission and all other-owner acceptance remain open. + +## T14 — Admit governed group rollback against native owner boundaries + +```task +id: NK-WP-0048-T14 +status: progress +depends_on: [NK-WP-0048-T20] +priority: high +state_hub_task_id: "fce95440-a25b-5f27-8ef3-a3d182099113" +``` + +Owner: NetKingdom. Next action: specify a CNPG-backed journal through the +existing `rapp-postgres` database-per-consumer contract, or obtain an explicit +scoped governed exception to accepted ADR-0015 before any production SQLite +proposal. The implemented SQLite backend supplies local/disposable proof only; +a new PVC is not an admitted production path. Prepare the native writer-isolation +contract for the existing identity-provisioner, and bind the +existing protected owner callbacks to the accepted case/source/principal, +per-attempt evidence and session revocation. Identify the actual deployment and +custody owner through the existing ADR-0015 routes before proposing a rollout; +an existing service token, caller-supplied JSON or provenance receipt establishes +no new authorization. + +2026-10-17 refinement: T20 extracts the completed durable provenance source +candidate and disposable LLDAP compatibility proof from this task. The source +returns exact creation inventories, retains prestate/intents across process +restart and refuses detected shared-member/group-identity drift. Its publication +and projection are still pending the coordinator's separate integration receipt. +This narrows T14 without closing its original externally referenced native gate. +The task remains `todo` because preparation of the native contracts can proceed; +no missing owner assent is represented as received. + +Completion still requires independently reviewed/admitted ADR-0015-conforming +CNPG storage (or an explicit governed exception), backup/lost-store recovery and +migration of legacy requests; a native +writer-isolation/fencing or provider CAS/version contract that closes external +writer races and remove/re-add ABA; exact case/source/principal and current +native object bindings for protected callbacks; and actual per-attempt evidence, +revocation and readback/refusal verification through the existing owner paths. +Only independently retained confirmed creations may be undone. Every +pre-existing/shared group and membership must remain preserved; unresolved +intents never expire into retry authority. The disposable fixture supplies none +of these native custody, authorization, owner-acceptance or production claims. + +## T15 — Define authenticated recovery for lost registration process state + +```task +id: NK-WP-0048-T15 +status: todo +priority: high +state_hub_task_id: "1b825705-4848-5edc-832f-b1622cd9bdb7" +``` + +Owner: NetKingdom for registration state, with Warden owning kernel evidence +admission. Next action: select the exact existing owner evidence holder/readback +contract and define authenticated lookup for the original creation attempt. +Completion requires restart/lost-receipt fixtures that bind case/source/principal, +original created-only provenance and current native revision before any undo; +uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses +post-restart rollback and installs no reconciliation callback. A new broker or +unverified JSON receipt is not a recovery contract. Actual evidence-holder +acceptance and attended recovery remain separately authorized native work. + +## T16 — Select and review the actual machine descriptor composition + +```task +id: NK-WP-0048-T16 +status: todo +priority: high +state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e" +``` + +Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden +independently owns global provenance/admission. Next action: obtain review of the +exact checker source/artifact proposed by T17 and identify native owner evidence/ +session callback contracts, then assemble the complete step descriptor set and +a proposed explicit type version selection. T13's historical descriptor retains +its synthetic checker; T17 supplies an artifact-bound proposal, not admission. +Completion requires separate owner source review of exact machine descriptors, +artifact/source/config pins, normal type/version admission and independent +evidence-verification boundaries. Warden d97652a refreshes producer validator +pins for NetKingdom 015694ee; it does not attest T13's new adapter. Pinning any +machine descriptor into an acknowledged type is a separately reviewed source +change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual +consumer login/group enforcement. Neither their assent nor native acceptance is +supplied by this task. + +2026-10-16 source checkpoint: T19 below extracts the completed per-attempt +executable snapshot implementation and reviewed fixture proof. T16 retains +native executable storage/custody, exact owner evidence/session callbacks, +complete descriptor composition and ordinary type/version admission. The +successor descriptor/source pins are proposed only; the earlier descriptor +history is preserved. T14 group provenance and T15 durable restart/recovery +remain independent open requirements. No native acceptance is counted here. + +## T17 — Build and exercise the exact offline config-checker artifact + +```task +id: NK-WP-0048-T17 +status: done +priority: high +state_hub_task_id: "8c3e4baa-a218-5996-af9e-fe1153a46dac" +``` + +Completed 2026-10-16, isolated artifact/source fixture proof only. Built the +existing `key-cape/src/cmd/keycape` command from published clean source revision +`3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`, source tree +`f0010b9cab17dbc3a787628a29657d98b900320f`, with cached Go 1.23.0, +CGO disabled, Linux/amd64 v1, trimmed paths, disabled VCS stamping and offline +read-only module selection. The full retained closure pins 108 source files, +9876 toolchain files and ten used module source directories. Three isolated +build locations produced identical 13633409-byte artifacts: +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. + +Eight disposable checks use the actual executable and maintained helper/kernel, +with complete synthetic configs and a minimal environment: add/created-only +removal, normal pinned producer admission, rollback, lost-readback uncertainty, +stale revisions, substituted checker refusal, invalid startup config and missing +secret-env refusal. No checker stub or producer-pin override remains. The +artifact-bound descriptor proposal is +`sha256:76b1153d188062ece53fcd5e8d35290fdbfd02a69dd79d4b001fc42781afcdc3`. +Evidence: Custodian +`docs/changes/netkingdom-target-2026-10-16/browser-checker-artifact-next/`. +The first two build attempts succeeded but their closure collection failed; +both failures are retained, as is refusal of the older incomplete synthetic +fixture. No production config, credentials or cluster mutation was used. + +This extracts completed build/fixture scope from T16 without claiming KeyCape +owner endorsement, native checker selection/storage custody, acknowledged type +integration, native evidence/session callbacks or attended browser acceptance. +T16 retains exact owner review and composition/admission scope. The attended +registration procedure remains unexercised. + +## T18 — Make checker input and executable selection deterministic + +```task +id: NK-WP-0048-T18 +status: done +priority: high +state_hub_task_id: "9c45da71-fb4c-5f20-9065-90f448f37c64" +``` + +Completed 2026-10-16, isolated source correction and real-artifact fixture proof +only; independent integration/publication remain separately recorded. KeyCape +source review identified non-secret `KEYCAPE_ACCOUNT_PORTAL_URL` and +`KEYCAPE_BROWSER_LOGOUT_URL` overrides in config.Load. The original helper +inherited ambient environment, allowing valid supplied configuration to be +rejected or invalid supplied URL configuration to be admitted. A bare relative +checker filename could also be replaced by PATH lookup after the adapter had +hashed the selected local file. All three counterexamples are retained against +the unchanged predecessor. + +The existing helper now resolves the selected checker file to an absolute path +and passes an explicit empty subprocess environment. Three opt-in real-checker +regressions verify both URL-verdict directions and relative-file selection under +a non-checker PATH decoy. All 39 affected checks pass with the exact supplied +artifact, as do 11 real-artifact helper/kernel scenarios. Source evidence: +Custodian `docs/changes/netkingdom-target-2026-10-16/browser-checker-environment-r2/`. +Historical descriptors remain retained; the helper correction changes successor +source-composition pins and requires their separate review/admission. An absolute +path is not immutable custody and does not eliminate file replacement races; +T16 retains native executable storage/custody and callback/type admission. +No production configuration, credentials or cluster mutation was used. + +Source integration and publication: `916ba4b33e03179274d03b78bd898a4843e8653f`, following independent +Custodian source review in browser-checker-environment-r2/coordinator-review.json. +The ordinary protected projection pass assigned T17/T18 UUIDs with zero failures +and zero automation errors; generated-only brief commit refusal is retained. +The exact prior staged/working brief bytes were restored. Final source/publication/ +projection evidence is retained in Custodian browser-checker-environment-r2/ +canonical-integration.json. These source receipts do not admit native executable +custody, owner callbacks, acknowledged type changes or browser identities. + +## T19 — Bind checker execution to verified immutable attempt bytes + +```task +id: NK-WP-0048-T19 +status: done +priority: high +state_hub_task_id: "f50fdcd1-00be-5c3c-be0c-345aa2126032" +``` + +Completed 2026-10-16, independently reviewed source implementation and synthetic +fixture proof only. The registration adapter now opens a bounded regular checker +artifact without following its terminal symlink, verifies the reviewed digest, +and snapshots the exact bytes into a write/grow/shrink/seal-removal-sealed Linux +memfd before helper access. Live and proposed config checks execute that same fd +through procfs with the existing empty environment; the fd closes on completion +or failure. Unsupported snapshot/seal/exec policy refuses through the existing +parent-session revocation and uncertainty boundaries. The earlier unbounded +pre-snapshot path read was removed during independent review; actual adapter +symlink/FIFO/oversize refusal tests verify helper access never starts. + +Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-resume-r1/`, +including exact base/preimages/postimages/patch, proposed descriptor, coordinator +source review and canonical integration receipt. Independent Custodian checks +passed 49 affected source tests and 12 actual Go checker/helper/kernel scenarios; +the sets overlap and are not separate acceptance counts. Existing published +Warden d97652a producer pins remain unchanged. The exact KeyCape artifact remains +`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`. +The predecessor path-replacement refusal and the first expanded reopened-fd test +failure are retained; the latter fixture was corrected to restore write mode +before independently proving seals block reopened-fd writes and shared mmap. + +This source task adds no native owner custody, callback, evidence trust, type +acknowledgment, credential access, consumer sign-in or rollback acceptance. +Executable memfds, Linux seals and usable procfs are explicit runtime dependencies +and still require native admission. This is an extracted source outcome from +T16, not completion of T16; T12/T14/T15 and all external-owner gates remain open. + + +Publication verified by the coordinator after the source worker usage limit: +`8790dd18ae83c4651ef3d1d923312a118a5b436b` is the actual remote main revision. +All five canonical source hashes match the reviewed packet. Normal synchronization +completed with zero failures/automation errors; all 19 task statuses match the +projection. T19's assigned UUID is retained. The original staged and working brief +bytes were restored and independently compared. The exact source, projection and +publication checks are in Custodian browser-resume-r1/canonical-integration.json. +Native acceptance remains open under T16 and the other named owner tasks. + + +## T20 — Prepare durable group provenance and prove disposable LLDAP compatibility + +```task +id: NK-WP-0048-T20 +status: done +priority: high +state_hub_task_id: "863fcbdb-e246-51ba-87ca-a2f44d01d6f8" +``` + +Completed 2026-10-17, source candidate preparation and bounded synthetic +compatibility proof only. This extracts evidenced implementation/fixture scope +from T14; T14 retains native storage, writer isolation, protected callback and +owner-acceptance gates. Source integration/publication and State Hub projection +are pending the coordinator's separate receipts; no revision or task UUID is +invented here. + +The existing identity-provisioner group ensure/revoke interface now has a +reviewed candidate for a bounded owner-private SQLite journal, proven only in +local/disposable use. The initial storage instructions wrongly suggested a native +persistent-volume path; source review against accepted ADR-0015 corrected them +before publication. Production SQLite-on-a-PVC is not admitted: T14 retains +CNPG/rapp-postgres storage admission or a separately governed exception. +The local journal saves +complete prestate and intent before each attempted directory mutation, inventories +only confirmed new groups/memberships, binds original ensure idempotency and +correlation to the exact request digest, and refuses unsigned caller inventories +or legacy broad revoke requests. Replays never recreate removed state. Missing, +replaced or untrusted storage and missing intent rows stop execution; definite +before-mutation refusals retain evidence without stranding unrelated valid work. +Interrupted writes/readback remain unresolved across reopening. Revoke validates +all targeted native ids and complete member postimages before destructive work, +removes only confirmed creations and never deletes a preexisting empty group. +Server/configuration wiring, image-copy closure and owner storage/request +migration instructions are included. The existing endpoint's caller +authentication remains the existing boundary, not a new grant. + +The candidate was prepared against NetKingdom +`878abc93588e12b6da1bac055cfbb3abb8d7adf1`; exact source patch digest +`sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e`. +The independent Custodian review passed all 65 provisioner checks. The earlier +62-check candidate, two independent-review findings and subsequent fixes remain +retained rather than overwritten. Seven additional fixture-plan checks exercise +the preparation boundary; these counts overlap the same candidate and do not +measure task-completion percentage or independent acceptance. + +A separately reviewed bounded fixture then exercised the exact candidate against +cached pinned disposable LLDAP and Python images, through actual GraphQL. All 14 +closed compatibility/containment oracles passed: exact creation inventory, +separate-process SQLite reopening/replay/revoke, complete preexisting directory +preservation including empty/shared groups and memberships, refusal of shared +membership drift, retained uncertainty after a lost response following a real +addition, and refusal of unknown/cross-case receipt bindings. The fixture used +only newly generated synthetic users/passwords/key material in private tmpfs, +with a network-none anchor and shared loopback helper, no host ports/mounts, +no image pulls and no production or actual credential contact. The coordinator's +safe receipt reports both exact owned containers absent after cleanup; final +independent cleanup verification remains the coordinator's integration duty. + +Evidence: Custodian +`docs/changes/netkingdom-target-2026-10-16/group-provenance-r1/`, including exact +source base/pre/postimages, patch/manifest, independent review and retained +refinements. Native fixture manifest +`sha256:8a0514f5f14137355972400cd4852f48c892316d28e3abd3c86635d7cb81eef2`; +retained safe result `native-fixture/execution-result.json` (original +`/tmp/nk48-groups-native-review-5pywb19z/result.json`). The source task supplies +no native owner assent, production rollout, protected session/evidence callbacks, +ordinary type admission or consumer login proof. LLDAP has no conditional +membership mutation or generation token here: exact readback cannot exclude +ABA or a concurrent external writer, so T14 remains open. + +2026-10-17 verified source follow-through: source commit `4f65e41` passed normal +local scanning and all 65 canonical provisioner tests. The actual disposable +LLDAP receipt passed all 14 oracles; independent verification on the pinned +daemon found both exact names and the run label absent. Source integration +includes the ADR-0015 documentation correction; the journal remains local-only. + +T14 source preparation is underway. Twenty-three pinned public sources identify +NetKingdom implementation/reconciliation, Platform custody/database governance +and rapp-postgres consumer provisioning; the inspected runtime binding does not +assign identity-provisioner a package. The existing User Engine runtime token +and pod route are not a protected-case caller lane. The native proposal preserves +the same journal seam on a dedicated CNPG consumer and requires authoritative +scope binding, admitted role/lease/placement/recovery and all LDAP writers fenced. +The declared exclusive maintenance window is not mechanical exclusion of WebUI, +helper or lifecycle writers. Capacity placement and EUR cost/risk remain owner +decisions. No package, database, credential, rollout or owner assent was created. +Custodian evidence: group-provenance-r1/native-storage-proposal-r1; final source, +projection and cleanup review: resume-integration-r1/publication-verification.json. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md new file mode 100644 index 00000000..25636992 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md @@ -0,0 +1,125 @@ +# Prepared disposable PostgreSQL compatibility fixture + +Status: prepared and unit checked; not executed or approved. Default +`python3 driver.py --plan` reads only the manifest, without loading containment, +generating material or calling Docker. Execution requires independent review of +the exact manifest/source closure and explicit execution flag/digest. The flag +and fixture files confer no authority. + +The fixture reuses the existing `isolated-keycape.py` command/environment guard, +containment inspector and exact name/label cleanup. A network-none PostgreSQL +anchor and a pinned cached User Engine Python helper share only loopback. Both +are nonroot/read-only/capability-free, with private bounded tmpfs, no host ports, +mounts or Docker socket, and no pulls/builds/packages. Generated admin/runtime +passwords enter through stdin only. Image-specific nonroot UID999/Python/psycopg +compatibility is an explicit precondition checked by execution; no cache or +runtime claim is made during preparation. Any incompatible image/anonymous +volume/error fails closed and still invokes exact cleanup. + +Six sequential Python phases use the actual candidate SQL backend and its +normal provisioner path with the **existing synthetic LDAP Directory**. Bootstrap +runs the real migration on generated state, checks runtime DDL denial and exact +created-only inventories. A separate process reopens and replays/revokes while +preserving the complete synthetic prestate. Concurrent reservations must yield +one original receipt; stale row CAS cannot advance its head. Two actual database +clones then pass the same old checkpoint check concurrently and commit different +receipts at the same next counter. Only one external digest CAS may win; a fresh +backend on the losing clone must refuse even at that same counter. Lost checkpoint +acknowledgment, lost synthetic directory response and a lost reply after actual +SQL commit must retain pending/uncertain state and refuse continuation. An actual +pg_dump taken after bootstrap is then pg_restored after later journal changes; +the external test checkpoint survives. Runtime SQL SELECT must work and show an +older restored head before the candidate's restore-quarantine refusal oracle can +pass. This prevents an unrelated connection/ACL failure from masquerading as +restore protection. + +All fresh names/labels are registered before create. `finally` uses the existing +cleanup helper for both resources, requires successful exact absence inventories +and retains any Docker uncertainty as failed. PID1 lifetimes and RPC/execution +limits are bounded in the manifest. This is not a parent-death/reaper proof. +Only safe booleans, public image/source/version pins and cleanup duties leave in +a private `/tmp/nk48-postgres-native-review-*/result.json`; synthetic backup bytes +are bounded in host memory and never written as an archive file. + +The test checkpoint implementation belongs only to this fixture. Its independent +file survives PostgreSQL restore and uses file locking/fsync; it is not offered +as native custody, case authority, a journal-recovery API or an evidence broker. +The fixture explicitly disposes one definitely pre-LDAP synthetic reservation to +move between fault cases; it does not clear an unknown native mutation. All +source/fixture passes remain distinct from live CNPG/lease/TLS/backup continuity, +protected owner callbacks, actual authoritative tenant scope, native LDAP fencing +and production acceptance. + + +## R2 cached helper selection + +The reviewed R1 preflight failed before `execute`, session allocation or private +import because its pinned helper image 1e6547 was absent. No pull or override was +used. Frozen manifest/driver/probe/source and the coordinator's structured failure +are retained under `revisions/r1`. There is no raw stderr file; the original +traceback was returned in tool output only, so no byte-exact stderr artifact is +invented. + +The proposed R2 helper is the coordinator-observed cached f239 image, bound by +existing published receipts to User Engine source 1bb7cacc. That source uses +system pip on official Python 3.12, not a virtualenv. R2 therefore binds every +helper launch/exec to `/usr/local/bin/python3`; it has no PATH or alternate-venv +fallback. A closed in-container probe must verify the exact executable and import +psycopg 3 before candidate source import, reporting the bundled version. Missing +or mismatched interpreter/dependency refuses and preserves exact cleanup duties. +No container count, limit, privilege, network or cleanup expansion is proposed. +Historical producer or rollout acceptance is not a new image/dependency grant. +See `helper-selection-r2.json`, `manifest-delta-r2.json` and `preparation-r2.json`. + + +## R3 bounded failure diagnostics + +R2 executed and failed after containment and interpreter verification (bundled +psycopg 3.3.6); all phase checks were empty. Its unchanged receipt and manifest +are preserved in `revisions/r2`. The old driver discarded a failed helper result, +so the exact failing subphase is unknown. Bootstrap is not asserted as the cause. + +R3 adds fixed public driver/RPC markers and helper NDJSON markers, retained before +an oracle raises. PostgreSQL initdb now completes synchronously with only a fixed +boolean result before the detached server launch. Bootstrap separately identifies +connection readiness, migration, epoch insertion, role/grants, checkpoint and +ensure/readback. Failures expose only an allowlisted exception class and optional +five-character SQLSTATE, including a wrapped cause; never exception messages, +SQL/DSN/password values, command arguments or database logs. Public frames are +bounded to 32768 bytes and 96 lines; RPC trace is bounded to 192 fixed rows. The +safe result is checkpointed before/after each RPC and on helper failure. This +remains a sequential fixture, with unchanged images, authority, backend source, +oracles, container/network limits and exact cleanup requirements. No retry or +runtime execution is authorized by this diagnostic revision. + + +## R4 demonstrated receipt-reference fix + +R3 reached bootstrap ensure and failed before readback, with fixed RuntimeError +class and no SQLSTATE. Its safe receipt and full source/fixture are retained. +Local tests demonstrated the candidate's save method detached the provisioner's +nested group-state reference while acknowledging a journal version. R4 changes +only that method to update the acknowledged PostgreSQL version in place after +commit/checkpoint success. The original failing regression and complete real +provisioner ensure path pass locally with the fix. No diagnostic/fixture driver, +phase oracle, SQL privilege, resource, image, network or cleanup change is made. +The runtime failure is not claimed to be fully resolved until reviewed execution. + + +## R5 faithful synthetic directory snapshots + +R4 passed all five real PostgreSQL bootstrap checks, then failed during restart +without a fine-grained marker. Its safe receipt and fixture/source are frozen. +Local execution of the existing Directory/provisioner proved two fixture snapshot +errors: returning the mutable group mapping let ensure contaminate the prestate +before JSON serialization; and the test double retains empty membership backing +sets for successfully deleted groups even though `_directory` no longer exposes +their IDs. R5 copies the group map and snapshots full memberships for every +current group ID. It explicitly refuses any nonempty orphan, rather than ignoring +unknown membership. Two local source-backed tests reproduce the old failure, +prove exact detached prestate after real ensure/revoke, and reject changed shared +memberships or nonempty orphans. The semantic complete visible-prestate equality +and all phase oracle names remain intact. Restart now has fixed load/reopen, +replay/revoke/readback/prestate/foreign-context markers. Backend source5549, image +pins, resources and exact cleanup remain unchanged; actual compatibility awaits +independent exact review and execution. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py new file mode 100644 index 00000000..312edc00 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py @@ -0,0 +1,88 @@ +"""Closed public fixture diagnostics. Never serialize exception text or SQL values.""" +import json +import re + +DRIVER_STAGES = ('session_registered', 'anchor_launch', 'anchor_containment', 'postgres_private_import', + 'postgres_initdb', 'postgres_launch', 'anchor_identity', 'helper_launch', 'full_containment', + 'helper_interpreter', 'candidate_import', 'bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', + 'restored', 'database_dump', 'database_restore', 'cleanup', 'complete') +PROBE_STAGES = ('probe_entry', 'loopback_check', 'phase_dispatch', 'bootstrap_connect_wait', + 'bootstrap_connected', 'bootstrap_schema', 'bootstrap_migration', 'bootstrap_epoch_insert', + 'bootstrap_role_create', 'bootstrap_runtime_grants', 'bootstrap_checkpoint_init', + 'bootstrap_directory_init', 'bootstrap_ensure', 'bootstrap_ensure_readback', + 'bootstrap_runtime_ddl_refusal', 'bootstrap_complete', 'restart_load_snapshot', + 'restart_reopen_store', 'restart_replay_ensure', 'restart_revoke', 'restart_revoke_readback', + 'restart_prestate_equality', 'restart_foreign_context', 'restart_complete', 'phase_complete') +ERROR_CLASSES = ('GroupProvenanceUnavailable', 'RuntimeError', 'ValueError', 'TypeError', 'KeyError', + 'FileNotFoundError', 'PermissionError', 'OperationalError', 'ProgrammingError', 'IntegrityError', + 'InsufficientPrivilege', 'InvalidPassword', 'UndefinedTable', 'UndefinedColumn', 'SyntaxError', + 'DuplicateTable', 'DuplicateObject', 'InvalidSchemaName', 'DeadlockDetected', 'ConnectionException', + 'InvalidAuthorizationSpecification', 'OSError', 'TimeoutExpired', 'ImportError', + 'ModuleNotFoundError', 'JSONDecodeError', 'OtherError') + + +def error_metadata(error, stage): + if stage not in DRIVER_STAGES + PROBE_STAGES: + stage = 'phase_dispatch' + name = type(error).__name__ + name = name if name in ERROR_CLASSES else 'OtherError' + state = None + current = error + for _ in range(8): + if current is None: + break + try: + candidate = getattr(current, 'sqlstate', None) + if isinstance(candidate, str) and re.fullmatch('[0-9A-Z]{5}', candidate): + state = candidate + break + current = current.__cause__ or current.__context__ + except Exception: + break + return {'stage': stage, 'exception_class': name, 'sqlstate': state} + + +def emit_stage(stage): + if stage not in PROBE_STAGES: + raise RuntimeError('invalid_public_fixture_stage') + print(json.dumps({'event': 'stage', 'stage': stage}), flush=True) + + +def parse_frames(raw, *, require_final=True): + if isinstance(raw, bytes): + raw = raw.decode('utf-8') + if not isinstance(raw, str) or len(raw) > 32768: + raise RuntimeError('invalid_public_fixture_diagnostics') + stages, final = [], None + lines = raw.splitlines() + if len(lines) > 96: + raise RuntimeError('invalid_public_fixture_diagnostics') + for line in lines: + value = json.loads(line) + if not isinstance(value, dict) or final is not None: + raise RuntimeError('invalid_public_fixture_diagnostics') + if value.get('event') == 'stage': + if set(value) != {'event', 'stage'} or value['stage'] not in PROBE_STAGES: + raise RuntimeError('invalid_public_fixture_diagnostics') + stages.append(value['stage']) + elif value.get('status') in {'passed', 'failed'} and final is None: + if value['status'] == 'failed': + if set(value) != {'status', 'failure', 'diagnostic'} or value['failure'] != 'synthetic_pg_oracle_failed': + raise RuntimeError('invalid_public_fixture_diagnostics') + diagnostic = value['diagnostic'] + if set(diagnostic) != {'stage', 'exception_class', 'sqlstate'} or diagnostic['stage'] not in PROBE_STAGES or diagnostic['exception_class'] not in ERROR_CLASSES: + raise RuntimeError('invalid_public_fixture_diagnostics') + if diagnostic['sqlstate'] is not None and not re.fullmatch('[0-9A-Z]{5}', str(diagnostic['sqlstate'])): + raise RuntimeError('invalid_public_fixture_diagnostics') + elif (set(value) != {'status', 'checks', 'psycopg_version', 'active_database'} + or not isinstance(value['checks'], dict) + or not isinstance(value['psycopg_version'], str) + or not re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', value['psycopg_version']) + or value['active_database'] not in {'postgres', 'fixture_fork'}): + raise RuntimeError('invalid_public_fixture_diagnostics') + final = value + else: + raise RuntimeError('invalid_public_fixture_diagnostics') + if require_final and final is None: + raise RuntimeError('missing_public_fixture_result') + return stages, final diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py new file mode 100644 index 00000000..23467e01 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""Default is a read-only plan. Docker execution awaits independent exact review.""" +import argparse +import base64 +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import secrets +import tarfile +import tempfile +import time +import uuid + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE / 'manifest.json' + +# Fixed local diagnostic parser; no dependency or credential discovery. +_spec = importlib.util.spec_from_file_location('fixture_diagnostic_protocol', HERE / 'diagnostic_protocol.py') +diagnostics = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(diagnostics) + + +def require(condition): + if not condition: + raise RuntimeError('bounded_synthetic_fixture_refused') + + +def reviewed_manifest(expected): + raw = MANIFEST.read_bytes() + require(expected == hashlib.sha256(raw).hexdigest()) + manifest = json.loads(raw) + for path, digest in manifest['source_sha256'].items(): + require(bool(re.fullmatch(r'sha256:[0-9a-f]{64}', digest))) + require('sha256:' + hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest) + return manifest + + +def load_containment(manifest): + spec = importlib.util.spec_from_file_location('reviewed_existing_containment', manifest['containment_helper']) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def session_plan(manifest, run_id): + require(bool(re.fullmatch(r'[a-f0-9]{32}', run_id))) + anchor = 'nk49-kc-' + run_id + '-journal-postgres' + probe = 'nk49-kc-' + run_id + '-journal-probe' + return {'run_id': run_id, 'containers': [anchor, probe], 'worker': anchor, 'issuer': probe, + 'images': {anchor: manifest['postgres_image'], probe: manifest['python_image']}, 'volumes': {}} + + +def base_args(session, name, network, user="1000:1000"): + uid, gid = user.split(":") + return ['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', network, + '--label', 'net-kingdom.recovery-run=' + session['run_id'], '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', user, '--memory', '384m', '--cpus', '0.5', + '--pids-limit', '64', '--log-driver', 'none', '--restart', 'no', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=' + uid + ',gid=' + gid + ',mode=0700'] + + +def tar_bytes(files): + out = io.BytesIO() + with tarfile.open(fileobj=out, mode='w') as archive: + for name, value in files.items(): + require(bool(re.fullmatch(r'[a-zA-Z0-9._/-]+', name)) and not name.startswith('/') and '..' not in Path(name).parts) + entry = tarfile.TarInfo(name) + entry.size = len(value) + entry.mode = 0o600 + entry.uid = entry.gid = 1000 + archive.addfile(entry, io.BytesIO(value)) + return out.getvalue() + + +IMPORT = r''' +import base64,json,os,sys +from pathlib import Path +os.umask(0o077) +root=Path('/fixture') +(root/'source').mkdir(mode=0o700) +(root/'source/migrations').mkdir(mode=0o700) +(root/'source/tests').mkdir(mode=0o700) +for name,encoded in json.load(sys.stdin).items(): + if name not in ('source/provisioner.py','source/group_provenance.py','source/group_provenance_postgres.py','source/migrations/group_journal_postgres_v1.sql','source/tests/test_service_groups.py','probe.py','private.json','diagnostic_protocol.py'):raise ValueError() + fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as output:output.write(base64.b64decode(encoded,validate=True)) +''' + + +def helper_python(manifest): + # Exact producer source installs into official Python's system prefix. + # Never fall back to PATH or another interpreter/virtual environment. + require(manifest.get('helper_interpreter') == '/usr/local/bin/python3') + return manifest['helper_interpreter'] + + +INTERPRETER_PROBE = r""" +import json,sys +try: + import psycopg + valid=sys.executable=='/usr/local/bin/python3' and psycopg.__version__.split('.')[0]=='3' + print(json.dumps({'helper_ready':valid,'executable':sys.executable,'psycopg_version':psycopg.__version__})) +except Exception: + print(json.dumps({'helper_ready':False})) +""" + + +def preflight(adapter, manifest): + helper_python(manifest) + adapter.docker_environment() # Refuse ambient alternate routing before calls. + contexts = json.loads(adapter.command(['docker', 'context', 'inspect'], timeout=15)) + require(len(contexts) == 1 and contexts[0]['Endpoints']['docker']['Host'] == manifest['fixed_endpoint']) + facts = json.loads(adapter.command(['docker', 'info', '--format', + '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'], timeout=15)) + require(facts == {'ID': manifest['engine_id'], 'Name': manifest['engine_name'], 'OSType': 'linux'}) + for image in (manifest['postgres_image'], manifest['python_image']): + value = adapter.command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image], timeout=15).decode().strip() + require(bool(re.fullmatch(r'sha256:[a-f0-9]{64}', value))) + + +def retain_helper_result(result, phase, raw, publish): + stages, value = diagnostics.parse_frames(raw) + safe = {'status': value['status'], 'stages': stages} + if value['status'] == 'failed': + safe['diagnostic'] = value['diagnostic'] + result.setdefault('helper_diagnostics', {})[phase] = safe + publish() # Persist failure evidence before the phase oracle can raise. + return value + + +def execute(manifest, adapter, result, checkpoint=None): + session = session_plan(manifest, uuid.uuid4().hex) + result['session'] = {'run_id': session['run_id'], 'containers': session['containers'], + 'label': 'net-kingdom.recovery-run=' + session['run_id']} + python = helper_python(manifest) + deadline = time.monotonic() + 210 + original_command = adapter.command + def publish(): + if checkpoint is not None: + try: checkpoint() + except OSError: result['receipt_checkpoint_failed'] = True + def mark(stage): + require(stage in diagnostics.DRIVER_STAGES) + result['driver_stage'] = stage + publish() + def event(argv): + require(len(result.setdefault('rpc_trace', [])) < 192) + operation = argv[1] if len(argv) > 1 and argv[1] in {'run','exec','rm','container','context','image','info','volume'} else 'other' + row = {'stage': result['driver_stage'], 'operation': operation, 'status': 'started'} + result['rpc_trace'].append(row) + publish() + return row + def run(argv, **kwargs): + row = event(argv) + remaining = deadline - time.monotonic() + require(remaining > 0) + try: + value = original_command(argv, timeout=min(50, remaining), **kwargs) + row['status'] = 'completed' + publish() + return value + except BaseException as error: + row['status'] = 'failed' + result['daemon_request_uncertain'] = True + result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage'])) + # Timeout output can contain complete public progress frames. Never + # retain raw stdout/stderr or command/exception text. + if result['driver_stage'] in manifest['phase_oracles']: + partial = getattr(error, 'stdout', None) + if partial: + try: + stages, _ = diagnostics.parse_frames(partial, require_final=False) + result.setdefault('helper_diagnostics', {})[result['driver_stage']] = {'status': 'interrupted', 'stages': stages} + except Exception: + pass + publish() + raise + mark('session_registered') + adapter.command = run + try: + anchor = session['worker'] + args = base_args(session, anchor, 'none', user='999:999') + [ + '--tmpfs', '/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700', + '--tmpfs', '/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--env', 'PGDATA=/var/lib/postgresql/data', '--entrypoint', '/bin/sh', + manifest['postgres_image'], '-c', 'sleep 240'] + mark('anchor_launch') + run(args) + mark('anchor_containment') + adapter.inspect_session({**session, 'containers': [anchor], 'issuer': anchor}, worker_only=True) + admin_password, runtime_password = secrets.token_hex(24), secrets.token_hex(24) + mark('postgres_private_import') + run(['docker', 'exec', '-i', anchor, 'tar', '-xf', '-', '-C', '/run/fixture'], + input=tar_bytes({'password': admin_password.encode()})) + mark('postgres_initdb') + initialized = json.loads(run(['docker', 'exec', anchor, '/bin/sh', '-c', + "if initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1; then echo '{\"postgres_initdb\":true}'; else echo '{\"postgres_initdb\":false}'; fi"], max_output=256)) + result['postgres_initdb_complete'] = initialized == {'postgres_initdb': True} + publish() + require(result['postgres_initdb_complete']) + mark('postgres_launch') + run(['docker', 'exec', '--detach', anchor, '/bin/sh', '-c', + 'exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1']) + mark('anchor_identity') + anchor_id = run(['docker', 'container', 'inspect', anchor, '--format', '{{.Id}}']).decode().strip() + require(bool(re.fullmatch(r'[a-f0-9]{64}', anchor_id))) + args = base_args(session, session['issuer'], 'container:' + anchor_id) + [ + '--tmpfs', '/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700', + '--entrypoint', python, manifest['python_image'], '-c', 'import time;time.sleep(240)'] + mark('helper_launch') + run(args) + mark('full_containment') + adapter.inspect_session(session, worker_only=True) + result['containment_verified_before_generated_private_import'] = True + mark('helper_interpreter') + interpreter = json.loads(run(['docker', 'exec', session['issuer'], python, '-c', INTERPRETER_PROBE], max_output=4096)) + require(interpreter.get('helper_ready') is True and interpreter.get('executable') == python) + require(isinstance(interpreter.get('psycopg_version'), str) and re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', interpreter['psycopg_version'])) + result['helper_interpreter_verified_before_candidate_import'] = True + result['helper_psycopg_version'] = interpreter['psycopg_version'] + files = {'source/' + name: (HERE.parent / 'postimage/identity-provisioner' / name).read_bytes() + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', + 'migrations/group_journal_postgres_v1.sql')} + files['source/tests/test_service_groups.py'] = (HERE / 'test_service_groups.py').read_bytes() + files.update({'probe.py': (HERE / 'probe.py').read_bytes(), 'diagnostic_protocol.py': (HERE / 'diagnostic_protocol.py').read_bytes(), 'private.json': json.dumps({ + 'admin_password': admin_password, 'runtime_password': runtime_password}).encode()}) + mark('candidate_import') + run(['docker', 'exec', '-i', session['issuer'], python, '-c', IMPORT], + input=json.dumps({k: base64.b64encode(v).decode() for k, v in files.items()}).encode()) + result['checks'] = {} + backup = None + active_database = 'postgres' + for phase in ('bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', 'restored'): + if phase == 'restored': + require(backup is not None) + mark('database_restore') + run(['docker', 'exec', '-i', anchor, 'pg_restore', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', active_database, '--clean', '--if-exists', '--single-transaction', '--exit-on-error', '--no-owner'], input=backup) + mark(phase) + raw = run(['docker', 'exec', session['issuer'], python, '/fixture/probe.py', phase], max_output=32768) + value = retain_helper_result(result, phase, raw, publish) + expected = set(manifest['phase_oracles'][phase]) + require(value.get('status') == 'passed' and set(value.get('checks', {})) == expected) + require(all(v is True for v in value['checks'].values())) + result['checks'].update(value['checks']) + result['psycopg_version'] = value['psycopg_version'] + require(value.get('active_database') in {'postgres', 'fixture_fork'}) + active_database = value['active_database'] + if phase == 'bootstrap': + mark('database_dump') + backup = run(['docker', 'exec', anchor, 'pg_dump', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', 'postgres', '--format=custom', '--no-owner'], max_output=4194304) + result['status'] = 'synthetic_postgres_journal_compatibility_passed' + mark('complete') + except BaseException as error: + result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage'])) + publish() + raise + finally: + mark('cleanup') + def cleanup_command(argv, **kwargs): + row = event(argv) + try: + value = original_command(argv, timeout=15, **kwargs) + row['status'] = 'completed' + publish() + return value + except BaseException: + result['daemon_request_uncertain'] = True + row['status'] = 'failed' + publish() + raise + adapter.command = cleanup_command + try: + adapter.cleanup(session, result) + result['container_cleanup_verified'] = True + except BaseException: + result['status'] = 'failed' + result['container_cleanup_verified'] = False + adapter.command = original_command + publish() + if result.get('daemon_request_uncertain') or result.get('receipt_checkpoint_failed'): + result['status'] = 'failed' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--plan', action='store_true') + parser.add_argument('--execute-reviewed-fixture', action='store_true') + parser.add_argument('--expected-manifest-sha256') + args = parser.parse_args() + if not args.execute_reviewed_fixture: + print(MANIFEST.read_text()) + return 0 + require(not args.plan) + manifest = reviewed_manifest(args.expected_manifest_sha256) + adapter = load_containment(manifest) + preflight(adapter, manifest) + os.umask(0o077) + output = Path(tempfile.mkdtemp(prefix='nk48-postgres-native-review-', dir='/tmp')) / 'result.json' + result = {'schema': 'nk48.synthetic-postgres-journal-result.v1', 'status': 'failed', + 'manifest_sha256': args.expected_manifest_sha256, 'production_contacted': False, + 'actual_credentials_used': False, 'native_acceptance_claimed': False} + try: + execute(manifest, adapter, result, checkpoint=lambda: output.write_text(json.dumps(result, indent=2) + '\n')) + except BaseException: + result['status'] = 'failed' + finally: + output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({'status': result['status'], 'safe_result': str(output)})) + return int(result['status'] != 'synthetic_postgres_journal_compatibility_passed') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/execution-result.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/execution-result.json new file mode 100644 index 00000000..1068a7e3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/execution-result.json @@ -0,0 +1,300 @@ +{ + "schema": "nk48.synthetic-postgres-journal-result.v1", + "status": "synthetic_postgres_journal_compatibility_passed", + "manifest_sha256": "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051", + "production_contacted": false, + "actual_credentials_used": false, + "native_acceptance_claimed": false, + "session": { + "run_id": "492a7afaec6b43ef92b98eb76025362f", + "containers": [ + "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-postgres", + "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-probe" + ], + "label": "net-kingdom.recovery-run=492a7afaec6b43ef92b98eb76025362f" + }, + "driver_stage": "cleanup", + "rpc_trace": [ + { + "stage": "anchor_launch", + "operation": "run", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "context", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "info", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "postgres_private_import", + "operation": "exec", + "status": "completed" + }, + { + "stage": "postgres_initdb", + "operation": "exec", + "status": "completed" + }, + { + "stage": "postgres_launch", + "operation": "exec", + "status": "completed" + }, + { + "stage": "anchor_identity", + "operation": "container", + "status": "completed" + }, + { + "stage": "helper_launch", + "operation": "run", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "context", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "info", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "helper_interpreter", + "operation": "exec", + "status": "completed" + }, + { + "stage": "candidate_import", + "operation": "exec", + "status": "completed" + }, + { + "stage": "bootstrap", + "operation": "exec", + "status": "completed" + }, + { + "stage": "database_dump", + "operation": "exec", + "status": "completed" + }, + { + "stage": "restart", + "operation": "exec", + "status": "completed" + }, + { + "stage": "concurrency", + "operation": "exec", + "status": "completed" + }, + { + "stage": "fork", + "operation": "exec", + "status": "completed" + }, + { + "stage": "lost-ack", + "operation": "exec", + "status": "completed" + }, + { + "stage": "database_restore", + "operation": "exec", + "status": "completed" + }, + { + "stage": "restored", + "operation": "exec", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "rm", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "rm", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + } + ], + "postgres_initdb_complete": true, + "containment_verified_before_generated_private_import": true, + "helper_interpreter_verified_before_candidate_import": true, + "helper_psycopg_version": "3.3.6", + "checks": { + "only_loopback_no_external_routes": true, + "actual_postgresql_journal_ensure": true, + "explicit_scope_context_retained": true, + "confirmed_inventory_excludes_preexisting": true, + "runtime_ddl_denied": true, + "separate_process_postgres_restart_and_revoke": true, + "preexisting_shared_state_preserved": true, + "foreign_context_receipt_refused": true, + "concurrent_original_reservation_exactly_once": true, + "stale_version_update_refused_without_head_advance": true, + "concurrent_divergent_clones_same_counter_verified": true, + "independent_exact_transition_cas_selects_one_winner": true, + "reopened_loser_clone_refused_at_same_counter": true, + "lost_checkpoint_ack_retains_pending_reservation": true, + "reopened_uncertainty_refuses_ldap_effects": true, + "active_fence_survives_process_reopen": true, + "lost_synthetic_directory_response_retains_postgres_intent": true, + "lost_sql_commit_reply_retains_committed_head": true, + "reopened_stale_checkpoint_refuses_continuation": true, + "actual_pg_restore_behind_checkpoint_detected": true, + "restored_store_cannot_gain_replay_authority": true + }, + "helper_diagnostics": { + "bootstrap": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "bootstrap_connect_wait", + "bootstrap_connected", + "bootstrap_schema", + "bootstrap_migration", + "bootstrap_epoch_insert", + "bootstrap_role_create", + "bootstrap_runtime_grants", + "bootstrap_checkpoint_init", + "bootstrap_directory_init", + "bootstrap_ensure", + "bootstrap_ensure_readback", + "bootstrap_runtime_ddl_refusal", + "bootstrap_complete", + "phase_complete" + ] + }, + "restart": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "restart_load_snapshot", + "restart_reopen_store", + "restart_replay_ensure", + "restart_revoke", + "restart_revoke_readback", + "restart_prestate_equality", + "restart_foreign_context", + "restart_complete", + "phase_complete" + ] + }, + "concurrency": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "phase_complete" + ] + }, + "fork": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "phase_complete" + ] + }, + "lost-ack": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "phase_complete" + ] + }, + "restored": { + "status": "passed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "phase_complete" + ] + } + }, + "psycopg_version": "3.3.6", + "container_cleanup_obligations": [ + { + "container": "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-probe", + "status": "absence_verified" + }, + { + "container": "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-postgres", + "status": "absence_verified" + } + ], + "container_cleanup_verified": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json new file mode 100644 index 00000000..608d1952 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json @@ -0,0 +1,32 @@ +{ + "status": "fixture_helper_successor_proposed_not_executed", + "selected_cached_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "producer_revision": "1bb7cacc98479f56f6b1c28c000479efd260a944", + "producer_containerfile_sha256": "668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a", + "producer_containerfile_public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/1bb7cacc98479f56f6b1c28c000479efd260a944/Containerfile", + "provenance_sources": [ + { + "repo": "rapp-user-engine", + "path": "docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json", + "revision": "a42c68f8701880bfeb93e28dd812fcf3691568a3", + "sha256": "2f84959e36d442860fb2b5a5817bb707c4f8372797a3ef8dd0d5cab956af86ce", + "public_source": "https://forgejo.coulomb.social/coulomb/rapp-user-engine/src/commit/a42c68f8701880bfeb93e28dd812fcf3691568a3/docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json" + }, + { + "repo": "user-engine", + "path": "docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json", + "revision": "9557d6f867fe03c546aa26655f84b4192109981d", + "sha256": "964cc3755e96330bf6d1ada63410f8adfdb3c45602e0446369d6dddace412405", + "public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/9557d6f867fe03c546aa26655f84b4192109981d/docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json" + } + ], + "selection_reason": "coordinator reports exact image cached; published source receipt binds image to producer; prior owner fixture used exact image for psycopg dependencies", + "interpreter": "/usr/local/bin/python3", + "interpreter_binding": "producer officialPython3.12+systempip runtime, no virtualenv; execution must verify exact sys.executable/psycopg3 before candidate import, no PATHfallback", + "not_claimed": [ + "worker did not query Docker or create/run containers", + "bundled psycopg version not observed yet; must be reported by exact helper", + "historical image risk/production acceptance not transferred", + "cached helper is not a future identity-provisioner image/dependency admission" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r2.json new file mode 100644 index 00000000..bafc66af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r2.json @@ -0,0 +1,28 @@ +{ + "from_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "to_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "source_patch_unchanged": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "helper_image": { + "before": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "after": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b" + }, + "helper_interpreter": { + "before": "PATH python3", + "after": "/usr/local/bin/python3", + "verification": "closed exact sys.executable/psycopg3 probe before candidate source import" + }, + "unchanged_contracts": { + "postgres_image": true, + "fixed_endpoint": true, + "engine_id": true, + "engine_name": true, + "limits": true, + "resources": true, + "phase_oracles": true, + "cleanup": true, + "network": true, + "pull_policy": true + }, + "worker_docker_calls": false, + "execution_requires_new_independent_review": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r3.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r3.json new file mode 100644 index 00000000..e56aad97 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r3.json @@ -0,0 +1,29 @@ +{ + "schema": "nk48.fixture-manifest-delta.v1", + "previous_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "successor_manifest_sha256": "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093", + "changed_top_level_fields": [ + "diagnostics", + "r2_outcome", + "revision", + "source_sha256", + "supersedes_manifest_sha256" + ], + "unchanged_contract_fields": [ + "candidate_patch_sha256", + "containment_helper", + "fixed_endpoint", + "engine_id", + "engine_name", + "postgres_image", + "python_image", + "helper_interpreter", + "limits", + "resources", + "phase_oracles", + "cleanup", + "network", + "pull_policy" + ], + "runtime_executed": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r4.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r4.json new file mode 100644 index 00000000..a9d46df1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r4.json @@ -0,0 +1,31 @@ +{ + "schema": "nk48.fixture-manifest-delta.v1", + "previous_manifest_sha256": "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093", + "successor_manifest_sha256": "d862780e2954d7103a32294ff67e1bfe5e70b5f33bf66ffe73b8521a76bb362d", + "changed_top_level_fields": [ + "candidate_patch_sha256", + "r3_outcome", + "r4_refinement", + "revision", + "source_sha256", + "supersedes_manifest_sha256" + ], + "unchanged_contract_fields": [ + "containment_helper", + "fixed_endpoint", + "engine_id", + "engine_name", + "postgres_image", + "python_image", + "helper_interpreter", + "limits", + "resources", + "phase_oracles", + "cleanup", + "network", + "pull_policy", + "diagnostics" + ], + "driver_probe_unchanged": true, + "runtime_executed": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r5.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r5.json new file mode 100644 index 00000000..74e760ee --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest-delta-r5.json @@ -0,0 +1,30 @@ +{ + "previous_manifest_sha256": "d862780e2954d7103a32294ff67e1bfe5e70b5f33bf66ffe73b8521a76bb362d", + "successor_manifest_sha256": "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051", + "changed_top_level_fields": [ + "r4_outcome", + "r5_refinement", + "revision", + "source_sha256", + "supersedes_manifest_sha256" + ], + "unchanged_contract_fields": [ + "candidate_patch_sha256", + "containment_helper", + "fixed_endpoint", + "engine_id", + "engine_name", + "postgres_image", + "python_image", + "helper_interpreter", + "limits", + "resources", + "phase_oracles", + "cleanup", + "network", + "pull_policy" + ], + "source_backend_unchanged": true, + "driver_unchanged": true, + "runtime_executed": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest.json new file mode 100644 index 00000000..dd9588b8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/manifest.json @@ -0,0 +1,146 @@ +{ + "schema": "nk48.postgres-journal-fixture-proposal.v1", + "status": "prepared_not_executed_not_approved", + "source_sha256": { + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py": "sha256:04237939df1887df8a201181d87ebfacfa25855401b64f4998a3921111bd91eb", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py": "sha256:370f569d75d657e030bf27f8b127b9f5e3ca49c4efa91e8d53a02ade35bdff78", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py": "sha256:ab735b251ca70c709035b62d501dd46f842792fcb2c3b69888ae2aad698cff6e", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/source-manifest.json": "sha256:89456d4c18510153a1aae61bcbc1163f4d2f28df6de29d8801ba49881a84f515", + "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/provisioner.py": "sha256:19d1b837cbafeeacb1aa815856cd68091e6702281d72b1d860230fa320e1725b", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance.py": "sha256:405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance_postgres.py": "sha256:2a0111e83ad7b611076a1b45634e7f698b3d2e9acc473320b4a8ba009ebd0932", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/migrations/group_journal_postgres_v1.sql": "sha256:3cdf82194d6f1e2dd865b0c7e7b8a14447fda8c49151148134a0d40a80fb46ac", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md": "sha256:e50dcc7002cc21c8f7f971878df4afc0c3516d281fe0ecb9a14600aacac56fb4", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/README.md": "sha256:cf31dd40ece9991c66fe8192e88db6e7714d260eac0b94403849b9825d763c7c", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json": "sha256:ec5464c0d5310028ab91073f985183af01da2d194358fad412d197dbf90106d9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt": "sha256:668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py": "sha256:99330dddba4ba801675a65eff3c43e402fe34b791b2aa8fed64c75fc43fd0b9d", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_snapshot.py": "sha256:67241095e55b857f081432517f35902cbb9e96204cc7fa1002142acd4361f37c", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r4/probe.py": "sha256:39eea88a246a799945da72d022200dd9e15662b765bbc896a4edbdc2f8bfd5a0" + }, + "candidate_patch_sha256": "5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d", + "containment_helper": "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py", + "fixed_endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "postgres_image": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "python_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "image_pin_origin": "PostgreSQL pin unchanged; cached f239 helper selected from coordinator inventory and tied to UserEngine producer1bb7cacc by existing published receipt/previous owner PostgreSQL fixture; fixture-only selection, no future image admission", + "pull_policy": "never", + "network": "network-none", + "scope": "real disposable PostgreSQL backend SQL/migration/concurrency/restore compatibility; LDAP is explicitly the existing synthetic test Directory, not actual native LDAP in this fixture", + "limits": { + "max_service_containers": 2, + "host_ports": 0, + "host_mounts": 0, + "named_or_anonymous_volumes": 0, + "memory_bytes_per_container": 402653184, + "cpus_per_container": 0.5, + "pids_per_container": 64, + "postgres_tmpfs_bytes": 153092096, + "helper_tmpfs_bytes": 33554432, + "backup_in_host_memory_max_bytes": 4194304, + "container_lifetime_seconds": 240, + "execution_deadline_seconds": 210, + "docker_rpc_timeout_seconds": 50, + "preflight_rpc_timeout_seconds": 15, + "cleanup_rpc_timeout_seconds": 15, + "cleanup_total_bound_seconds": 120, + "new_cloud_spend_eur": 0 + }, + "compatibility_preconditions": [ + "exact cached image/engine pins reverified by admitted preflight, never pulled", + "PostgreSQL image supports nonroot UID999 with initdb/postgres/pg_dump/pg_restore in PATH; failed compatibility refuses/cleans", + "User Engine f239 producer source uses official Python system prefix; exact /usr/local/bin/python3 and bundled psycopg3 must pass closed probe before candidate import, no PATH/venv fallback or downloads", + "all image-declared volumes must be masked by private tmpfs; existing containment guard refuses any anonymous volume before private import" + ], + "resources": { + "container_names": "nk49-kc--journal-{postgres,probe}", + "exact_label": "net-kingdom.recovery-run=", + "private_state": "generated admin/runtime passwords, source/migration, synthetic directory, external test checkpoint within private container tmpfs", + "safe_result": "private /tmp/nk48-postgres-native-review-/result.json" + }, + "phase_oracles": { + "bootstrap": [ + "only_loopback_no_external_routes", + "actual_postgresql_journal_ensure", + "explicit_scope_context_retained", + "confirmed_inventory_excludes_preexisting", + "runtime_ddl_denied" + ], + "restart": [ + "only_loopback_no_external_routes", + "separate_process_postgres_restart_and_revoke", + "preexisting_shared_state_preserved", + "foreign_context_receipt_refused" + ], + "concurrency": [ + "only_loopback_no_external_routes", + "concurrent_original_reservation_exactly_once", + "stale_version_update_refused_without_head_advance" + ], + "lost-ack": [ + "only_loopback_no_external_routes", + "lost_checkpoint_ack_retains_pending_reservation", + "reopened_uncertainty_refuses_ldap_effects", + "active_fence_survives_process_reopen", + "lost_synthetic_directory_response_retains_postgres_intent", + "lost_sql_commit_reply_retains_committed_head", + "reopened_stale_checkpoint_refuses_continuation" + ], + "restored": [ + "only_loopback_no_external_routes", + "actual_pg_restore_behind_checkpoint_detected", + "restored_store_cannot_gain_replay_authority" + ], + "fork": [ + "only_loopback_no_external_routes", + "concurrent_divergent_clones_same_counter_verified", + "independent_exact_transition_cas_selects_one_winner", + "reopened_loser_clone_refused_at_same_counter" + ] + }, + "restore": "actual generated pg_dump after bootstrap; winner database advances through reopen/concurrency/fork/uncertainty; actual pg_restore of original archive while independent content checkpoint survives; runtime SELECT must work and show older restored head before refusal can pass", + "cleanup": "finally reuses exact name/label cleanup for both preregistered containers, including failed/lost-create replies; successful explicit absence required; any Docker uncertainty remains failed; no prune/global removal", + "not_claimed": [ + "CNPG HA, native runtime storage/backup/restore admission", + "authoritative tenant/case/PDP/evidence/session owner callbacks", + "external LDAP writer fencing/CAS/ABA safety", + "production endpoint activation, credentials or owner assent", + "KeyCape hard-parent-loss reaper proof" + ], + "forbidden": [ + "Docker commands before exact independent review", + "pull/build/push, host networking/ports/mounts, production APIs", + "real database/LDAP, identity archives, credentials from environment/custody", + "clearing production restore quarantine or manufacturing scope/authority" + ], + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "" + ], + "fork": "actual PostgreSQL CREATE DATABASE TEMPLATE clone; both stores pass exact same old external marker check under test barrier, commit different bodies at same counter, race external strict CAS; exactly one wins and fresh loser store refuses; no directory effect in this direct reservation case", + "revision": "r5-detached-current-directory-snapshot", + "helper_interpreter": "/usr/local/bin/python3", + "supersedes_manifest_sha256": "d862780e2954d7103a32294ff67e1bfe5e70b5f33bf66ffe73b8521a76bb362d", + "r1_preflight": "failed missing helper before execute/session/private import; preserved structured failure, no raw stderr file exists", + "r2_outcome": "failed after containment/interpreter verification; checks empty, exact helper subphase unavailable; byte-exact safe receipt retained revisions/r2/execution-result.json; cleanup absence verified", + "diagnostics": { + "driver_stage": "fixed closed vocabulary before each RPC/bootstep, checkpointed result", + "probe_stage": "fixed closed vocabulary NDJSON before bootstrap substeps, failed final retained before oracle refusal", + "exceptions": "allowlisted class and optional five-character SQLSTATE only, never values/logs/exception messages", + "max_probe_output_bytes": 32768, + "max_probe_frames": 96, + "max_rpc_trace_rows": 192, + "initdb": "synchronous fixed boolean completion before detached server launch" + }, + "r3_outcome": "failed safely at bootstrap_ensure RuntimeError/noSQLSTATE before readback; earlier boot/migration/grants complete; source nested reference replacement defect independently reproduced locally; frozen source and safe receipt retained", + "r4_refinement": "Postgres.save updates only acknowledged backend version, preserving caller nested references; two local regressions; source82 tests pass; phase oracles unchanged", + "r4_outcome": "all five bootstrap oracles passed actual SQL; restart RuntimeError before fine-grained markers; safe receipt/full fixture/source retained revisions/r4 and review-r4", + "r5_refinement": "snapshot copies mutable group mapping, checks all current IDs and full memberships, refuses nonempty orphan backing sets; two local actual Directory/provisioner tests demonstrate old defects and preservation; fixed restart markers; oracle names unchanged" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r2.json new file mode 100644 index 00000000..1bea487e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r2.json @@ -0,0 +1,13 @@ +{ + "status": "prepared_review_required_not_executed", + "fixture_plan_command": "python3 -m pytest -q docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py", + "fixture_plan_result": "9 passed in0.05s", + "source_backend_unchanged": true, + "source_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "r2_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "r1_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "worker_docker_executed": false, + "root_reported_r1_preflight": "missing helper, failed before execute/session/private import; structured original retained, rawstderrfile absent", + "native_success_claimed": false, + "producer_history_is_not_future_image_admission": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r3.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r3.json new file mode 100644 index 00000000..94190acd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r3.json @@ -0,0 +1,30 @@ +{ + "schema": "nk48.fixture-preparation.v1", + "revision": "r3-bounded-stage-diagnostics", + "status": "prepared_not_executed_not_approved", + "manifest_sha256": "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093", + "candidate_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "unit_check": { + "command": "python3 -m unittest discover -s docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture -p test_driver.py", + "tests": 13, + "passed": true, + "seconds": 0.015 + }, + "previous_failure": { + "safe_receipt": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/execution-result.json", + "sha256": "fb134a5f0444f7aabb6ea8f48f7e9bcdeb028bf42faecd477d0eeee882f14d3c", + "exact_helper_failure_subphase": "unknown_not_retrospectively_assigned", + "helper_psycopg_version": "3.3.6", + "cleanup_absence_verified": true + }, + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093" + ], + "source_backend_changed": false, + "docker_commands_executed_in_preparation": false, + "independent_review_required": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r4.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r4.json new file mode 100644 index 00000000..859243e1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r4.json @@ -0,0 +1,17 @@ +{ + "status": "prepared_not_executed_not_approved", + "manifest_sha256": "d862780e2954d7103a32294ff67e1bfe5e70b5f33bf66ffe73b8521a76bb362d", + "candidate_patch_sha256": "5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d", + "source_tests": "82 passed in 8.46s", + "patch_apply_check": true, + "regression_before_fix": "FAIL test_save_preserves_nested_receipt_references_held_by_provisioner; nested groups identity changed", + "fix": "acknowledged version only after transaction+independent checkpoint, preserve nested references", + "runtime_compatibility": "pending exact independent review/execution", + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "d862780e2954d7103a32294ff67e1bfe5e70b5f33bf66ffe73b8521a76bb362d" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r5.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r5.json new file mode 100644 index 00000000..cbd07930 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/preparation-r5.json @@ -0,0 +1,20 @@ +{ + "status": "prepared_not_executed_not_approved", + "manifest_sha256": "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051", + "candidate_patch_sha256": "5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d", + "local_snapshot_tests": "2 passed in0.236s", + "snapshot_test_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_snapshot.py" + ], + "source_backend_unchanged": true, + "frozen_r4_receipt_sha256": "540a4080f57514fde4e07c2fab888456e4d6fbe7b22d016fe4bcce7bbc35e715", + "runtime_compatibility": "bootstrap5passed; remaining phases pending exact successor review/execution", + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py new file mode 100644 index 00000000..54ebaae2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py @@ -0,0 +1,335 @@ +"""Synthetic PostgreSQL journal compatibility; LDAP remains a declared test double.""" +import concurrent.futures +from dataclasses import replace +import fcntl +import json +import os +from pathlib import Path +import sys +import time +import threading +import uuid + +ROOT = Path('/fixture') +sys.path[:0] = [str(ROOT / 'source'), str(ROOT / 'source/tests')] +from diagnostic_protocol import emit_stage, error_metadata +CURRENT_STAGE = 'probe_entry' + +def stage(value): + global CURRENT_STAGE + CURRENT_STAGE = value + emit_stage(value) + +stage('probe_entry') +try: + import psycopg + from group_provenance import GroupProvenanceUnavailable + from group_provenance_postgres import GroupJournalBinding, PostgresGroupProvenance, psycopg_connector, journal_genesis + from test_service_groups import Directory, req + + EPOCH = 'bfbb8939-e88c-4f9c-a5c6-14ba3e399ddf' + CONTEXT = GroupJournalBinding('tenant:synthetic:one', 'synthetic-pg-directory', 'sha256:' + '1' * 64, + 'sha256:' + '2' * 64, 'synthetic-principal', EPOCH) +except Exception as error: + print(json.dumps({'status': 'failed', 'failure': 'synthetic_pg_oracle_failed', + 'diagnostic': error_metadata(error, CURRENT_STAGE)}), flush=True) + raise SystemExit(0) + + +def require(value): + if not value: + raise RuntimeError('postgres_fixture_oracle_failed') + + +def info(admin=False, database=None): + values = json.loads((ROOT / 'private.json').read_text()) + database = database or ((ROOT / 'active-database').read_text() if (ROOT / 'active-database').exists() else 'postgres') + require(database in {'postgres', 'fixture_fork'}) + return {'host': '127.0.0.1', 'port': 5432, 'dbname': database, 'user': 'fixture_admin' if admin else 'fixture_app', + 'password': values['admin_password'] if admin else values['runtime_password'], 'sslmode': 'disable'} + + +class FileCheckpoint: + """Fixture-only independently retained checkpoint; never a native owner grant.""" + def __init__(self): + self.path = ROOT / 'checkpoint.json' + def _apply(self, epoch, directory, previous, previous_digest, current=None, current_digest=None): + with (ROOT / 'checkpoint.lock').open('a+') as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + value = json.loads(self.path.read_text()) + require(value == {'epoch': epoch, 'directory': directory, 'head': previous, 'digest': previous_digest}) + if current is not None: + temporary = ROOT / ('checkpoint-' + uuid.uuid4().hex) + with temporary.open('x') as out: + json.dump({'epoch': epoch, 'directory': directory, 'head': current, 'digest': current_digest}, out) + out.flush(); os.fsync(out.fileno()) + os.replace(temporary, self.path) + fd = os.open(ROOT, os.O_RDONLY | os.O_DIRECTORY) + try: os.fsync(fd) + finally: os.close(fd) + return True + def check(self, epoch, directory, head, digest): + return self._apply(epoch, directory, head, digest) + def advance(self, epoch, directory, previous, previous_digest, current, current_digest): + return self._apply(epoch, directory, previous, previous_digest, current, current_digest) + + +def store(context=CONTEXT, checkpoint=None, database=None): + return PostgresGroupProvenance(connect=psycopg_connector(connection_info=lambda: info(database=database)), binding=context, + continuity=checkpoint or FileCheckpoint()) + + +def refused(call): + try: call() + except (ValueError, RuntimeError): return + raise RuntimeError('postgres_fixture_expected_refusal_missing') + + +def snapshot(directory): + # Directory._gql(deleteGroup) removes the visible group but retains its + # empty backing set. Reject any nonempty orphan; compare every current + # group and its complete membership. Copy mappings so later mutations + # cannot change the recorded prestate in memory before serialization. + current = set(directory.groups.values()) + require(all(not users for gid, users in directory.members.items() if gid not in current)) + return {'users': sorted(directory.users), 'groups': dict(directory.groups), + 'members': {str(gid): sorted(directory.members[gid]) for gid in current}} + + +def restore_directory(value): + directory = Directory() + directory.users = set(value['users']) + directory.groups = value['groups'] + directory.members = {int(k): set(v) for k, v in value['members'].items()} + directory.group_provenance = store() + return directory + + +def bootstrap(): + stage('bootstrap_connect_wait') + last_connection_error = None + for _ in range(80): + try: + db = psycopg.connect(**info(admin=True), connect_timeout=1) + db.close(); break + except Exception as error: + last_connection_error = error + time.sleep(.25) + else: raise RuntimeError('synthetic_postgres_not_ready') from last_connection_error + stage('bootstrap_connected') + from psycopg import sql + with psycopg.connect(**info(admin=True)) as db: + stage('bootstrap_schema') + db.execute('CREATE SCHEMA identity_provisioner') + stage('bootstrap_migration') + db.execute((ROOT / 'source/migrations/group_journal_postgres_v1.sql').read_text()) + stage('bootstrap_epoch_insert') + db.execute('INSERT INTO identity_provisioner.group_journal_meta VALUES (true, %s, %s, 0, %s, false)', (EPOCH, CONTEXT.directory, journal_genesis(EPOCH, CONTEXT.directory))) + stage('bootstrap_role_create') + db.execute(sql.SQL('CREATE ROLE fixture_app LOGIN PASSWORD {}').format(sql.Literal(info()['password']))) + stage('bootstrap_runtime_grants') + db.execute('GRANT USAGE ON SCHEMA identity_provisioner TO fixture_app') + db.execute('GRANT SELECT, INSERT, UPDATE ON identity_provisioner.group_receipts TO fixture_app') + db.execute('GRANT SELECT ON identity_provisioner.group_journal_meta TO fixture_app') + db.execute('GRANT UPDATE (head,head_digest) ON identity_provisioner.group_journal_meta TO fixture_app') + stage('bootstrap_checkpoint_init') + (ROOT / 'checkpoint.json').write_text(json.dumps({'epoch': EPOCH, 'directory': CONTEXT.directory, 'head': 0, 'digest': journal_genesis(EPOCH, CONTEXT.directory)})) + stage('bootstrap_directory_init') + directory = Directory() + directory.members[3].add('tegwick') + before = snapshot(directory) + directory.group_provenance = store() + stage('bootstrap_ensure') + result = directory.ensure_service_groups(req()) + stage('bootstrap_ensure_readback') + require(result.status == 'groups_ensured') + require({x['name'] for x in result.created_groups} == {'forge-coulomb-owners', 'forge-coulomb-readers'}) + require({(x['name'], x['username']) for x in result.created_memberships} == {('forge-coulomb-owners', 'tegwick')}) + (ROOT / 'directory.json').write_text(json.dumps({'before': before, 'after': snapshot(directory)})) + stage('bootstrap_runtime_ddl_refusal') + with psycopg.connect(**info()) as db: + try: db.execute('CREATE TABLE identity_provisioner.runtime_ddl_must_fail (id integer)') + except psycopg.errors.InsufficientPrivilege: pass + else: raise RuntimeError('runtime_ddl_not_denied') + stage('bootstrap_complete') + return {'actual_postgresql_journal_ensure': True, 'explicit_scope_context_retained': True, + 'confirmed_inventory_excludes_preexisting': True, 'runtime_ddl_denied': True} + + +def restart(): + stage('restart_load_snapshot') + saved = json.loads((ROOT / 'directory.json').read_text()) + stage('restart_reopen_store') + directory = restore_directory(saved['after']) + stage('restart_replay_ensure') + require(directory.ensure_service_groups(req()).resumed) + require(not directory.calls) + stage('restart_revoke') + result = directory.revoke_service_groups(req(ensure_idempotency_key=req()['idempotency_key'], + ensure_correlation_id=req()['correlation_id'], delete_if_empty=True)) + stage('restart_revoke_readback') + require(result.status == 'groups_revoked_and_deleted') + stage('restart_prestate_equality') + require(snapshot(directory) == saved['before']) + stage('restart_foreign_context') + foreign = store(replace(CONTEXT, principal='other-synthetic-principal')) + refused(lambda: foreign.get(req()['idempotency_key'], directory._group_binding(req()['groups'], req()['members'], req()['correlation_id']))) + stage('restart_complete') + return {'separate_process_postgres_restart_and_revoke': True, 'preexisting_shared_state_preserved': True, + 'foreign_context_receipt_refused': True} + + +def concurrency(): + binding = '4' * 64 + def reserve(_): + try: return store().reserve('synthetic-concurrent-reservation', binding) + except GroupProvenanceUnavailable: return None + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: + results = list(pool.map(reserve, range(24))) + created = [r[0] for r in results if r is not None and r[1]] + require(len(created) == 1) + receipt = created[0] + receipt['state'] = 'refused' + store().save(receipt) + stale = json.loads(json.dumps(receipt)) + store().save(receipt) + head = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: store().save(stale)) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == head) + with psycopg.connect(**info()) as db: + require(db.execute('SELECT count(*) FROM identity_provisioner.group_receipts WHERE key=%s', ('synthetic-concurrent-reservation',)).fetchone()[0] == 1) + return {'concurrent_original_reservation_exactly_once': True, 'stale_version_update_refused_without_head_advance': True} + + +def fork(): + # Clone actual PostgreSQL state at the exact same head/digest. Both callers + # must pass the old external check before either commits a divergent body. + with psycopg.connect(**{**info(admin=True), 'dbname': 'template1'}, autocommit=True) as db: + db.execute('CREATE DATABASE fixture_fork TEMPLATE postgres') + barrier = threading.Barrier(2) + class SimultaneousCheckpoint(FileCheckpoint): + def check(self, *args): + result = super().check(*args) + barrier.wait(timeout=10) + return result + def attempt(database): + candidate = store(database=database, checkpoint=SimultaneousCheckpoint()) + try: + record, fresh = candidate.reserve('synthetic-fork-' + database, '8' * 64) + require(fresh) + return database, record + except GroupProvenanceUnavailable: + return database, None + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(attempt, ('postgres', 'fixture_fork'))) + winners = [(database, record) for database, record in results if record is not None] + require(len(winners) == 1) + winner, record = winners[0] + loser = next(database for database, result in results if result is None) + markers = {} + for database in ('postgres', 'fixture_fork'): + with psycopg.connect(**info(admin=True, database=database)) as db: + markers[database] = db.execute('SELECT head,head_digest FROM identity_provisioner.group_journal_meta').fetchone() + require(markers[winner][0] == markers[loser][0] and markers[winner][1] != markers[loser][1]) + store(database=winner).check() + refused(lambda: store(database=loser).check()) + retained = json.loads((ROOT / 'checkpoint.json').read_text()) + require((retained['head'], retained['digest']) == markers[winner]) + # No directory call was attempted by this direct reservation fixture. Retain + # terminal refusal for that key before continuing against the winner only. + record['state'] = 'refused' + store(database=winner).save(record) + (ROOT / 'active-database').write_text(winner) + return {'concurrent_divergent_clones_same_counter_verified': True, + 'independent_exact_transition_cas_selects_one_winner': True, + 'reopened_loser_clone_refused_at_same_counter': True} + + +def restored(): + # Actual pg_restore has rolled SQL back while this checkpoint survived. + with psycopg.connect(**info()) as db: + epoch, directory, head, quarantine = db.execute('SELECT epoch::text,directory,head,quarantined FROM identity_provisioner.group_journal_meta').fetchone() + require(epoch == EPOCH and directory == CONTEXT.directory and quarantine is False) + require(head < json.loads((ROOT / 'checkpoint.json').read_text())['head']) + refused(lambda: store().check()) + refused(lambda: store().reserve('no-replay-after-restore', '6' * 64)) + return {'actual_pg_restore_behind_checkpoint_detected': True, 'restored_store_cannot_gain_replay_authority': True} + + +def lost_ack(): + class LoseAcknowledgment(FileCheckpoint): + def advance(self, *args): + super().advance(*args) + raise RuntimeError('synthetic-lost-ack-after-durable-checkpoint') + directory = Directory() + directory.group_provenance = store(checkpoint=LoseAcknowledgment()) + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + receipt = store().get('synthetic-lost-sql-ack-key', directory._group_binding(req()['groups'], req()['members'], req()['correlation_id'])) + require(receipt['state'] == 'pending') + refused(lambda: store().reserve('unrelated-after-unresolved', '7' * 64)) + # Definite no-directory-call refusal is disposed only inside this generated + # fixture, before moving to the next synthetic fault. No native recovery API. + receipt['state'] = 'refused' + store().save(receipt) + directory = Directory() + directory.group_provenance = store() + original = directory._gql + def lose_directory_response(token, query, variables): + result = original(token, query, variables) + if 'addUserToGroup' in query: + raise RuntimeError('synthetic-directory-response-lost') + return result + directory._gql = lose_directory_response + payload = req(idempotency_key='synthetic-lost-directory-response') + refused(lambda: directory.ensure_service_groups(payload)) + checked = snapshot(directory) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(payload)) + require(snapshot(directory) == checked) + record = store().get(payload['idempotency_key'], directory._group_binding(payload['groups'], payload['members'], payload['correlation_id'])) + require(record['state'] == 'uncertain' and record['intent']['operation'] == 'add_member') + class LostCommitReply: + def __init__(self): self.db = psycopg.connect(**info()) + def __enter__(self): self.db.__enter__(); return self + def __exit__(self, typ, value, traceback): + result = self.db.__exit__(typ, value, traceback) + if typ is None: raise OSError('synthetic-reply-lost-after-actual-sql-commit') + return result + def cursor(self): return self.db.cursor() + def close(self): self.db.close() + broken = PostgresGroupProvenance(connect=LostCommitReply, binding=CONTEXT, continuity=FileCheckpoint()) + checkpoint = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: broken.save(record)) + with psycopg.connect(**info()) as db: + committed_head = db.execute('SELECT head FROM identity_provisioner.group_journal_meta').fetchone()[0] + require(committed_head == checkpoint + 1) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == checkpoint) + refused(lambda: store().check()) + return {'lost_checkpoint_ack_retains_pending_reservation': True, 'reopened_uncertainty_refuses_ldap_effects': True, + 'active_fence_survives_process_reopen': True, 'lost_synthetic_directory_response_retains_postgres_intent': True, + 'lost_sql_commit_reply_retains_committed_head': True, 'reopened_stale_checkpoint_refuses_continuation': True} + + +def main(): + try: + stage('loopback_check') + require({p.name for p in Path('/sys/class/net').iterdir()} == {'lo'}) + require(len(Path('/proc/net/route').read_text().splitlines()) == 1) + checks = {'only_loopback_no_external_routes': True} + stage('phase_dispatch') + checks.update({'bootstrap': bootstrap, 'restart': restart, 'concurrency': concurrency, + 'fork': fork, 'restored': restored, 'lost-ack': lost_ack}[sys.argv[1]]()) + stage('phase_complete') + print(json.dumps({'status': 'passed', 'checks': checks, 'psycopg_version': psycopg.__version__, 'active_database': info()['dbname']})) + except Exception as error: + print(json.dumps({'status': 'failed', 'failure': 'synthetic_pg_oracle_failed', + 'diagnostic': error_metadata(error, CURRENT_STAGE)}), flush=True) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt new file mode 100644 index 00000000..fe9b998b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt @@ -0,0 +1,12 @@ +FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 + +RUN useradd --create-home --uid 10001 user-engine +WORKDIR /app +COPY pyproject.toml README.md /app/ +COPY src /app/src +RUN pip install --no-cache-dir ".[runtime]" + +USER 10001:10001 +EXPOSE 8080 +ENV PYTHONUNBUFFERED=1 +CMD ["python", "-m", "user_engine.runtime"] diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/README.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/README.md new file mode 100644 index 00000000..1947016f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/README.md @@ -0,0 +1,51 @@ +# Prepared disposable PostgreSQL compatibility fixture + +Status: prepared and unit checked; not executed or approved. Default +`python3 driver.py --plan` reads only the manifest, without loading containment, +generating material or calling Docker. Execution requires independent review of +the exact manifest/source closure and explicit execution flag/digest. The flag +and fixture files confer no authority. + +The fixture reuses the existing `isolated-keycape.py` command/environment guard, +containment inspector and exact name/label cleanup. A network-none PostgreSQL +anchor and a pinned cached User Engine Python helper share only loopback. Both +are nonroot/read-only/capability-free, with private bounded tmpfs, no host ports, +mounts or Docker socket, and no pulls/builds/packages. Generated admin/runtime +passwords enter through stdin only. Image-specific nonroot UID999/Python/psycopg +compatibility is an explicit precondition checked by execution; no cache or +runtime claim is made during preparation. Any incompatible image/anonymous +volume/error fails closed and still invokes exact cleanup. + +Six sequential Python phases use the actual candidate SQL backend and its +normal provisioner path with the **existing synthetic LDAP Directory**. Bootstrap +runs the real migration on generated state, checks runtime DDL denial and exact +created-only inventories. A separate process reopens and replays/revokes while +preserving the complete synthetic prestate. Concurrent reservations must yield +one original receipt; stale row CAS cannot advance its head. Two actual database +clones then pass the same old checkpoint check concurrently and commit different +receipts at the same next counter. Only one external digest CAS may win; a fresh +backend on the losing clone must refuse even at that same counter. Lost checkpoint +acknowledgment, lost synthetic directory response and a lost reply after actual +SQL commit must retain pending/uncertain state and refuse continuation. An actual +pg_dump taken after bootstrap is then pg_restored after later journal changes; +the external test checkpoint survives. Runtime SQL SELECT must work and show an +older restored head before the candidate's restore-quarantine refusal oracle can +pass. This prevents an unrelated connection/ACL failure from masquerading as +restore protection. + +All fresh names/labels are registered before create. `finally` uses the existing +cleanup helper for both resources, requires successful exact absence inventories +and retains any Docker uncertainty as failed. PID1 lifetimes and RPC/execution +limits are bounded in the manifest. This is not a parent-death/reaper proof. +Only safe booleans, public image/source/version pins and cleanup duties leave in +a private `/tmp/nk48-postgres-native-review-*/result.json`; synthetic backup bytes +are bounded in host memory and never written as an archive file. + +The test checkpoint implementation belongs only to this fixture. Its independent +file survives PostgreSQL restore and uses file locking/fsync; it is not offered +as native custody, case authority, a journal-recovery API or an evidence broker. +The fixture explicitly disposes one definitely pre-LDAP synthetic reservation to +move between fault cases; it does not clear an unknown native mutation. All +source/fixture passes remain distinct from live CNPG/lease/TLS/backup continuity, +protected owner callbacks, actual authoritative tenant scope, native LDAP fencing +and production acceptance. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/coordinator-preflight-report.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/coordinator-preflight-report.json new file mode 100644 index 00000000..3d566c0d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/coordinator-preflight-report.json @@ -0,0 +1,20 @@ +{ + "source": "coordinator message; raw command output not supplied to worker", + "reviewed_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "independent_source_tests": "80 passed in8.56s", + "independent_fixture_plan_tests": "8 passed in0.03s", + "preflight_status": "failed_before_execute", + "postgres_image_cached": true, + "missing_helper_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "execute_called": false, + "resource_creation_attempted": false, + "pull_or_override_used": false, + "cached_helper_inventory_reported": [ + "sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "sha256:312dcb062b237c518148e1b4f09798a4e6ad08bc60d4add6834aab3f246f2002" + ], + "scope": "read-only preflight failure is retained, not a successful native compatibility outcome", + "structured_original_receipt": "../../../preflight-r1-failure.json", + "structured_original_sha256": "88b2f48c18429fff745f881066eac1f7729233fc59fa676dc1eee7eccbe6ace4", + "raw_stderr_file": "absent; original tool traceback was returned to coordinator and not captured as a file" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/driver.py new file mode 100644 index 00000000..d53ffe62 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/driver.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Default is a read-only plan. Docker execution awaits independent exact review.""" +import argparse +import base64 +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import secrets +import tarfile +import tempfile +import time +import uuid + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE / 'manifest.json' + + +def require(condition): + if not condition: + raise RuntimeError('bounded_synthetic_fixture_refused') + + +def reviewed_manifest(expected): + raw = MANIFEST.read_bytes() + require(expected == hashlib.sha256(raw).hexdigest()) + manifest = json.loads(raw) + for path, digest in manifest['source_sha256'].items(): + require(bool(re.fullmatch(r'sha256:[0-9a-f]{64}', digest))) + require('sha256:' + hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest) + return manifest + + +def load_containment(manifest): + spec = importlib.util.spec_from_file_location('reviewed_existing_containment', manifest['containment_helper']) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def session_plan(manifest, run_id): + require(bool(re.fullmatch(r'[a-f0-9]{32}', run_id))) + anchor = 'nk49-kc-' + run_id + '-journal-postgres' + probe = 'nk49-kc-' + run_id + '-journal-probe' + return {'run_id': run_id, 'containers': [anchor, probe], 'worker': anchor, 'issuer': probe, + 'images': {anchor: manifest['postgres_image'], probe: manifest['python_image']}, 'volumes': {}} + + +def base_args(session, name, network, user="1000:1000"): + uid, gid = user.split(":") + return ['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', network, + '--label', 'net-kingdom.recovery-run=' + session['run_id'], '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', user, '--memory', '384m', '--cpus', '0.5', + '--pids-limit', '64', '--log-driver', 'none', '--restart', 'no', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=' + uid + ',gid=' + gid + ',mode=0700'] + + +def tar_bytes(files): + out = io.BytesIO() + with tarfile.open(fileobj=out, mode='w') as archive: + for name, value in files.items(): + require(bool(re.fullmatch(r'[a-zA-Z0-9._/-]+', name)) and not name.startswith('/') and '..' not in Path(name).parts) + entry = tarfile.TarInfo(name) + entry.size = len(value) + entry.mode = 0o600 + entry.uid = entry.gid = 1000 + archive.addfile(entry, io.BytesIO(value)) + return out.getvalue() + + +IMPORT = r''' +import base64,json,os,sys +from pathlib import Path +os.umask(0o077) +root=Path('/fixture') +(root/'source').mkdir(mode=0o700) +(root/'source/migrations').mkdir(mode=0o700) +(root/'source/tests').mkdir(mode=0o700) +for name,encoded in json.load(sys.stdin).items(): + if name not in ('source/provisioner.py','source/group_provenance.py','source/group_provenance_postgres.py','source/migrations/group_journal_postgres_v1.sql','source/tests/test_service_groups.py','probe.py','private.json'):raise ValueError() + fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as output:output.write(base64.b64decode(encoded,validate=True)) +''' + + +def preflight(adapter, manifest): + adapter.docker_environment() # Refuse ambient alternate routing before calls. + contexts = json.loads(adapter.command(['docker', 'context', 'inspect'], timeout=15)) + require(len(contexts) == 1 and contexts[0]['Endpoints']['docker']['Host'] == manifest['fixed_endpoint']) + facts = json.loads(adapter.command(['docker', 'info', '--format', + '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'], timeout=15)) + require(facts == {'ID': manifest['engine_id'], 'Name': manifest['engine_name'], 'OSType': 'linux'}) + for image in (manifest['postgres_image'], manifest['python_image']): + value = adapter.command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image], timeout=15).decode().strip() + require(bool(re.fullmatch(r'sha256:[a-f0-9]{64}', value))) + + +def execute(manifest, adapter, result): + session = session_plan(manifest, uuid.uuid4().hex) + result['session'] = {'run_id': session['run_id'], 'containers': session['containers'], + 'label': 'net-kingdom.recovery-run=' + session['run_id']} + deadline = time.monotonic() + 210 + original_command = adapter.command + def run(argv, **kwargs): + remaining = deadline - time.monotonic() + require(remaining > 0) + try: + return original_command(argv, timeout=min(50, remaining), **kwargs) + except BaseException: + result['daemon_request_uncertain'] = True + raise + adapter.command = run + try: + anchor = session['worker'] + args = base_args(session, anchor, 'none', user='999:999') + [ + '--tmpfs', '/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700', + '--tmpfs', '/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--env', 'PGDATA=/var/lib/postgresql/data', '--entrypoint', '/bin/sh', + manifest['postgres_image'], '-c', 'sleep 240'] + run(args) + adapter.inspect_session({**session, 'containers': [anchor], 'issuer': anchor}, worker_only=True) + admin_password, runtime_password = secrets.token_hex(24), secrets.token_hex(24) + run(['docker', 'exec', '-i', anchor, 'tar', '-xf', '-', '-C', '/run/fixture'], + input=tar_bytes({'password': admin_password.encode()})) + run(['docker', 'exec', '--detach', anchor, '/bin/sh', '-c', + 'initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1 && exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1']) + anchor_id = run(['docker', 'container', 'inspect', anchor, '--format', '{{.Id}}']).decode().strip() + require(bool(re.fullmatch(r'[a-f0-9]{64}', anchor_id))) + args = base_args(session, session['issuer'], 'container:' + anchor_id) + [ + '--tmpfs', '/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700', + '--entrypoint', 'python3', manifest['python_image'], '-c', 'import time;time.sleep(240)'] + run(args) + adapter.inspect_session(session, worker_only=True) + result['containment_verified_before_generated_private_import'] = True + files = {'source/' + name: (HERE.parent / 'postimage/identity-provisioner' / name).read_bytes() + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', + 'migrations/group_journal_postgres_v1.sql')} + files['source/tests/test_service_groups.py'] = (HERE / 'test_service_groups.py').read_bytes() + files.update({'probe.py': (HERE / 'probe.py').read_bytes(), 'private.json': json.dumps({ + 'admin_password': admin_password, 'runtime_password': runtime_password}).encode()}) + run(['docker', 'exec', '-i', session['issuer'], 'python3', '-c', IMPORT], + input=json.dumps({k: base64.b64encode(v).decode() for k, v in files.items()}).encode()) + result['checks'] = {} + backup = None + active_database = 'postgres' + for phase in ('bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', 'restored'): + if phase == 'restored': + require(backup is not None) + run(['docker', 'exec', '-i', anchor, 'pg_restore', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', active_database, '--clean', '--if-exists', '--single-transaction', '--exit-on-error', '--no-owner'], input=backup) + value = json.loads(run(['docker', 'exec', session['issuer'], 'python3', '/fixture/probe.py', phase], max_output=8192)) + expected = set(manifest['phase_oracles'][phase]) + require(value.get('status') == 'passed' and set(value.get('checks', {})) == expected) + require(all(v is True for v in value['checks'].values())) + result['checks'].update(value['checks']) + result['psycopg_version'] = value['psycopg_version'] + require(value.get('active_database') in {'postgres', 'fixture_fork'}) + active_database = value['active_database'] + if phase == 'bootstrap': + backup = run(['docker', 'exec', anchor, 'pg_dump', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', 'postgres', '--format=custom', '--no-owner'], max_output=4194304) + result['status'] = 'synthetic_postgres_journal_compatibility_passed' + finally: + def cleanup_command(argv, **kwargs): + try: return original_command(argv, timeout=15, **kwargs) + except BaseException: + result['daemon_request_uncertain'] = True + raise + adapter.command = cleanup_command + try: + adapter.cleanup(session, result) + result['container_cleanup_verified'] = True + except BaseException: + result['status'] = 'failed' + result['container_cleanup_verified'] = False + adapter.command = original_command + if result.get('daemon_request_uncertain'): + result['status'] = 'failed' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--plan', action='store_true') + parser.add_argument('--execute-reviewed-fixture', action='store_true') + parser.add_argument('--expected-manifest-sha256') + args = parser.parse_args() + if not args.execute_reviewed_fixture: + print(MANIFEST.read_text()) + return 0 + require(not args.plan) + manifest = reviewed_manifest(args.expected_manifest_sha256) + adapter = load_containment(manifest) + preflight(adapter, manifest) + os.umask(0o077) + output = Path(tempfile.mkdtemp(prefix='nk48-postgres-native-review-', dir='/tmp')) / 'result.json' + result = {'schema': 'nk48.synthetic-postgres-journal-result.v1', 'status': 'failed', + 'manifest_sha256': args.expected_manifest_sha256, 'production_contacted': False, + 'actual_credentials_used': False, 'native_acceptance_claimed': False} + try: + execute(manifest, adapter, result) + except BaseException: + result['status'] = 'failed' + finally: + output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({'status': result['status'], 'safe_result': str(output)})) + return int(result['status'] != 'synthetic_postgres_journal_compatibility_passed') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/manifest.json new file mode 100644 index 00000000..f4c7872c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/manifest.json @@ -0,0 +1,123 @@ +{ + "schema": "nk48.postgres-journal-fixture-proposal.v1", + "status": "prepared_not_executed_not_approved", + "source_sha256": { + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py": "sha256:54cc76913e7fdaa5c75cc31040aea33378792bf10393210339a5dbd5973db32c", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py": "sha256:97265a65f008e86c6f3de54eaa1a7d77f069117d4b923fb85cf835a8816adf00", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py": "sha256:28a96975546c82d8e42f302d14f29051b18ed6264839234698802ac1ef09f500", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/source-manifest.json": "sha256:0c16e19415ad93b6ca73c92a05c205d263adde8a780c8c33119cd5f9d40f4752", + "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/provisioner.py": "sha256:19d1b837cbafeeacb1aa815856cd68091e6702281d72b1d860230fa320e1725b", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance.py": "sha256:405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance_postgres.py": "sha256:55282af26f95af66ff8d444d38d0bcbcf3ee58a615adedfff8da5db2b0716889", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/migrations/group_journal_postgres_v1.sql": "sha256:3cdf82194d6f1e2dd865b0c7e7b8a14447fda8c49151148134a0d40a80fb46ac", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md": "sha256:c762987d320d745289d1a9068a2994b0c9c6d9bb53a545ad03841e434c51c040", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/README.md": "sha256:4bf4678a1f1c70fbd3d4ffa77c70821be0372f03210b2164a2895fc99ca6a6b9" + }, + "candidate_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "containment_helper": "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py", + "fixed_endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "postgres_image": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "python_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "image_pin_origin": "existing check-privacyidea-database.py and rapp-user-engine/manifests/runtime.yaml; cache presence not queried; missing image refuses without pull", + "pull_policy": "never", + "network": "network-none", + "scope": "real disposable PostgreSQL backend SQL/migration/concurrency/restore compatibility; LDAP is explicitly the existing synthetic test Directory, not actual native LDAP in this fixture", + "limits": { + "max_service_containers": 2, + "host_ports": 0, + "host_mounts": 0, + "named_or_anonymous_volumes": 0, + "memory_bytes_per_container": 402653184, + "cpus_per_container": 0.5, + "pids_per_container": 64, + "postgres_tmpfs_bytes": 153092096, + "helper_tmpfs_bytes": 33554432, + "backup_in_host_memory_max_bytes": 4194304, + "container_lifetime_seconds": 240, + "execution_deadline_seconds": 210, + "docker_rpc_timeout_seconds": 50, + "preflight_rpc_timeout_seconds": 15, + "cleanup_rpc_timeout_seconds": 15, + "cleanup_total_bound_seconds": 120, + "new_cloud_spend_eur": 0 + }, + "compatibility_preconditions": [ + "exact cached image/engine pins reverified by admitted preflight, never pulled", + "PostgreSQL image supports nonroot UID999 with initdb/postgres/pg_dump/pg_restore in PATH; failed compatibility refuses/cleans", + "User Engine pinned image includes psycopg3; exact bundled version reported; no dependency downloads/install/build in fixture", + "all image-declared volumes must be masked by private tmpfs; existing containment guard refuses any anonymous volume before private import" + ], + "resources": { + "container_names": "nk49-kc--journal-{postgres,probe}", + "exact_label": "net-kingdom.recovery-run=", + "private_state": "generated admin/runtime passwords, source/migration, synthetic directory, external test checkpoint within private container tmpfs", + "safe_result": "private /tmp/nk48-postgres-native-review-/result.json" + }, + "phase_oracles": { + "bootstrap": [ + "only_loopback_no_external_routes", + "actual_postgresql_journal_ensure", + "explicit_scope_context_retained", + "confirmed_inventory_excludes_preexisting", + "runtime_ddl_denied" + ], + "restart": [ + "only_loopback_no_external_routes", + "separate_process_postgres_restart_and_revoke", + "preexisting_shared_state_preserved", + "foreign_context_receipt_refused" + ], + "concurrency": [ + "only_loopback_no_external_routes", + "concurrent_original_reservation_exactly_once", + "stale_version_update_refused_without_head_advance" + ], + "lost-ack": [ + "only_loopback_no_external_routes", + "lost_checkpoint_ack_retains_pending_reservation", + "reopened_uncertainty_refuses_ldap_effects", + "active_fence_survives_process_reopen", + "lost_synthetic_directory_response_retains_postgres_intent", + "lost_sql_commit_reply_retains_committed_head", + "reopened_stale_checkpoint_refuses_continuation" + ], + "restored": [ + "only_loopback_no_external_routes", + "actual_pg_restore_behind_checkpoint_detected", + "restored_store_cannot_gain_replay_authority" + ], + "fork": [ + "only_loopback_no_external_routes", + "concurrent_divergent_clones_same_counter_verified", + "independent_exact_transition_cas_selects_one_winner", + "reopened_loser_clone_refused_at_same_counter" + ] + }, + "restore": "actual generated pg_dump after bootstrap; winner database advances through reopen/concurrency/fork/uncertainty; actual pg_restore of original archive while independent content checkpoint survives; runtime SELECT must work and show older restored head before refusal can pass", + "cleanup": "finally reuses exact name/label cleanup for both preregistered containers, including failed/lost-create replies; successful explicit absence required; any Docker uncertainty remains failed; no prune/global removal", + "not_claimed": [ + "CNPG HA, native runtime storage/backup/restore admission", + "authoritative tenant/case/PDP/evidence/session owner callbacks", + "external LDAP writer fencing/CAS/ABA safety", + "production endpoint activation, credentials or owner assent", + "KeyCape hard-parent-loss reaper proof" + ], + "forbidden": [ + "Docker commands before exact independent review", + "pull/build/push, host networking/ports/mounts, production APIs", + "real database/LDAP, identity archives, credentials from environment/custody", + "clearing production restore quarantine or manufacturing scope/authority" + ], + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "" + ], + "fork": "actual PostgreSQL CREATE DATABASE TEMPLATE clone; both stores pass exact same old external marker check under test barrier, commit different bodies at same counter, race external strict CAS; exactly one wins and fresh loser store refuses; no directory effect in this direct reservation case" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/preflight-r1-failure.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/preflight-r1-failure.json new file mode 100644 index 00000000..d3620b86 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/preflight-r1-failure.json @@ -0,0 +1,13 @@ +{ + "status": "preflight_refused", + "reviewed_manifest_sha256": "sha256:4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "phase": "preflight.image_inspect", + "postgres_exact_pin_cached": true, + "user_engine_exact_pin_cached": false, + "missing_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "docker_session_created": false, + "generated_private_import_started": false, + "pull_or_build_attempted": false, + "native_acceptance": false, + "evidence_basis": "Coordinator tool-returned traceback at preflight image inspect, followed by exact metadata-only image inspection; no original stderr file was captured." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/probe.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/probe.py new file mode 100644 index 00000000..2763bd48 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/probe.py @@ -0,0 +1,287 @@ +"""Synthetic PostgreSQL journal compatibility; LDAP remains a declared test double.""" +import concurrent.futures +from dataclasses import replace +import fcntl +import json +import os +from pathlib import Path +import sys +import time +import threading +import uuid + +ROOT = Path('/fixture') +sys.path[:0] = [str(ROOT / 'source'), str(ROOT / 'source/tests')] +import psycopg +from group_provenance import GroupProvenanceUnavailable +from group_provenance_postgres import GroupJournalBinding, PostgresGroupProvenance, psycopg_connector, journal_genesis +from test_service_groups import Directory, req + +EPOCH = 'bfbb8939-e88c-4f9c-a5c6-14ba3e399ddf' +CONTEXT = GroupJournalBinding('tenant:synthetic:one', 'synthetic-pg-directory', 'sha256:' + '1' * 64, + 'sha256:' + '2' * 64, 'synthetic-principal', EPOCH) + + +def require(value): + if not value: + raise RuntimeError('postgres_fixture_oracle_failed') + + +def info(admin=False, database=None): + values = json.loads((ROOT / 'private.json').read_text()) + database = database or ((ROOT / 'active-database').read_text() if (ROOT / 'active-database').exists() else 'postgres') + require(database in {'postgres', 'fixture_fork'}) + return {'host': '127.0.0.1', 'port': 5432, 'dbname': database, 'user': 'fixture_admin' if admin else 'fixture_app', + 'password': values['admin_password'] if admin else values['runtime_password'], 'sslmode': 'disable'} + + +class FileCheckpoint: + """Fixture-only independently retained checkpoint; never a native owner grant.""" + def __init__(self): + self.path = ROOT / 'checkpoint.json' + def _apply(self, epoch, directory, previous, previous_digest, current=None, current_digest=None): + with (ROOT / 'checkpoint.lock').open('a+') as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + value = json.loads(self.path.read_text()) + require(value == {'epoch': epoch, 'directory': directory, 'head': previous, 'digest': previous_digest}) + if current is not None: + temporary = ROOT / ('checkpoint-' + uuid.uuid4().hex) + with temporary.open('x') as out: + json.dump({'epoch': epoch, 'directory': directory, 'head': current, 'digest': current_digest}, out) + out.flush(); os.fsync(out.fileno()) + os.replace(temporary, self.path) + fd = os.open(ROOT, os.O_RDONLY | os.O_DIRECTORY) + try: os.fsync(fd) + finally: os.close(fd) + return True + def check(self, epoch, directory, head, digest): + return self._apply(epoch, directory, head, digest) + def advance(self, epoch, directory, previous, previous_digest, current, current_digest): + return self._apply(epoch, directory, previous, previous_digest, current, current_digest) + + +def store(context=CONTEXT, checkpoint=None, database=None): + return PostgresGroupProvenance(connect=psycopg_connector(connection_info=lambda: info(database=database)), binding=context, + continuity=checkpoint or FileCheckpoint()) + + +def refused(call): + try: call() + except (ValueError, RuntimeError): return + raise RuntimeError('postgres_fixture_expected_refusal_missing') + + +def snapshot(directory): + return {'users': sorted(directory.users), 'groups': directory.groups, + 'members': {str(k): sorted(v) for k, v in directory.members.items()}} + + +def restore_directory(value): + directory = Directory() + directory.users = set(value['users']) + directory.groups = value['groups'] + directory.members = {int(k): set(v) for k, v in value['members'].items()} + directory.group_provenance = store() + return directory + + +def bootstrap(): + for _ in range(80): + try: + db = psycopg.connect(**info(admin=True), connect_timeout=1) + db.close(); break + except Exception: time.sleep(.25) + else: raise RuntimeError('synthetic_postgres_not_ready') + from psycopg import sql + with psycopg.connect(**info(admin=True)) as db: + db.execute('CREATE SCHEMA identity_provisioner') + db.execute((ROOT / 'source/migrations/group_journal_postgres_v1.sql').read_text()) + db.execute('INSERT INTO identity_provisioner.group_journal_meta VALUES (true, %s, %s, 0, %s, false)', (EPOCH, CONTEXT.directory, journal_genesis(EPOCH, CONTEXT.directory))) + db.execute(sql.SQL('CREATE ROLE fixture_app LOGIN PASSWORD {}').format(sql.Literal(info()['password']))) + db.execute('GRANT USAGE ON SCHEMA identity_provisioner TO fixture_app') + db.execute('GRANT SELECT, INSERT, UPDATE ON identity_provisioner.group_receipts TO fixture_app') + db.execute('GRANT SELECT ON identity_provisioner.group_journal_meta TO fixture_app') + db.execute('GRANT UPDATE (head,head_digest) ON identity_provisioner.group_journal_meta TO fixture_app') + (ROOT / 'checkpoint.json').write_text(json.dumps({'epoch': EPOCH, 'directory': CONTEXT.directory, 'head': 0, 'digest': journal_genesis(EPOCH, CONTEXT.directory)})) + directory = Directory() + directory.members[3].add('tegwick') + before = snapshot(directory) + directory.group_provenance = store() + result = directory.ensure_service_groups(req()) + require(result.status == 'groups_ensured') + require({x['name'] for x in result.created_groups} == {'forge-coulomb-owners', 'forge-coulomb-readers'}) + require({(x['name'], x['username']) for x in result.created_memberships} == {('forge-coulomb-owners', 'tegwick')}) + (ROOT / 'directory.json').write_text(json.dumps({'before': before, 'after': snapshot(directory)})) + with psycopg.connect(**info()) as db: + try: db.execute('CREATE TABLE identity_provisioner.runtime_ddl_must_fail (id integer)') + except psycopg.errors.InsufficientPrivilege: pass + else: raise RuntimeError('runtime_ddl_not_denied') + return {'actual_postgresql_journal_ensure': True, 'explicit_scope_context_retained': True, + 'confirmed_inventory_excludes_preexisting': True, 'runtime_ddl_denied': True} + + +def restart(): + saved = json.loads((ROOT / 'directory.json').read_text()) + directory = restore_directory(saved['after']) + require(directory.ensure_service_groups(req()).resumed) + require(not directory.calls) + result = directory.revoke_service_groups(req(ensure_idempotency_key=req()['idempotency_key'], + ensure_correlation_id=req()['correlation_id'], delete_if_empty=True)) + require(result.status == 'groups_revoked_and_deleted') + require(snapshot(directory) == saved['before']) + foreign = store(replace(CONTEXT, principal='other-synthetic-principal')) + refused(lambda: foreign.get(req()['idempotency_key'], directory._group_binding(req()['groups'], req()['members'], req()['correlation_id']))) + return {'separate_process_postgres_restart_and_revoke': True, 'preexisting_shared_state_preserved': True, + 'foreign_context_receipt_refused': True} + + +def concurrency(): + binding = '4' * 64 + def reserve(_): + try: return store().reserve('synthetic-concurrent-reservation', binding) + except GroupProvenanceUnavailable: return None + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: + results = list(pool.map(reserve, range(24))) + created = [r[0] for r in results if r is not None and r[1]] + require(len(created) == 1) + receipt = created[0] + receipt['state'] = 'refused' + store().save(receipt) + stale = json.loads(json.dumps(receipt)) + store().save(receipt) + head = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: store().save(stale)) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == head) + with psycopg.connect(**info()) as db: + require(db.execute('SELECT count(*) FROM identity_provisioner.group_receipts WHERE key=%s', ('synthetic-concurrent-reservation',)).fetchone()[0] == 1) + return {'concurrent_original_reservation_exactly_once': True, 'stale_version_update_refused_without_head_advance': True} + + +def fork(): + # Clone actual PostgreSQL state at the exact same head/digest. Both callers + # must pass the old external check before either commits a divergent body. + with psycopg.connect(**{**info(admin=True), 'dbname': 'template1'}, autocommit=True) as db: + db.execute('CREATE DATABASE fixture_fork TEMPLATE postgres') + barrier = threading.Barrier(2) + class SimultaneousCheckpoint(FileCheckpoint): + def check(self, *args): + result = super().check(*args) + barrier.wait(timeout=10) + return result + def attempt(database): + candidate = store(database=database, checkpoint=SimultaneousCheckpoint()) + try: + record, fresh = candidate.reserve('synthetic-fork-' + database, '8' * 64) + require(fresh) + return database, record + except GroupProvenanceUnavailable: + return database, None + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(attempt, ('postgres', 'fixture_fork'))) + winners = [(database, record) for database, record in results if record is not None] + require(len(winners) == 1) + winner, record = winners[0] + loser = next(database for database, result in results if result is None) + markers = {} + for database in ('postgres', 'fixture_fork'): + with psycopg.connect(**info(admin=True, database=database)) as db: + markers[database] = db.execute('SELECT head,head_digest FROM identity_provisioner.group_journal_meta').fetchone() + require(markers[winner][0] == markers[loser][0] and markers[winner][1] != markers[loser][1]) + store(database=winner).check() + refused(lambda: store(database=loser).check()) + retained = json.loads((ROOT / 'checkpoint.json').read_text()) + require((retained['head'], retained['digest']) == markers[winner]) + # No directory call was attempted by this direct reservation fixture. Retain + # terminal refusal for that key before continuing against the winner only. + record['state'] = 'refused' + store(database=winner).save(record) + (ROOT / 'active-database').write_text(winner) + return {'concurrent_divergent_clones_same_counter_verified': True, + 'independent_exact_transition_cas_selects_one_winner': True, + 'reopened_loser_clone_refused_at_same_counter': True} + + +def restored(): + # Actual pg_restore has rolled SQL back while this checkpoint survived. + with psycopg.connect(**info()) as db: + epoch, directory, head, quarantine = db.execute('SELECT epoch::text,directory,head,quarantined FROM identity_provisioner.group_journal_meta').fetchone() + require(epoch == EPOCH and directory == CONTEXT.directory and quarantine is False) + require(head < json.loads((ROOT / 'checkpoint.json').read_text())['head']) + refused(lambda: store().check()) + refused(lambda: store().reserve('no-replay-after-restore', '6' * 64)) + return {'actual_pg_restore_behind_checkpoint_detected': True, 'restored_store_cannot_gain_replay_authority': True} + + +def lost_ack(): + class LoseAcknowledgment(FileCheckpoint): + def advance(self, *args): + super().advance(*args) + raise RuntimeError('synthetic-lost-ack-after-durable-checkpoint') + directory = Directory() + directory.group_provenance = store(checkpoint=LoseAcknowledgment()) + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + receipt = store().get('synthetic-lost-sql-ack-key', directory._group_binding(req()['groups'], req()['members'], req()['correlation_id'])) + require(receipt['state'] == 'pending') + refused(lambda: store().reserve('unrelated-after-unresolved', '7' * 64)) + # Definite no-directory-call refusal is disposed only inside this generated + # fixture, before moving to the next synthetic fault. No native recovery API. + receipt['state'] = 'refused' + store().save(receipt) + directory = Directory() + directory.group_provenance = store() + original = directory._gql + def lose_directory_response(token, query, variables): + result = original(token, query, variables) + if 'addUserToGroup' in query: + raise RuntimeError('synthetic-directory-response-lost') + return result + directory._gql = lose_directory_response + payload = req(idempotency_key='synthetic-lost-directory-response') + refused(lambda: directory.ensure_service_groups(payload)) + checked = snapshot(directory) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(payload)) + require(snapshot(directory) == checked) + record = store().get(payload['idempotency_key'], directory._group_binding(payload['groups'], payload['members'], payload['correlation_id'])) + require(record['state'] == 'uncertain' and record['intent']['operation'] == 'add_member') + class LostCommitReply: + def __init__(self): self.db = psycopg.connect(**info()) + def __enter__(self): self.db.__enter__(); return self + def __exit__(self, typ, value, traceback): + result = self.db.__exit__(typ, value, traceback) + if typ is None: raise OSError('synthetic-reply-lost-after-actual-sql-commit') + return result + def cursor(self): return self.db.cursor() + def close(self): self.db.close() + broken = PostgresGroupProvenance(connect=LostCommitReply, binding=CONTEXT, continuity=FileCheckpoint()) + checkpoint = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: broken.save(record)) + with psycopg.connect(**info()) as db: + committed_head = db.execute('SELECT head FROM identity_provisioner.group_journal_meta').fetchone()[0] + require(committed_head == checkpoint + 1) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == checkpoint) + refused(lambda: store().check()) + return {'lost_checkpoint_ack_retains_pending_reservation': True, 'reopened_uncertainty_refuses_ldap_effects': True, + 'active_fence_survives_process_reopen': True, 'lost_synthetic_directory_response_retains_postgres_intent': True, + 'lost_sql_commit_reply_retains_committed_head': True, 'reopened_stale_checkpoint_refuses_continuation': True} + + +def main(): + try: + require({p.name for p in Path('/sys/class/net').iterdir()} == {'lo'}) + require(len(Path('/proc/net/route').read_text().splitlines()) == 1) + checks = {'only_loopback_no_external_routes': True} + checks.update({'bootstrap': bootstrap, 'restart': restart, 'concurrency': concurrency, + 'fork': fork, 'restored': restored, 'lost-ack': lost_ack}[sys.argv[1]]()) + print(json.dumps({'status': 'passed', 'checks': checks, 'psycopg_version': psycopg.__version__, 'active_database': info()['dbname']})) + except Exception: + print(json.dumps({'status': 'failed', 'failure': 'synthetic_pg_oracle_failed', 'phase': sys.argv[1]})) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source-manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source-manifest.json new file mode 100644 index 00000000..3c80043f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source-manifest.json @@ -0,0 +1,65 @@ +{ + "base_revision": "6cb43c0cc55212363a5406b02db046e2384a7c40", + "base_tree": "615095596348b23ad34da79cd0e8597cdb815c5d", + "scope": "source successor with content-bound continuity; no native/canonical admission", + "files": [ + { + "path": "docs/service-group-provenance.md", + "preimage_sha256": "cd6b45640983b11e29a6bc6278bcb45de8fd8a5ddcad53eb77ab1a1f3f1f1280", + "postimage_sha256": "b287e7a85aa2146f35d2bc8473dfd05239ea0179b7b8b186d2e85bc07c7b650c", + "preimage_bytes": 7455, + "postimage_bytes": 11401 + }, + { + "path": "identity-provisioner/Containerfile", + "preimage_sha256": "c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6", + "postimage_sha256": "31e7d948d4f40efd6950764a3dbb8c410d50ed7a1c13c18e0830a1186f2abafc", + "preimage_bytes": 459, + "postimage_bytes": 651 + }, + { + "path": "identity-provisioner/group_provenance.py", + "preimage_sha256": "fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4", + "postimage_sha256": "405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10", + "preimage_bytes": 8947, + "postimage_bytes": 9409 + }, + { + "path": "identity-provisioner/group_provenance_postgres.py", + "preimage_sha256": null, + "postimage_sha256": "55282af26f95af66ff8d444d38d0bcbcf3ee58a615adedfff8da5db2b0716889", + "preimage_bytes": 0, + "postimage_bytes": 13176 + }, + { + "path": "identity-provisioner/migrations/group_journal_postgres_v1.sql", + "preimage_sha256": null, + "postimage_sha256": "3cdf82194d6f1e2dd865b0c7e7b8a14447fda8c49151148134a0d40a80fb46ac", + "preimage_bytes": 0, + "postimage_bytes": 1353 + }, + { + "path": "identity-provisioner/provisioner.py", + "preimage_sha256": "2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5", + "postimage_sha256": "19d1b837cbafeeacb1aa815856cd68091e6702281d72b1d860230fa320e1725b", + "preimage_bytes": 31721, + "postimage_bytes": 31727 + }, + { + "path": "identity-provisioner/requirements-postgres.txt", + "preimage_sha256": null, + "postimage_sha256": "14f1beae54a789d2c880654ca0ce4094c9aa54645e79314b4297c12938ba8877", + "preimage_bytes": 0, + "postimage_bytes": 450 + }, + { + "path": "identity-provisioner/tests/test_group_postgres.py", + "preimage_sha256": null, + "postimage_sha256": "6fdf86b7158ce263b141265346d2910c074a8ae5483417c961053c7792db96c0", + "preimage_bytes": 0, + "postimage_bytes": 10173 + } + ], + "patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "superseded_counter_only": "review-r0" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source.patch b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source.patch new file mode 100644 index 00000000..81698fb8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/source.patch @@ -0,0 +1,626 @@ +diff --git a/docs/service-group-provenance.md b/docs/service-group-provenance.md +--- a/docs/service-group-provenance.md ++++ b/docs/service-group-provenance.md +@@ -80,6 +80,64 @@ + and replay. No arbitrary JSON import, receipt expiry or automatic uncertainty + clearing is introduced. Owner reconciliation needs independent evidence. + ++## PostgreSQL source seam (not native admission) ++ ++`PostgresGroupProvenance` implements the same reserve/get/claim-revoke/save/check ++journal contract. It is injection-only: the ordinary HTTP server has no ++PostgreSQL environment switch and remains without a group store unless the ++existing local SQLite configuration is explicitly supplied. `GROUP_POSTGRES_DSN` ++or a caller JSON object cannot enable the PostgreSQL path. Production group ++operations remain unadmitted and disabled pending the owner callbacks and gates ++above; a backend constructor is not an access decision. ++ ++A trusted owner integration must provide an exact `GroupJournalBinding` with ++opaque authoritative tenant scope, directory instance, accepted case digest, ++source digest, represented principal and journal epoch. There is no guessed ++platform-root scope or request-derived tenant fallback. The owner also injects a ++renewable private connection-info callback using the existing `rapp-postgres` ++consumer/lease contract, and independently durable `JournalContinuity` callbacks. ++These callbacks retain/check/advance exact epoch+directory+head+commit digest outside the ++PostgreSQL state that might be restored. Neither callback authenticates a caller ++or grants a protected side effect. Their native custody, source binding, ++revocation and execution bounds still require independent admission. Only the ++disposable fixture supplies a test checkpoint implementation here. ++ ++The v1 migration under `identity-provisioner/migrations/` creates tables within ++the existing consumer schema; it creates no database/schema/roles, inserts no ++epoch and clears no quarantine. Startup does not run migrations or initialize ++missing state. The owner must separately admit an epoch/checkpoint after ++independent continuity/recovery review; absent or quarantined state refuses. ++A restored database with an older head than the independently retained checkpoint ++also refuses rather than treating missing later intents as authority to replay. ++ ++PostgreSQL transactions lock the single journal metadata row, commit bounded ++receipts/intents and advance the head and exact transition digest with an exactly-one conditional ++update. The digest binds the prior marker, canonical body, context, request key ++and receipt version; the independent checkpoint must compare/CAS both content ++and sequence, never accept a counter-only callback. Divergent restored clones ++that commit different transitions at the same counter cannot both be accepted. ++Every receipt update also requires its exact context/key/request/version and ++increments its version. The active slot spans all contexts in the journal; ++it serializes cooperating producer operations, not external LDAP writers. ++SQL commit precedes independently durable checkpoint acknowledgment. A lost ++commit/acknowledgment, stale version, missing row or continuity mismatch refuses ++and poisons that backend object; exception handling cannot automatically rewrite ++its journal into success. Known request-binding mismatches remain domain ++refusals. Definite before-mutation refusal records can release the active slot ++while the original key remains terminal. ++ ++The container source includes a hash-pinned Linux amd64 CPython3.12 psycopg3 ++closure selected from User Engine's existing lockfile. No image build or package ++installation is part of this source proof; build/publication, artifact scanning ++and native runtime/dependency admission remain owner work. SQLite is still ++local/disposable only. The separate PostgreSQL fixture proposal uses a cached ++pinned PostgreSQL image and a cached pinned User Engine Python image, real SQL, ++separate-process reopen, concurrency, stale-CAS and actual pg_dump/pg_restore ++behind an independently retained test checkpoint. That fixture needs exact ++review before Docker execution and does not prove live CNPG HA, TLS/lease ++custody, tenant authority, external writer fencing, production retention or ++protected session/evidence integration. ++ + ## Safety and remaining native acceptance + + Before each directory mutation, durable intent is committed. A successful +diff --git a/identity-provisioner/Containerfile b/identity-provisioner/Containerfile +--- a/identity-provisioner/Containerfile ++++ b/identity-provisioner/Containerfile +@@ -3,7 +3,10 @@ + RUN useradd --create-home --uid 10001 provisioner + WORKDIR /app + COPY --from=lldap /app/lldap_set_password /app/lldap_set_password +-COPY provisioner.py group_provenance.py password_setup.py server.py /app/ ++COPY requirements-postgres.txt /app/ ++RUN python -m pip install --no-cache-dir --require-hashes -r /app/requirements-postgres.txt ++COPY provisioner.py group_provenance.py group_provenance_postgres.py password_setup.py server.py /app/ ++COPY migrations/ /app/migrations/ + USER 10001:10001 + EXPOSE 8080 + CMD ["python", "server.py"] +diff --git a/identity-provisioner/group_provenance.py b/identity-provisioner/group_provenance.py +--- a/identity-provisioner/group_provenance.py ++++ b/identity-provisioner/group_provenance.py +@@ -11,8 +11,17 @@ + from pathlib import Path + import sqlite3 + import stat +-from typing import Any ++from typing import Any, Protocol + from urllib.parse import quote ++ ++ ++class GroupJournalStore(Protocol): ++ """Owner-injected persistence only; grants no caller or native authority.""" ++ def check(self) -> None: ... ++ def reserve(self, key: str, binding: str) -> tuple[dict[str, Any], bool]: ... ++ def get(self, key: str, binding: str) -> dict[str, Any]: ... ++ def claim_revoke(self, key: str, binding: str, delete_if_empty: bool) -> dict[str, Any]: ... ++ def save(self, receipt: dict[str, Any]) -> None: ... + + + class GroupProvenanceUnavailable(RuntimeError): +diff --git a/identity-provisioner/group_provenance_postgres.py b/identity-provisioner/group_provenance_postgres.py +new file mode 100644 +--- /dev/null ++++ b/identity-provisioner/group_provenance_postgres.py +@@ -0,0 +1,251 @@ ++"""Injection-only PostgreSQL implementation of the existing owner journal seam. ++ ++Binding and continuity callbacks are supplied by a trusted owner integration, ++never constructed from an HTTP payload. Neither is an authorization decision. ++No server environment switch, automatic schema creation or recovery admission. ++""" ++from __future__ import annotations ++ ++from contextlib import contextmanager ++from dataclasses import asdict, dataclass ++import hashlib ++import json ++import re ++from typing import Any, Callable, Protocol ++from uuid import UUID ++ ++from group_provenance import GroupProvenance, GroupProvenanceUnavailable ++ ++ ++@dataclass(frozen=True) ++class GroupJournalBinding: ++ tenant_scope: str ++ directory: str ++ case_digest: str ++ source_digest: str ++ principal: str ++ epoch: str ++ ++ def __post_init__(self): ++ for value in (self.tenant_scope, self.directory, self.principal): ++ if not isinstance(value, str) or not 1 <= len(value) <= 256 or any(ord(c) < 32 for c in value): ++ raise ValueError("explicit owner journal binding required") ++ for value in (self.case_digest, self.source_digest): ++ if not isinstance(value, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", value): ++ raise ValueError("exact owner source and case digests required") ++ if not isinstance(self.epoch, str) or str(UUID(self.epoch)) != self.epoch: ++ raise ValueError("explicit canonical journal epoch required") ++ ++ @property ++ def digest(self): ++ return hashlib.sha256(json.dumps(asdict(self), sort_keys=True, separators=(",", ":")).encode()).hexdigest() ++ ++ ++def journal_genesis(epoch: str, directory: str) -> str: ++ """Deterministic initial marker for explicit owner migration/admission only.""" ++ return hashlib.sha256(json.dumps({"schema": "nk-group-journal-genesis.v1", "epoch": epoch, ++ "directory": directory}, sort_keys=True, separators=(",", ":")).encode()).hexdigest() ++ ++ ++class JournalContinuity(Protocol): ++ """Existing independently retained owner checkpoint, outside restored SQL state. ++ ++ Implementations compare/advance exact epoch+directory+head+commit digest atomically and ++ durably. An in-memory implementation is a fixture only. Missing/stale or ++ ambiguous checkpoints must refuse; no callback grants mutation authority. ++ """ ++ def check(self, epoch: str, directory: str, head: int, digest: str) -> bool: ... ++ def advance(self, epoch: str, directory: str, previous: int, previous_digest: str, current: int, current_digest: str) -> bool: ... ++ ++ ++class PostgresGroupProvenance: ++ def __init__(self, *, connect: Callable, binding: GroupJournalBinding, ++ continuity: JournalContinuity, capacity: int = 256): ++ if not callable(connect) or not isinstance(binding, GroupJournalBinding) or not 1 <= capacity <= 4096: ++ raise ValueError("explicit owner connector, binding and bounded capacity required") ++ if not callable(getattr(continuity, "check", None)) or not callable(getattr(continuity, "advance", None)): ++ raise ValueError("independent owner continuity callbacks required") ++ self.connect, self.binding, self.continuity, self.capacity = connect, binding, continuity, capacity ++ self._poisoned = False ++ ++ def _continuity(self, method, *args): ++ try: ++ if getattr(self.continuity, method)(self.binding.epoch, self.binding.directory, *args) is not True: ++ raise GroupProvenanceUnavailable() ++ except Exception: ++ raise GroupProvenanceUnavailable() from None ++ ++ @contextmanager ++ def _transaction(self): ++ if self._poisoned: ++ raise GroupProvenanceUnavailable() ++ connection = None ++ tx = None ++ try: ++ connection = self.connect() ++ with connection: ++ with connection.cursor() as cursor: ++ cursor.execute("SET LOCAL statement_timeout = '5s'") ++ cursor.execute("SET LOCAL idle_in_transaction_session_timeout = '15s'") ++ cursor.execute("SELECT epoch::text, directory, head, head_digest, quarantined FROM identity_provisioner.group_journal_meta WHERE singleton = true FOR UPDATE") ++ meta = cursor.fetchone() ++ if not meta or meta[0] != self.binding.epoch or meta[1] != self.binding.directory or meta[4] is not False or type(meta[2]) is not int or meta[2] < 0 or not isinstance(meta[3], str) or not re.fullmatch(r"[0-9a-f]{64}", meta[3]): ++ raise GroupProvenanceUnavailable() ++ self._continuity("check", meta[2], meta[3]) ++ tx = {"cursor": cursor, "head": meta[2], "digest": meta[3], "dirty": False} ++ yield tx ++ if tx["dirty"]: ++ cursor.execute("UPDATE identity_provisioner.group_journal_meta SET head = head + 1, head_digest = %s WHERE singleton = true AND epoch = %s AND directory = %s AND head = %s AND head_digest = %s AND quarantined = false", ++ (tx["next_digest"], self.binding.epoch, self.binding.directory, tx["head"], tx["digest"])) ++ if cursor.rowcount != 1: ++ raise GroupProvenanceUnavailable() ++ # SQL commit precedes the independent checkpoint acknowledgment. ++ # A lost acknowledgment refuses before the next directory effect. ++ if tx and tx["dirty"]: ++ self._continuity("advance", tx["head"], tx["digest"], tx["head"] + 1, tx["next_digest"]) ++ except ValueError: ++ raise ++ except GroupProvenanceUnavailable: ++ # Caller mismatches are domain refusals. Storage/continuity failures ++ # poison this object; durable pending records are never rewritten ++ # automatically by an exception handler after ambiguous commit. ++ self._poisoned = True ++ raise ++ except Exception: ++ self._poisoned = True ++ raise GroupProvenanceUnavailable() from None ++ finally: ++ if connection is not None: ++ try: ++ connection.close() ++ except Exception: ++ self._poisoned = True ++ raise GroupProvenanceUnavailable() from None ++ ++ def check(self): ++ with self._transaction(): ++ pass ++ ++ def _read(self, cursor, key, request_binding): ++ cursor.execute("SELECT binding, version, body, context_json FROM identity_provisioner.group_receipts WHERE context_digest = %s AND key = %s", ++ (self.binding.digest, key)) ++ row = cursor.fetchone() ++ if row is None: ++ return None ++ if row[0] != request_binding: ++ raise ValueError("ensure receipt request binding mismatch") ++ if row[3] != json.dumps(asdict(self.binding), sort_keys=True): ++ raise GroupProvenanceUnavailable() ++ receipt = GroupProvenance._load(row[2], key, request_binding) ++ if type(row[1]) is not int or row[1] < 1 or type(receipt.get("_pg_version")) is not int or receipt.get("_pg_version") != row[1] or receipt.get("_pg_context") != self.binding.digest: ++ raise GroupProvenanceUnavailable() ++ return receipt ++ ++ @staticmethod ++ def _inputs(key, binding): ++ if not isinstance(key, str) or not 1 <= len(key) <= 256 or not isinstance(binding, str) or not re.fullmatch(r"[0-9a-f]{64}", binding): ++ raise ValueError("bounded exact journal request binding required") ++ ++ def _active(self, cursor): ++ cursor.execute("SELECT 1 FROM identity_provisioner.group_receipts WHERE active = true LIMIT 1") ++ if cursor.fetchone(): ++ raise GroupProvenanceUnavailable() ++ ++ def _transition(self, tx, receipt, operation): ++ # Bind the exact transition, not merely a counter. Two restored/forked ++ # copies that commit different bodies at the same next head cannot both ++ # match an independently retained checkpoint after reopening. ++ transition = {"schema": "nk-group-journal-transition.v1", "epoch": self.binding.epoch, ++ "directory": self.binding.directory, "prior_head": tx["head"], "prior_digest": tx["digest"], ++ "head": tx["head"] + 1, "operation": operation, "context": asdict(self.binding), ++ "key": receipt["key"], "request_binding": receipt["binding"], ++ "version": receipt["_pg_version"], "body": receipt} ++ tx["next_digest"] = hashlib.sha256(json.dumps(transition, sort_keys=True, separators=(",", ":")).encode()).hexdigest() ++ tx["dirty"] = True ++ ++ def reserve(self, key, binding): ++ self._inputs(key, binding) ++ with self._transaction() as tx: ++ cursor = tx["cursor"] ++ receipt = self._read(cursor, key, binding) ++ if receipt is not None: ++ return receipt, False ++ self._active(cursor) ++ cursor.execute("SELECT COUNT(*) FROM identity_provisioner.group_receipts") ++ if cursor.fetchone()[0] >= self.capacity: ++ raise GroupProvenanceUnavailable() ++ receipt = {"key": key, "binding": binding, "state": "pending", "intent": None, "groups": {}, ++ "created_groups": [], "created_memberships": [], "_pg_version": 1, "_pg_context": self.binding.digest} ++ cursor.execute("INSERT INTO identity_provisioner.group_receipts (context_digest, context_json, key, binding, version, state, active, body) VALUES (%s, %s, %s, %s, 1, 'pending', true, %s)", ++ (self.binding.digest, json.dumps(asdict(self.binding), sort_keys=True), key, binding, json.dumps(receipt, sort_keys=True))) ++ if cursor.rowcount != 1: ++ raise GroupProvenanceUnavailable() ++ self._transition(tx, receipt, "reserve") ++ return receipt, True ++ ++ def get(self, key, binding): ++ self._inputs(key, binding) ++ with self._transaction() as tx: ++ receipt = self._read(tx["cursor"], key, binding) ++ if receipt is None: ++ raise ValueError("unknown ensure receipt or request binding mismatch") ++ return receipt ++ ++ def _write(self, tx, receipt, operation="save"): ++ self._inputs(receipt.get("key"), receipt.get("binding")) ++ if receipt.get("_pg_context") != self.binding.digest or type(receipt.get("_pg_version")) is not int or receipt["_pg_version"] < 1: ++ raise GroupProvenanceUnavailable() ++ encoded = json.dumps(receipt, sort_keys=True) ++ GroupProvenance._load(encoded, receipt["key"], receipt["binding"]) ++ updated = json.loads(encoded) ++ updated["_pg_version"] += 1 ++ active = True if receipt["state"] in {"pending", "revoking", "uncertain"} else None ++ tx["cursor"].execute("UPDATE identity_provisioner.group_receipts SET state = %s, active = %s, body = %s, version = version + 1 WHERE context_digest = %s AND key = %s AND binding = %s AND version = %s", ++ (receipt["state"], active, json.dumps(updated, sort_keys=True), self.binding.digest, receipt["key"], receipt["binding"], receipt["_pg_version"])) ++ if tx["cursor"].rowcount != 1: ++ raise GroupProvenanceUnavailable() ++ self._transition(tx, updated, operation) ++ return updated ++ ++ def save(self, receipt): ++ with self._transaction() as tx: ++ updated = self._write(tx, receipt) ++ receipt.clear() ++ receipt.update(updated) ++ ++ def claim_revoke(self, key, binding, delete_if_empty): ++ self._inputs(key, binding) ++ if not isinstance(delete_if_empty, bool): ++ raise ValueError("delete_if_empty must be boolean") ++ with self._transaction() as tx: ++ receipt = self._read(tx["cursor"], key, binding) ++ if receipt is None: ++ raise ValueError("unknown ensure receipt or request binding mismatch") ++ if receipt["state"] == "revoked": ++ if receipt["delete_if_empty"] != delete_if_empty: ++ raise ValueError("completed rollback options cannot change") ++ return receipt ++ if receipt["state"] != "ensured": ++ raise GroupProvenanceUnavailable() ++ self._active(tx["cursor"]) ++ receipt.update(state="revoking", delete_if_empty=delete_if_empty) ++ updated = self._write(tx, receipt, "claim_revoke") ++ return updated ++ ++ ++def psycopg_connector(*, connection_info: Callable[[], dict[str, Any]]): ++ """Owner-provided renewable lease values stay in memory; never env/argv/logs. ++ ++ No connection happens here. Lazy import keeps disabled group endpoints free ++ of a runtime dependency. The admitted owner supplies TLS/host/db/role facts. ++ """ ++ if not callable(connection_info): ++ raise ValueError("owner connection-info callback required") ++ def connect(): ++ try: ++ import psycopg ++ values = dict(connection_info()) ++ return psycopg.connect(**values, connect_timeout=5) ++ except Exception: ++ raise GroupProvenanceUnavailable() from None ++ return connect +diff --git a/identity-provisioner/migrations/group_journal_postgres_v1.sql b/identity-provisioner/migrations/group_journal_postgres_v1.sql +new file mode 100644 +--- /dev/null ++++ b/identity-provisioner/migrations/group_journal_postgres_v1.sql +@@ -0,0 +1,23 @@ ++-- Run only by the admitted migration role in its existing owner schema. ++-- Does not create a database/schema/credential, admit an epoch or clear quarantine. ++CREATE TABLE identity_provisioner.group_journal_meta ( ++ singleton boolean PRIMARY KEY CHECK (singleton = true), ++ epoch uuid NOT NULL, ++ directory text NOT NULL CHECK (octet_length(directory) BETWEEN 1 AND 1024), ++ head bigint NOT NULL CHECK (head >= 0), ++ head_digest text NOT NULL CHECK (head_digest ~ '^[0-9a-f]{64}$'), ++ quarantined boolean NOT NULL DEFAULT true ++); ++CREATE TABLE identity_provisioner.group_receipts ( ++ context_digest text NOT NULL CHECK (context_digest ~ '^[0-9a-f]{64}$'), ++ context_json text NOT NULL CHECK (octet_length(context_json) <= 16384), ++ key text NOT NULL CHECK (char_length(key) BETWEEN 1 AND 256), ++ binding text NOT NULL CHECK (binding ~ '^[0-9a-f]{64}$'), ++ version bigint NOT NULL CHECK (version >= 1), ++ state text NOT NULL CHECK (state IN ('pending','ensured','revoking','revoked','uncertain','refused')), ++ active boolean UNIQUE CHECK (active IS NULL OR active = true), ++ body text NOT NULL CHECK (octet_length(body) <= 1048576), ++ PRIMARY KEY (context_digest, key) ++); ++-- Epoch insertion, initial continuity checkpoint and admission are separate ++-- owner actions; this migration leaves all operations refused without them. +diff --git a/identity-provisioner/provisioner.py b/identity-provisioner/provisioner.py +--- a/identity-provisioner/provisioner.py ++++ b/identity-provisioner/provisioner.py +@@ -11,7 +11,7 @@ + from urllib.error import HTTPError, URLError + from urllib.request import Request, urlopen + +-from group_provenance import GroupProvenance, GroupProvenanceUnavailable ++from group_provenance import GroupJournalStore, GroupProvenanceUnavailable + + + @dataclass(frozen=True) +@@ -64,7 +64,7 @@ + opener: Callable = urlopen, + auth_rejected_cooldown: float = 30.0, + clock: Callable[[], float] = time.monotonic, +- group_provenance: GroupProvenance | None = None, ++ group_provenance: GroupJournalStore | None = None, + ) -> None: + self.group_provenance = group_provenance + self.base_url = base_url.rstrip("/") +@@ -243,7 +243,7 @@ + timeout=timeout, + ) + +- def _group_store(self) -> GroupProvenance: ++ def _group_store(self) -> GroupJournalStore: + store = self.group_provenance + if store is None: + raise GroupProvenanceUnavailable() +diff --git a/identity-provisioner/requirements-postgres.txt b/identity-provisioner/requirements-postgres.txt +new file mode 100644 +--- /dev/null ++++ b/identity-provisioner/requirements-postgres.txt +@@ -0,0 +1,5 @@ ++# Linux amd64 CPython3.12 runtime closure from user-engine/uv.lock. ++# Build-only dependency resolution; no packages are installed at service startup. ++psycopg==3.3.5 --hash=sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e ++psycopg-binary==3.3.5 --hash=sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672 ++typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 +diff --git a/identity-provisioner/tests/test_group_postgres.py b/identity-provisioner/tests/test_group_postgres.py +new file mode 100644 +--- /dev/null ++++ b/identity-provisioner/tests/test_group_postgres.py +@@ -0,0 +1,192 @@ ++"""Binding, CAS and ambiguous-commit refusal tests; native SQL proof is separate.""" ++from dataclasses import replace ++import json ++from unittest.mock import Mock ++import unittest ++ ++from group_provenance import GroupProvenanceUnavailable ++from group_provenance_postgres import GroupJournalBinding, PostgresGroupProvenance ++from server import configured_group_provenance ++from unittest.mock import patch ++import os ++ ++EPOCH = '41fbb0ed-ea3a-4682-996b-e9c94a6aa9c6' ++BINDING = GroupJournalBinding('tenant:fixture:one', 'synthetic-directory', 'sha256:' + '1' * 64, ++ 'sha256:' + '2' * 64, 'synthetic-owner', EPOCH) ++REQUEST = '3' * 64 ++HEAD_DIGEST = 'a' * 64 ++META = (EPOCH, 'synthetic-directory', 7, HEAD_DIGEST, False) ++ ++ ++def fixture(rows=(), rowcount=1, commit_error=None): ++ cursor = Mock() ++ cursor.__enter__ = Mock(return_value=cursor) ++ cursor.__exit__ = Mock(return_value=False) ++ cursor.fetchone.side_effect = rows ++ cursor.rowcount = rowcount ++ connection = Mock() ++ connection.__enter__ = Mock(return_value=connection) ++ connection.__exit__ = Mock(return_value=False, side_effect=commit_error) ++ connection.cursor.return_value = cursor ++ connector = Mock(return_value=connection) ++ continuity = Mock() ++ continuity.check.return_value = True ++ continuity.advance.return_value = True ++ store = PostgresGroupProvenance(connect=connector, binding=BINDING, continuity=continuity) ++ return store, connector, connection, cursor, continuity ++ ++ ++def receipt(version=4): ++ return {'key': 'synthetic-operation-key', 'binding': REQUEST, 'state': 'pending', 'intent': None, ++ 'groups': {}, 'created_groups': [], 'created_memberships': [], ++ '_pg_version': version, '_pg_context': BINDING.digest} ++ ++ ++class PostgresSourceTests(unittest.TestCase): ++ def test_explicit_binding_and_continuity_required_without_connecting(self): ++ connector = Mock() ++ for binding, continuity in ((None, Mock()), (BINDING, None), ({'tenant_scope': 'caller-json'}, Mock())): ++ with self.assertRaises(ValueError): ++ PostgresGroupProvenance(connect=connector, binding=binding, continuity=continuity) ++ connector.assert_not_called() ++ for change in ({'tenant_scope': ''}, {'principal': ''}, {'case_digest': 'unsigned'}, {'epoch': 'default'}): ++ with self.assertRaises(ValueError): ++ replace(BINDING, **change) ++ self.assertNotEqual(BINDING.digest, replace(BINDING, principal='another-owner').digest) ++ self.assertNotEqual(BINDING.digest, replace(BINDING, tenant_scope='another-explicit-scope').digest) ++ ++ def test_missing_environment_callback_path_keeps_production_disabled(self): ++ with patch.dict(os.environ, {'GROUP_POSTGRES_DSN': 'synthetic-only', 'GROUP_PROVENANCE_POSTGRES': 'true'}, clear=True): ++ self.assertIsNone(configured_group_provenance()) ++ ++ def test_checkpoint_mismatch_poisoned_without_receipt_query_or_effect(self): ++ store, connect, _, cursor, checkpoint = fixture([META]) ++ checkpoint.check.return_value = False ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.reserve('synthetic-operation-key', REQUEST) ++ self.assertFalse(any('group_receipts' in c.args[0] for c in cursor.execute.call_args_list)) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.check() ++ self.assertEqual(1, connect.call_count) ++ checkpoint.advance.assert_not_called() ++ ++ def test_restored_epoch_or_quarantine_refused(self): ++ for meta in ((EPOCH, 'other-directory', 7, HEAD_DIGEST, False), (EPOCH, 'synthetic-directory', 7, HEAD_DIGEST, True), ++ ('98aa2b6d-a32a-44a5-a89f-b2480befc00e', 'synthetic-directory', 7, HEAD_DIGEST, False), None): ++ store, _, _, _, checkpoint = fixture([meta]) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.check() ++ checkpoint.check.assert_not_called() ++ ++ def test_exactly_one_version_update_required_before_checkpoint_advance(self): ++ store, _, _, cursor, checkpoint = fixture([META], rowcount=0) ++ original = receipt() ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.save(original) ++ query, params = next(c.args for c in cursor.execute.call_args_list if c.args[0].startswith('UPDATE identity_provisioner.group_receipts')) ++ self.assertIn('AND version = %s', query) ++ self.assertEqual(4, params[-1]) ++ self.assertEqual(4, original['_pg_version']) ++ checkpoint.advance.assert_not_called() ++ ++ def test_success_acknowledges_commit_and_updates_receipt_version(self): ++ store, _, connection, _, checkpoint = fixture([META]) ++ original = receipt() ++ store.save(original) ++ self.assertEqual(5, original['_pg_version']) ++ connection.__exit__.assert_called_once() ++ args = checkpoint.advance.call_args.args ++ self.assertEqual((EPOCH, 'synthetic-directory', 7, HEAD_DIGEST, 8), args[:5]) ++ self.assertRegex(args[5], r'^[0-9a-f]{64}$') ++ self.assertNotEqual(HEAD_DIGEST, args[5]) ++ connection.close.assert_called_once() ++ ++ def test_connection_close_failure_is_sanitized_and_stops_continuation(self): ++ store, _, connection, _, _ = fixture([META]) ++ connection.close.side_effect = OSError('synthetic-private-diagnostic') ++ with self.assertRaisesRegex(GroupProvenanceUnavailable, '^group_provenance_unavailable$'): ++ store.check() ++ self.assertTrue(store._poisoned) ++ ++ def test_commit_failure_never_advances_owner_checkpoint_or_receipt(self): ++ store, _, _, _, checkpoint = fixture([META], commit_error=OSError('synthetic-secret-diagnostic')) ++ original = receipt() ++ with self.assertRaisesRegex(GroupProvenanceUnavailable, '^group_provenance_unavailable$'): ++ store.save(original) ++ self.assertEqual(4, original['_pg_version']) ++ checkpoint.advance.assert_not_called() ++ ++ def test_lost_checkpoint_acknowledgment_refuses_then_poisoned(self): ++ store, connect, _, _, checkpoint = fixture([META]) ++ checkpoint.advance.side_effect = OSError('synthetic-secret-diagnostic') ++ original = receipt() ++ with self.assertRaisesRegex(GroupProvenanceUnavailable, '^group_provenance_unavailable$'): ++ store.save(original) ++ self.assertEqual(4, original['_pg_version']) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.save(original) ++ self.assertEqual(1, connect.call_count) ++ ++ def test_cross_case_body_or_version_cannot_be_imported(self): ++ for body in (dict(receipt(), _pg_context='0' * 64), dict(receipt(), _pg_version=6)): ++ store, _, _, _, _ = fixture([META, (REQUEST, 4, json.dumps(body), json.dumps(BINDING.__dict__, sort_keys=True))]) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.get('synthetic-operation-key', REQUEST) ++ ++ def test_same_counter_divergent_body_or_context_yields_distinct_marker(self): ++ store, *_ = fixture() ++ markers = [] ++ for change in ({}, {'intent': {'operation': 'different'}}, {'key': 'another-exact-key'}, {'_pg_version': 5}): ++ tx = {'head': 7, 'digest': HEAD_DIGEST} ++ store._transition(tx, dict(receipt(), **change), 'save') ++ markers.append(tx['next_digest']) ++ self.assertEqual(4, len(set(markers))) ++ tx = {'head': 7, 'digest': 'b' * 64} ++ store._transition(tx, receipt(), 'save') ++ self.assertNotIn(tx['next_digest'], markers) ++ foreign, *_ = fixture() ++ foreign.binding = replace(BINDING, principal='another-owner') ++ tx = {'head': 7, 'digest': HEAD_DIGEST} ++ foreign._transition(tx, receipt(), 'save') ++ self.assertNotIn(tx['next_digest'], markers) ++ ++ def test_reopened_loser_clone_at_same_head_refuses_different_marker(self): ++ # Both DBs report head8. The independent callback accepts only winner ++ # digest; loser must be refused before loading any receipt or effect. ++ loser_digest = 'b' * 64 ++ store, _, _, cursor, checkpoint = fixture([(EPOCH, 'synthetic-directory', 8, loser_digest, False)]) ++ checkpoint.check.side_effect = lambda epoch, directory, head, digest: (head, digest) == (8, 'c' * 64) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.check() ++ checkpoint.check.assert_called_once_with(EPOCH, 'synthetic-directory', 8, loser_digest) ++ self.assertFalse(any('group_receipts' in c.args[0] for c in cursor.execute.call_args_list)) ++ ++ def test_existing_request_mismatch_is_refused_not_rebound(self): ++ store, _, _, _, checkpoint = fixture([META, ('0' * 64, 4, '{}', '{}')]) ++ with self.assertRaisesRegex(ValueError, 'binding mismatch'): ++ store.get('synthetic-operation-key', REQUEST) ++ checkpoint.advance.assert_not_called() ++ self.assertFalse(store._poisoned) ++ ++ def test_active_fence_capacity_refusal_and_no_insert(self): ++ for rows in ([META, None, (1,)], [META, None, None, (256,)]): ++ store, _, _, cursor, checkpoint = fixture(rows) ++ with self.assertRaises(GroupProvenanceUnavailable): ++ store.reserve('synthetic-operation-key', REQUEST) ++ self.assertFalse(any('INSERT' in c.args[0] for c in cursor.execute.call_args_list)) ++ checkpoint.advance.assert_not_called() ++ ++ def test_new_reservation_retains_bound_context_and_write_intent(self): ++ store, _, _, cursor, checkpoint = fixture([META, None, None, (0,)]) ++ created, fresh = store.reserve('synthetic-operation-key', REQUEST) ++ self.assertTrue(fresh) ++ self.assertEqual('pending', created['state']) ++ self.assertEqual(BINDING.digest, created['_pg_context']) ++ insert = next(c for c in cursor.execute.call_args_list if c.args[0].startswith('INSERT')) ++ self.assertIn('context_json', insert.args[0]) ++ self.assertEqual(BINDING.tenant_scope, json.loads(insert.args[1][1])['tenant_scope']) ++ checkpoint.advance.assert_called_once() ++ ++ ++if __name__ == '__main__': ++ unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_driver.py new file mode 100644 index 00000000..095ddfbc --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_driver.py @@ -0,0 +1,96 @@ +import contextlib +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import sys +import tarfile +import unittest +from unittest.mock import Mock, patch + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('group_fixture_driver', HERE / 'driver.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class PlanTests(unittest.TestCase): + def test_default_and_explicit_plan_do_not_load_containment_or_generate_material(self): + for args in ([], ['--plan']): + with patch.object(sys, 'argv', ['fixture', *args]), patch.object(f, 'load_containment') as load, patch.object(f, 'execute') as execute, patch.object(f.secrets, 'token_hex') as generate, contextlib.redirect_stdout(io.StringIO()) as out: + self.assertEqual(0, f.main()) + self.assertEqual('prepared_not_executed_not_approved', json.loads(out.getvalue())['status']) + load.assert_not_called(); execute.assert_not_called(); generate.assert_not_called() + + def test_wrong_manifest_digest_refuses_before_loading_helper(self): + with patch.object(sys, 'argv', ['fixture', '--execute-reviewed-fixture', '--expected-manifest-sha256', '0' * 64]), patch.object(f, 'load_containment') as load: + with self.assertRaises(RuntimeError): + f.main() + load.assert_not_called() + + def test_manifest_exact_source_closure_and_images(self): + manifest = f.reviewed_manifest(hashlib.sha256(f.MANIFEST.read_bytes()).hexdigest()) + self.assertEqual('unix:///var/run/docker.sock', manifest['fixed_endpoint']) + self.assertEqual('never', manifest['pull_policy']) + self.assertEqual('network-none', manifest['network']) + self.assertEqual(0, manifest['limits']['host_ports']) + self.assertEqual(0, manifest['limits']['host_mounts']) + self.assertEqual(2, manifest['limits']['max_service_containers']) + self.assertIn('sha256:', manifest['postgres_image']) + self.assertIn('sha256:', manifest['python_image']) + + def test_exact_fresh_names_no_host_network_ports_or_mounts(self): + manifest = json.loads(f.MANIFEST.read_text()) + session = f.session_plan(manifest, 'f' * 32) + self.assertEqual(2, len(set(session['containers']))) + for name in session['containers']: + args = f.base_args(session, name, 'none') + self.assertIn('--pull=never', args) + self.assertIn('--read-only', args) + self.assertIn('--log-driver', args) + self.assertIn('net-kingdom.recovery-run=' + 'f' * 32, args) + self.assertFalse({'--publish', '-p', '--volume', '-v', '--privileged', '--network=host'} & set(args)) + with self.assertRaises(RuntimeError): + f.session_plan(manifest, 'not-a-fresh-run') + + def test_tar_private_only_generated_named_material(self): + raw = f.tar_bytes({'password': b'synthetic-only', 'lldap.toml': b'synthetic-config'}) + with tarfile.open(fileobj=io.BytesIO(raw)) as archive: + self.assertTrue(all(member.uid == member.gid == 1000 and member.mode == 0o600 for member in archive)) + for path in ('../outside', '/absolute', 'path/../../escape'): + with self.assertRaises(RuntimeError): + f.tar_bytes({path: b'synthetic'}) + + def test_failure_still_invokes_existing_exact_cleanup(self): + manifest = json.loads(f.MANIFEST.read_text()) + adapter = Mock() + adapter.command.side_effect = RuntimeError('synthetic transport fault') + result = {'status': 'failed'} + with self.assertRaises(RuntimeError): + f.execute(manifest, adapter, result) + adapter.cleanup.assert_called_once() + session = adapter.cleanup.call_args[0][0] + self.assertEqual(session['containers'], result['session']['containers']) + self.assertTrue(result['daemon_request_uncertain']) + self.assertEqual('failed', result['status']) + + def test_every_declared_source_input_exists_and_schema_does_not_admit_epoch(self): + source = HERE.parent / 'postimage/identity-provisioner' + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', 'migrations/group_journal_postgres_v1.sql'): + self.assertTrue((source / name).is_file()) + self.assertTrue((HERE / 'test_service_groups.py').is_file()) + schema = (source / 'migrations/group_journal_postgres_v1.sql').read_text() + self.assertNotIn('INSERT INTO', schema) + self.assertIn('DEFAULT true', schema) + self.assertIn('active boolean UNIQUE', schema) + + def test_probe_syntax_and_no_network_expansion(self): + compile((HERE / 'probe.py').read_text(), str(HERE / 'probe.py'), 'exec') + compile(f.IMPORT, 'private-import', 'exec') + self.assertNotIn('subprocess', (HERE / 'probe.py').read_text()) + self.assertIn("'host': '127.0.0.1'", (HERE / 'probe.py').read_text()) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_service_groups.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_service_groups.py new file mode 100644 index 00000000..c6dd48bd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r1/test_service_groups.py @@ -0,0 +1,140 @@ +import unittest +import tempfile +from pathlib import Path + +from group_provenance import GroupProvenance + +from provisioner import LLDAPProvisioner, dispatch + + +class Directory(LLDAPProvisioner): + """In-memory LLDAP: users, groups and memberships.""" + + def __init__(self): + self.storage = tempfile.TemporaryDirectory() + self.group_provenance = GroupProvenance(str(Path(self.storage.name) / "groups.db"), initialize=True) + self.users = {"tegwick", "agent-codex"} + self.groups = {"lldap_admin": 1, "tenant:trial:one:users": 2, "forge-admins": 3} + self.members = {1: {"tegwick"}, 2: set(), 3: set()} + self.calls = [] + self.drop_add = False + + def _login(self): + return "test-token" + + def _directory(self, token): + return ([{"id": u} for u in sorted(self.users)], + [{"id": i, "displayName": n} for n, i in self.groups.items()]) + + def _user(self, token, subject): + return {"id": subject, "groups": [{"id": i, "displayName": n} for n, i in self.groups.items() + if subject in self.members[i]]} + + def _ensure_group(self, token, groups, name): + if name not in self.groups: + self.calls.append(("create", name)) + self.groups[name] = max(self.groups.values()) + 1 + self.members[self.groups[name]] = set() + return self.groups[name] + + def _add_group(self, token, username, group_id): + self.calls.append(("add", username, group_id)) + if not self.drop_add: + self.members[group_id].add(username) + + def _remove_group(self, token, username, group_id): + self.calls.append(("remove", username, group_id)) + self.members[group_id].discard(username) + + def _group_members(self, token, group_id): + return sorted(self.members[group_id]) + + def _gql(self, token, query, variables): + if "addUserToGroup" in query: + self._add_group(token, variables["userId"], variables["groupId"]) + return {"addUserToGroup": {"ok": True}} + if "deleteGroup" in query: + self.calls.append(("delete", variables["id"])) + name = next(n for n, i in self.groups.items() if i == variables["id"]) + del self.groups[name] + return {"deleteGroup": {"ok": True}} + raise AssertionError(query) + + +def req(**kw): + p = {"groups": ["forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"], + "members": {"forge-admins": ["tegwick"], "forge-coulomb-owners": ["tegwick"]}, + "idempotency_key": "forgejo-netkingdom-oidc-1", "correlation_id": "test"} + p.update(kw) + return p + + +class EnsureTests(unittest.TestCase): + def test_creates_missing_groups_and_adds_members(self): + d = Directory() + r = dispatch(d, "/v1/groups/ensure", req()) + self.assertEqual("groups_ensured", r.status) + self.assertFalse(r.resumed) + self.assertEqual({"forge-coulomb-owners", "forge-coulomb-readers"}, + {c[1] for c in d.calls if c[0] == "create"}) + self.assertIn("tegwick", d.members[d.groups["forge-admins"]]) + self.assertIn("tegwick", d.members[d.groups["forge-coulomb-owners"]]) + + def test_idempotent_and_reports_resumed(self): + d = Directory() + dispatch(d, "/v1/groups/ensure", req()) + d.calls.clear() + r = dispatch(d, "/v1/groups/ensure", req()) + self.assertTrue(r.resumed) + self.assertEqual([], [c for c in d.calls if c[0] == "create"]) + + def test_never_removes_other_memberships(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + self.assertIn("agent-codex", d.members[3]) + self.assertEqual([], [c for c in d.calls if c[0] == "remove"]) + + def test_reserved_and_invalid_requests_change_nothing(self): + bad = [req(groups=["lldap_admin"]), req(groups=["tenant-x"]), req(groups=["netkingdom-suspended"]), + req(groups=["Forge Admins"]), req(groups=[]), + req(members={"not-listed": ["tegwick"]}), + req(members={"forge-admins": ["Not A User"]}), + req(members={"forge-admins": ["nobody"]})] + for payload in bad: + d = Directory() + with self.assertRaises(ValueError, msg=payload): + dispatch(d, "/v1/groups/ensure", payload) + self.assertEqual([], d.calls, payload) + + def test_failed_readback_never_reports_success(self): + d = Directory() + d.drop_add = True + with self.assertRaises(RuntimeError): + dispatch(d, "/v1/groups/ensure", req()) + + +class RevokeTests(unittest.TestCase): + def test_removes_only_listed_memberships_and_keeps_groups_by_default(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + r = dispatch(d, "/v1/groups/revoke", req(ensure_idempotency_key=req()["idempotency_key"], ensure_correlation_id=req()["correlation_id"])) + self.assertEqual("groups_revoked", r.status) + self.assertEqual({"agent-codex"}, d.members[d.groups["forge-admins"]]) + self.assertIn("forge-coulomb-owners", d.groups) + + def test_deletes_only_empty_listed_groups_when_asked(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + r = dispatch(d, "/v1/groups/revoke", req(delete_if_empty=True, ensure_idempotency_key=req()["idempotency_key"], ensure_correlation_id=req()["correlation_id"])) + self.assertEqual("groups_revoked_and_deleted", r.status) + self.assertIn("forge-admins", d.groups) + self.assertNotIn("forge-coulomb-owners", d.groups) + self.assertNotIn("forge-coulomb-readers", d.groups) + self.assertIn("lldap_admin", d.groups) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/README.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/README.md new file mode 100644 index 00000000..0c3cfbc9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/README.md @@ -0,0 +1,72 @@ +# Prepared disposable PostgreSQL compatibility fixture + +Status: prepared and unit checked; not executed or approved. Default +`python3 driver.py --plan` reads only the manifest, without loading containment, +generating material or calling Docker. Execution requires independent review of +the exact manifest/source closure and explicit execution flag/digest. The flag +and fixture files confer no authority. + +The fixture reuses the existing `isolated-keycape.py` command/environment guard, +containment inspector and exact name/label cleanup. A network-none PostgreSQL +anchor and a pinned cached User Engine Python helper share only loopback. Both +are nonroot/read-only/capability-free, with private bounded tmpfs, no host ports, +mounts or Docker socket, and no pulls/builds/packages. Generated admin/runtime +passwords enter through stdin only. Image-specific nonroot UID999/Python/psycopg +compatibility is an explicit precondition checked by execution; no cache or +runtime claim is made during preparation. Any incompatible image/anonymous +volume/error fails closed and still invokes exact cleanup. + +Six sequential Python phases use the actual candidate SQL backend and its +normal provisioner path with the **existing synthetic LDAP Directory**. Bootstrap +runs the real migration on generated state, checks runtime DDL denial and exact +created-only inventories. A separate process reopens and replays/revokes while +preserving the complete synthetic prestate. Concurrent reservations must yield +one original receipt; stale row CAS cannot advance its head. Two actual database +clones then pass the same old checkpoint check concurrently and commit different +receipts at the same next counter. Only one external digest CAS may win; a fresh +backend on the losing clone must refuse even at that same counter. Lost checkpoint +acknowledgment, lost synthetic directory response and a lost reply after actual +SQL commit must retain pending/uncertain state and refuse continuation. An actual +pg_dump taken after bootstrap is then pg_restored after later journal changes; +the external test checkpoint survives. Runtime SQL SELECT must work and show an +older restored head before the candidate's restore-quarantine refusal oracle can +pass. This prevents an unrelated connection/ACL failure from masquerading as +restore protection. + +All fresh names/labels are registered before create. `finally` uses the existing +cleanup helper for both resources, requires successful exact absence inventories +and retains any Docker uncertainty as failed. PID1 lifetimes and RPC/execution +limits are bounded in the manifest. This is not a parent-death/reaper proof. +Only safe booleans, public image/source/version pins and cleanup duties leave in +a private `/tmp/nk48-postgres-native-review-*/result.json`; synthetic backup bytes +are bounded in host memory and never written as an archive file. + +The test checkpoint implementation belongs only to this fixture. Its independent +file survives PostgreSQL restore and uses file locking/fsync; it is not offered +as native custody, case authority, a journal-recovery API or an evidence broker. +The fixture explicitly disposes one definitely pre-LDAP synthetic reservation to +move between fault cases; it does not clear an unknown native mutation. All +source/fixture passes remain distinct from live CNPG/lease/TLS/backup continuity, +protected owner callbacks, actual authoritative tenant scope, native LDAP fencing +and production acceptance. + + +## R2 cached helper selection + +The reviewed R1 preflight failed before `execute`, session allocation or private +import because its pinned helper image 1e6547 was absent. No pull or override was +used. Frozen manifest/driver/probe/source and the coordinator's structured failure +are retained under `revisions/r1`. There is no raw stderr file; the original +traceback was returned in tool output only, so no byte-exact stderr artifact is +invented. + +The proposed R2 helper is the coordinator-observed cached f239 image, bound by +existing published receipts to User Engine source 1bb7cacc. That source uses +system pip on official Python 3.12, not a virtualenv. R2 therefore binds every +helper launch/exec to `/usr/local/bin/python3`; it has no PATH or alternate-venv +fallback. A closed in-container probe must verify the exact executable and import +psycopg 3 before candidate source import, reporting the bundled version. Missing +or mismatched interpreter/dependency refuses and preserves exact cleanup duties. +No container count, limit, privilege, network or cleanup expansion is proposed. +Historical producer or rollout acceptance is not a new image/dependency grant. +See `helper-selection-r2.json`, `manifest-delta-r2.json` and `preparation-r2.json`. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/driver.py new file mode 100644 index 00000000..1932231d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/driver.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Default is a read-only plan. Docker execution awaits independent exact review.""" +import argparse +import base64 +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import secrets +import tarfile +import tempfile +import time +import uuid + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE / 'manifest.json' + + +def require(condition): + if not condition: + raise RuntimeError('bounded_synthetic_fixture_refused') + + +def reviewed_manifest(expected): + raw = MANIFEST.read_bytes() + require(expected == hashlib.sha256(raw).hexdigest()) + manifest = json.loads(raw) + for path, digest in manifest['source_sha256'].items(): + require(bool(re.fullmatch(r'sha256:[0-9a-f]{64}', digest))) + require('sha256:' + hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest) + return manifest + + +def load_containment(manifest): + spec = importlib.util.spec_from_file_location('reviewed_existing_containment', manifest['containment_helper']) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def session_plan(manifest, run_id): + require(bool(re.fullmatch(r'[a-f0-9]{32}', run_id))) + anchor = 'nk49-kc-' + run_id + '-journal-postgres' + probe = 'nk49-kc-' + run_id + '-journal-probe' + return {'run_id': run_id, 'containers': [anchor, probe], 'worker': anchor, 'issuer': probe, + 'images': {anchor: manifest['postgres_image'], probe: manifest['python_image']}, 'volumes': {}} + + +def base_args(session, name, network, user="1000:1000"): + uid, gid = user.split(":") + return ['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', network, + '--label', 'net-kingdom.recovery-run=' + session['run_id'], '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', user, '--memory', '384m', '--cpus', '0.5', + '--pids-limit', '64', '--log-driver', 'none', '--restart', 'no', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=' + uid + ',gid=' + gid + ',mode=0700'] + + +def tar_bytes(files): + out = io.BytesIO() + with tarfile.open(fileobj=out, mode='w') as archive: + for name, value in files.items(): + require(bool(re.fullmatch(r'[a-zA-Z0-9._/-]+', name)) and not name.startswith('/') and '..' not in Path(name).parts) + entry = tarfile.TarInfo(name) + entry.size = len(value) + entry.mode = 0o600 + entry.uid = entry.gid = 1000 + archive.addfile(entry, io.BytesIO(value)) + return out.getvalue() + + +IMPORT = r''' +import base64,json,os,sys +from pathlib import Path +os.umask(0o077) +root=Path('/fixture') +(root/'source').mkdir(mode=0o700) +(root/'source/migrations').mkdir(mode=0o700) +(root/'source/tests').mkdir(mode=0o700) +for name,encoded in json.load(sys.stdin).items(): + if name not in ('source/provisioner.py','source/group_provenance.py','source/group_provenance_postgres.py','source/migrations/group_journal_postgres_v1.sql','source/tests/test_service_groups.py','probe.py','private.json'):raise ValueError() + fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as output:output.write(base64.b64decode(encoded,validate=True)) +''' + + +def helper_python(manifest): + # Exact producer source installs into official Python's system prefix. + # Never fall back to PATH or another interpreter/virtual environment. + require(manifest.get('helper_interpreter') == '/usr/local/bin/python3') + return manifest['helper_interpreter'] + + +INTERPRETER_PROBE = r""" +import json,sys +try: + import psycopg + valid=sys.executable=='/usr/local/bin/python3' and psycopg.__version__.split('.')[0]=='3' + print(json.dumps({'helper_ready':valid,'executable':sys.executable,'psycopg_version':psycopg.__version__})) +except Exception: + print(json.dumps({'helper_ready':False})) +""" + + +def preflight(adapter, manifest): + helper_python(manifest) + adapter.docker_environment() # Refuse ambient alternate routing before calls. + contexts = json.loads(adapter.command(['docker', 'context', 'inspect'], timeout=15)) + require(len(contexts) == 1 and contexts[0]['Endpoints']['docker']['Host'] == manifest['fixed_endpoint']) + facts = json.loads(adapter.command(['docker', 'info', '--format', + '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'], timeout=15)) + require(facts == {'ID': manifest['engine_id'], 'Name': manifest['engine_name'], 'OSType': 'linux'}) + for image in (manifest['postgres_image'], manifest['python_image']): + value = adapter.command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image], timeout=15).decode().strip() + require(bool(re.fullmatch(r'sha256:[a-f0-9]{64}', value))) + + +def execute(manifest, adapter, result): + session = session_plan(manifest, uuid.uuid4().hex) + result['session'] = {'run_id': session['run_id'], 'containers': session['containers'], + 'label': 'net-kingdom.recovery-run=' + session['run_id']} + python = helper_python(manifest) + deadline = time.monotonic() + 210 + original_command = adapter.command + def run(argv, **kwargs): + remaining = deadline - time.monotonic() + require(remaining > 0) + try: + return original_command(argv, timeout=min(50, remaining), **kwargs) + except BaseException: + result['daemon_request_uncertain'] = True + raise + adapter.command = run + try: + anchor = session['worker'] + args = base_args(session, anchor, 'none', user='999:999') + [ + '--tmpfs', '/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700', + '--tmpfs', '/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--env', 'PGDATA=/var/lib/postgresql/data', '--entrypoint', '/bin/sh', + manifest['postgres_image'], '-c', 'sleep 240'] + run(args) + adapter.inspect_session({**session, 'containers': [anchor], 'issuer': anchor}, worker_only=True) + admin_password, runtime_password = secrets.token_hex(24), secrets.token_hex(24) + run(['docker', 'exec', '-i', anchor, 'tar', '-xf', '-', '-C', '/run/fixture'], + input=tar_bytes({'password': admin_password.encode()})) + run(['docker', 'exec', '--detach', anchor, '/bin/sh', '-c', + 'initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1 && exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1']) + anchor_id = run(['docker', 'container', 'inspect', anchor, '--format', '{{.Id}}']).decode().strip() + require(bool(re.fullmatch(r'[a-f0-9]{64}', anchor_id))) + args = base_args(session, session['issuer'], 'container:' + anchor_id) + [ + '--tmpfs', '/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700', + '--entrypoint', python, manifest['python_image'], '-c', 'import time;time.sleep(240)'] + run(args) + adapter.inspect_session(session, worker_only=True) + result['containment_verified_before_generated_private_import'] = True + interpreter = json.loads(run(['docker', 'exec', session['issuer'], python, '-c', INTERPRETER_PROBE], max_output=4096)) + require(interpreter.get('helper_ready') is True and interpreter.get('executable') == python) + require(isinstance(interpreter.get('psycopg_version'), str) and interpreter['psycopg_version'].split('.')[0] == '3') + result['helper_interpreter_verified_before_candidate_import'] = True + result['helper_psycopg_version'] = interpreter['psycopg_version'] + files = {'source/' + name: (HERE.parent / 'postimage/identity-provisioner' / name).read_bytes() + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', + 'migrations/group_journal_postgres_v1.sql')} + files['source/tests/test_service_groups.py'] = (HERE / 'test_service_groups.py').read_bytes() + files.update({'probe.py': (HERE / 'probe.py').read_bytes(), 'private.json': json.dumps({ + 'admin_password': admin_password, 'runtime_password': runtime_password}).encode()}) + run(['docker', 'exec', '-i', session['issuer'], python, '-c', IMPORT], + input=json.dumps({k: base64.b64encode(v).decode() for k, v in files.items()}).encode()) + result['checks'] = {} + backup = None + active_database = 'postgres' + for phase in ('bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', 'restored'): + if phase == 'restored': + require(backup is not None) + run(['docker', 'exec', '-i', anchor, 'pg_restore', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', active_database, '--clean', '--if-exists', '--single-transaction', '--exit-on-error', '--no-owner'], input=backup) + value = json.loads(run(['docker', 'exec', session['issuer'], python, '/fixture/probe.py', phase], max_output=8192)) + expected = set(manifest['phase_oracles'][phase]) + require(value.get('status') == 'passed' and set(value.get('checks', {})) == expected) + require(all(v is True for v in value['checks'].values())) + result['checks'].update(value['checks']) + result['psycopg_version'] = value['psycopg_version'] + require(value.get('active_database') in {'postgres', 'fixture_fork'}) + active_database = value['active_database'] + if phase == 'bootstrap': + backup = run(['docker', 'exec', anchor, 'pg_dump', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', 'postgres', '--format=custom', '--no-owner'], max_output=4194304) + result['status'] = 'synthetic_postgres_journal_compatibility_passed' + finally: + def cleanup_command(argv, **kwargs): + try: return original_command(argv, timeout=15, **kwargs) + except BaseException: + result['daemon_request_uncertain'] = True + raise + adapter.command = cleanup_command + try: + adapter.cleanup(session, result) + result['container_cleanup_verified'] = True + except BaseException: + result['status'] = 'failed' + result['container_cleanup_verified'] = False + adapter.command = original_command + if result.get('daemon_request_uncertain'): + result['status'] = 'failed' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--plan', action='store_true') + parser.add_argument('--execute-reviewed-fixture', action='store_true') + parser.add_argument('--expected-manifest-sha256') + args = parser.parse_args() + if not args.execute_reviewed_fixture: + print(MANIFEST.read_text()) + return 0 + require(not args.plan) + manifest = reviewed_manifest(args.expected_manifest_sha256) + adapter = load_containment(manifest) + preflight(adapter, manifest) + os.umask(0o077) + output = Path(tempfile.mkdtemp(prefix='nk48-postgres-native-review-', dir='/tmp')) / 'result.json' + result = {'schema': 'nk48.synthetic-postgres-journal-result.v1', 'status': 'failed', + 'manifest_sha256': args.expected_manifest_sha256, 'production_contacted': False, + 'actual_credentials_used': False, 'native_acceptance_claimed': False} + try: + execute(manifest, adapter, result) + except BaseException: + result['status'] = 'failed' + finally: + output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({'status': result['status'], 'safe_result': str(output)})) + return int(result['status'] != 'synthetic_postgres_journal_compatibility_passed') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/execution-result.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/execution-result.json new file mode 100644 index 00000000..ee09e81d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/execution-result.json @@ -0,0 +1,31 @@ +{ + "schema": "nk48.synthetic-postgres-journal-result.v1", + "status": "failed", + "manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "production_contacted": false, + "actual_credentials_used": false, + "native_acceptance_claimed": false, + "session": { + "run_id": "e6e2c053a85346508fc0e35d4530b951", + "containers": [ + "nk49-kc-e6e2c053a85346508fc0e35d4530b951-journal-postgres", + "nk49-kc-e6e2c053a85346508fc0e35d4530b951-journal-probe" + ], + "label": "net-kingdom.recovery-run=e6e2c053a85346508fc0e35d4530b951" + }, + "containment_verified_before_generated_private_import": true, + "helper_interpreter_verified_before_candidate_import": true, + "helper_psycopg_version": "3.3.6", + "checks": {}, + "container_cleanup_obligations": [ + { + "container": "nk49-kc-e6e2c053a85346508fc0e35d4530b951-journal-probe", + "status": "absence_verified" + }, + { + "container": "nk49-kc-e6e2c053a85346508fc0e35d4530b951-journal-postgres", + "status": "absence_verified" + } + ], + "container_cleanup_verified": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/helper-selection-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/helper-selection-r2.json new file mode 100644 index 00000000..608d1952 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/helper-selection-r2.json @@ -0,0 +1,32 @@ +{ + "status": "fixture_helper_successor_proposed_not_executed", + "selected_cached_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "producer_revision": "1bb7cacc98479f56f6b1c28c000479efd260a944", + "producer_containerfile_sha256": "668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a", + "producer_containerfile_public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/1bb7cacc98479f56f6b1c28c000479efd260a944/Containerfile", + "provenance_sources": [ + { + "repo": "rapp-user-engine", + "path": "docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json", + "revision": "a42c68f8701880bfeb93e28dd812fcf3691568a3", + "sha256": "2f84959e36d442860fb2b5a5817bb707c4f8372797a3ef8dd0d5cab956af86ce", + "public_source": "https://forgejo.coulomb.social/coulomb/rapp-user-engine/src/commit/a42c68f8701880bfeb93e28dd812fcf3691568a3/docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json" + }, + { + "repo": "user-engine", + "path": "docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json", + "revision": "9557d6f867fe03c546aa26655f84b4192109981d", + "sha256": "964cc3755e96330bf6d1ada63410f8adfdb3c45602e0446369d6dddace412405", + "public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/9557d6f867fe03c546aa26655f84b4192109981d/docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json" + } + ], + "selection_reason": "coordinator reports exact image cached; published source receipt binds image to producer; prior owner fixture used exact image for psycopg dependencies", + "interpreter": "/usr/local/bin/python3", + "interpreter_binding": "producer officialPython3.12+systempip runtime, no virtualenv; execution must verify exact sys.executable/psycopg3 before candidate import, no PATHfallback", + "not_claimed": [ + "worker did not query Docker or create/run containers", + "bundled psycopg version not observed yet; must be reported by exact helper", + "historical image risk/production acceptance not transferred", + "cached helper is not a future identity-provisioner image/dependency admission" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest-delta-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest-delta-r2.json new file mode 100644 index 00000000..bafc66af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest-delta-r2.json @@ -0,0 +1,28 @@ +{ + "from_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "to_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "source_patch_unchanged": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "helper_image": { + "before": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "after": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b" + }, + "helper_interpreter": { + "before": "PATH python3", + "after": "/usr/local/bin/python3", + "verification": "closed exact sys.executable/psycopg3 probe before candidate source import" + }, + "unchanged_contracts": { + "postgres_image": true, + "fixed_endpoint": true, + "engine_id": true, + "engine_name": true, + "limits": true, + "resources": true, + "phase_oracles": true, + "cleanup": true, + "network": true, + "pull_policy": true + }, + "worker_docker_calls": false, + "execution_requires_new_independent_review": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest.json new file mode 100644 index 00000000..503652eb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/manifest.json @@ -0,0 +1,129 @@ +{ + "schema": "nk48.postgres-journal-fixture-proposal.v1", + "status": "prepared_not_executed_not_approved", + "source_sha256": { + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py": "sha256:3dc033d7f1defc6c9b3d521889e0172aa05da3dadf4b9703d5c00a9beee9d9a9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py": "sha256:97265a65f008e86c6f3de54eaa1a7d77f069117d4b923fb85cf835a8816adf00", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py": "sha256:98ca14d4919323effb6e9b32a88ba657ef3249c47b6ed85092b5c80eb5833457", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/source-manifest.json": "sha256:0c16e19415ad93b6ca73c92a05c205d263adde8a780c8c33119cd5f9d40f4752", + "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/provisioner.py": "sha256:19d1b837cbafeeacb1aa815856cd68091e6702281d72b1d860230fa320e1725b", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance.py": "sha256:405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance_postgres.py": "sha256:55282af26f95af66ff8d444d38d0bcbcf3ee58a615adedfff8da5db2b0716889", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/migrations/group_journal_postgres_v1.sql": "sha256:3cdf82194d6f1e2dd865b0c7e7b8a14447fda8c49151148134a0d40a80fb46ac", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md": "sha256:0dde6c2831b4a836824a6bec1d78f1014306bca27dcb33fa5d923ddd9cbe7cba", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/README.md": "sha256:4bf4678a1f1c70fbd3d4ffa77c70821be0372f03210b2164a2895fc99ca6a6b9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json": "sha256:ec5464c0d5310028ab91073f985183af01da2d194358fad412d197dbf90106d9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt": "sha256:668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a" + }, + "candidate_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "containment_helper": "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py", + "fixed_endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "postgres_image": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "python_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "image_pin_origin": "PostgreSQL pin unchanged; cached f239 helper selected from coordinator inventory and tied to UserEngine producer1bb7cacc by existing published receipt/previous owner PostgreSQL fixture; fixture-only selection, no future image admission", + "pull_policy": "never", + "network": "network-none", + "scope": "real disposable PostgreSQL backend SQL/migration/concurrency/restore compatibility; LDAP is explicitly the existing synthetic test Directory, not actual native LDAP in this fixture", + "limits": { + "max_service_containers": 2, + "host_ports": 0, + "host_mounts": 0, + "named_or_anonymous_volumes": 0, + "memory_bytes_per_container": 402653184, + "cpus_per_container": 0.5, + "pids_per_container": 64, + "postgres_tmpfs_bytes": 153092096, + "helper_tmpfs_bytes": 33554432, + "backup_in_host_memory_max_bytes": 4194304, + "container_lifetime_seconds": 240, + "execution_deadline_seconds": 210, + "docker_rpc_timeout_seconds": 50, + "preflight_rpc_timeout_seconds": 15, + "cleanup_rpc_timeout_seconds": 15, + "cleanup_total_bound_seconds": 120, + "new_cloud_spend_eur": 0 + }, + "compatibility_preconditions": [ + "exact cached image/engine pins reverified by admitted preflight, never pulled", + "PostgreSQL image supports nonroot UID999 with initdb/postgres/pg_dump/pg_restore in PATH; failed compatibility refuses/cleans", + "User Engine f239 producer source uses official Python system prefix; exact /usr/local/bin/python3 and bundled psycopg3 must pass closed probe before candidate import, no PATH/venv fallback or downloads", + "all image-declared volumes must be masked by private tmpfs; existing containment guard refuses any anonymous volume before private import" + ], + "resources": { + "container_names": "nk49-kc--journal-{postgres,probe}", + "exact_label": "net-kingdom.recovery-run=", + "private_state": "generated admin/runtime passwords, source/migration, synthetic directory, external test checkpoint within private container tmpfs", + "safe_result": "private /tmp/nk48-postgres-native-review-/result.json" + }, + "phase_oracles": { + "bootstrap": [ + "only_loopback_no_external_routes", + "actual_postgresql_journal_ensure", + "explicit_scope_context_retained", + "confirmed_inventory_excludes_preexisting", + "runtime_ddl_denied" + ], + "restart": [ + "only_loopback_no_external_routes", + "separate_process_postgres_restart_and_revoke", + "preexisting_shared_state_preserved", + "foreign_context_receipt_refused" + ], + "concurrency": [ + "only_loopback_no_external_routes", + "concurrent_original_reservation_exactly_once", + "stale_version_update_refused_without_head_advance" + ], + "lost-ack": [ + "only_loopback_no_external_routes", + "lost_checkpoint_ack_retains_pending_reservation", + "reopened_uncertainty_refuses_ldap_effects", + "active_fence_survives_process_reopen", + "lost_synthetic_directory_response_retains_postgres_intent", + "lost_sql_commit_reply_retains_committed_head", + "reopened_stale_checkpoint_refuses_continuation" + ], + "restored": [ + "only_loopback_no_external_routes", + "actual_pg_restore_behind_checkpoint_detected", + "restored_store_cannot_gain_replay_authority" + ], + "fork": [ + "only_loopback_no_external_routes", + "concurrent_divergent_clones_same_counter_verified", + "independent_exact_transition_cas_selects_one_winner", + "reopened_loser_clone_refused_at_same_counter" + ] + }, + "restore": "actual generated pg_dump after bootstrap; winner database advances through reopen/concurrency/fork/uncertainty; actual pg_restore of original archive while independent content checkpoint survives; runtime SELECT must work and show older restored head before refusal can pass", + "cleanup": "finally reuses exact name/label cleanup for both preregistered containers, including failed/lost-create replies; successful explicit absence required; any Docker uncertainty remains failed; no prune/global removal", + "not_claimed": [ + "CNPG HA, native runtime storage/backup/restore admission", + "authoritative tenant/case/PDP/evidence/session owner callbacks", + "external LDAP writer fencing/CAS/ABA safety", + "production endpoint activation, credentials or owner assent", + "KeyCape hard-parent-loss reaper proof" + ], + "forbidden": [ + "Docker commands before exact independent review", + "pull/build/push, host networking/ports/mounts, production APIs", + "real database/LDAP, identity archives, credentials from environment/custody", + "clearing production restore quarantine or manufacturing scope/authority" + ], + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "" + ], + "fork": "actual PostgreSQL CREATE DATABASE TEMPLATE clone; both stores pass exact same old external marker check under test barrier, commit different bodies at same counter, race external strict CAS; exactly one wins and fresh loser store refuses; no directory effect in this direct reservation case", + "revision": "r2-cached-helper-explicit-interpreter", + "helper_interpreter": "/usr/local/bin/python3", + "supersedes_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "r1_preflight": "failed missing helper before execute/session/private import; preserved structured failure, no raw stderr file exists" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/outcome.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/outcome.json new file mode 100644 index 00000000..489aa504 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/outcome.json @@ -0,0 +1,13 @@ +{ + "status": "failed_exact_subphase_unknown", + "source": "coordinator execution and safe receipt; no worker Docker calls", + "manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "receipt_sha256": "fb134a5f0444f7aabb6ea8f48f7e9bcdeb028bf42faecd477d0eeee882f14d3c", + "containment_verified": true, + "interpreter_verified": true, + "bundled_psycopg_version": "3.3.6", + "checks": {}, + "both_containers_absence_verified_in_receipt": true, + "daemon_uncertainty_reported": false, + "diagnostic_limit": "R2driver discarded failed helperresponse before retaining it; bootstrap plausible but not proved; no exact failure retroassigned" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/preparation-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/preparation-r2.json new file mode 100644 index 00000000..1bea487e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/preparation-r2.json @@ -0,0 +1,13 @@ +{ + "status": "prepared_review_required_not_executed", + "fixture_plan_command": "python3 -m pytest -q docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py", + "fixture_plan_result": "9 passed in0.05s", + "source_backend_unchanged": true, + "source_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "r2_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "r1_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "worker_docker_executed": false, + "root_reported_r1_preflight": "missing helper, failed before execute/session/private import; structured original retained, rawstderrfile absent", + "native_success_claimed": false, + "producer_history_is_not_future_image_admission": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/probe.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/probe.py new file mode 100644 index 00000000..2763bd48 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/probe.py @@ -0,0 +1,287 @@ +"""Synthetic PostgreSQL journal compatibility; LDAP remains a declared test double.""" +import concurrent.futures +from dataclasses import replace +import fcntl +import json +import os +from pathlib import Path +import sys +import time +import threading +import uuid + +ROOT = Path('/fixture') +sys.path[:0] = [str(ROOT / 'source'), str(ROOT / 'source/tests')] +import psycopg +from group_provenance import GroupProvenanceUnavailable +from group_provenance_postgres import GroupJournalBinding, PostgresGroupProvenance, psycopg_connector, journal_genesis +from test_service_groups import Directory, req + +EPOCH = 'bfbb8939-e88c-4f9c-a5c6-14ba3e399ddf' +CONTEXT = GroupJournalBinding('tenant:synthetic:one', 'synthetic-pg-directory', 'sha256:' + '1' * 64, + 'sha256:' + '2' * 64, 'synthetic-principal', EPOCH) + + +def require(value): + if not value: + raise RuntimeError('postgres_fixture_oracle_failed') + + +def info(admin=False, database=None): + values = json.loads((ROOT / 'private.json').read_text()) + database = database or ((ROOT / 'active-database').read_text() if (ROOT / 'active-database').exists() else 'postgres') + require(database in {'postgres', 'fixture_fork'}) + return {'host': '127.0.0.1', 'port': 5432, 'dbname': database, 'user': 'fixture_admin' if admin else 'fixture_app', + 'password': values['admin_password'] if admin else values['runtime_password'], 'sslmode': 'disable'} + + +class FileCheckpoint: + """Fixture-only independently retained checkpoint; never a native owner grant.""" + def __init__(self): + self.path = ROOT / 'checkpoint.json' + def _apply(self, epoch, directory, previous, previous_digest, current=None, current_digest=None): + with (ROOT / 'checkpoint.lock').open('a+') as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + value = json.loads(self.path.read_text()) + require(value == {'epoch': epoch, 'directory': directory, 'head': previous, 'digest': previous_digest}) + if current is not None: + temporary = ROOT / ('checkpoint-' + uuid.uuid4().hex) + with temporary.open('x') as out: + json.dump({'epoch': epoch, 'directory': directory, 'head': current, 'digest': current_digest}, out) + out.flush(); os.fsync(out.fileno()) + os.replace(temporary, self.path) + fd = os.open(ROOT, os.O_RDONLY | os.O_DIRECTORY) + try: os.fsync(fd) + finally: os.close(fd) + return True + def check(self, epoch, directory, head, digest): + return self._apply(epoch, directory, head, digest) + def advance(self, epoch, directory, previous, previous_digest, current, current_digest): + return self._apply(epoch, directory, previous, previous_digest, current, current_digest) + + +def store(context=CONTEXT, checkpoint=None, database=None): + return PostgresGroupProvenance(connect=psycopg_connector(connection_info=lambda: info(database=database)), binding=context, + continuity=checkpoint or FileCheckpoint()) + + +def refused(call): + try: call() + except (ValueError, RuntimeError): return + raise RuntimeError('postgres_fixture_expected_refusal_missing') + + +def snapshot(directory): + return {'users': sorted(directory.users), 'groups': directory.groups, + 'members': {str(k): sorted(v) for k, v in directory.members.items()}} + + +def restore_directory(value): + directory = Directory() + directory.users = set(value['users']) + directory.groups = value['groups'] + directory.members = {int(k): set(v) for k, v in value['members'].items()} + directory.group_provenance = store() + return directory + + +def bootstrap(): + for _ in range(80): + try: + db = psycopg.connect(**info(admin=True), connect_timeout=1) + db.close(); break + except Exception: time.sleep(.25) + else: raise RuntimeError('synthetic_postgres_not_ready') + from psycopg import sql + with psycopg.connect(**info(admin=True)) as db: + db.execute('CREATE SCHEMA identity_provisioner') + db.execute((ROOT / 'source/migrations/group_journal_postgres_v1.sql').read_text()) + db.execute('INSERT INTO identity_provisioner.group_journal_meta VALUES (true, %s, %s, 0, %s, false)', (EPOCH, CONTEXT.directory, journal_genesis(EPOCH, CONTEXT.directory))) + db.execute(sql.SQL('CREATE ROLE fixture_app LOGIN PASSWORD {}').format(sql.Literal(info()['password']))) + db.execute('GRANT USAGE ON SCHEMA identity_provisioner TO fixture_app') + db.execute('GRANT SELECT, INSERT, UPDATE ON identity_provisioner.group_receipts TO fixture_app') + db.execute('GRANT SELECT ON identity_provisioner.group_journal_meta TO fixture_app') + db.execute('GRANT UPDATE (head,head_digest) ON identity_provisioner.group_journal_meta TO fixture_app') + (ROOT / 'checkpoint.json').write_text(json.dumps({'epoch': EPOCH, 'directory': CONTEXT.directory, 'head': 0, 'digest': journal_genesis(EPOCH, CONTEXT.directory)})) + directory = Directory() + directory.members[3].add('tegwick') + before = snapshot(directory) + directory.group_provenance = store() + result = directory.ensure_service_groups(req()) + require(result.status == 'groups_ensured') + require({x['name'] for x in result.created_groups} == {'forge-coulomb-owners', 'forge-coulomb-readers'}) + require({(x['name'], x['username']) for x in result.created_memberships} == {('forge-coulomb-owners', 'tegwick')}) + (ROOT / 'directory.json').write_text(json.dumps({'before': before, 'after': snapshot(directory)})) + with psycopg.connect(**info()) as db: + try: db.execute('CREATE TABLE identity_provisioner.runtime_ddl_must_fail (id integer)') + except psycopg.errors.InsufficientPrivilege: pass + else: raise RuntimeError('runtime_ddl_not_denied') + return {'actual_postgresql_journal_ensure': True, 'explicit_scope_context_retained': True, + 'confirmed_inventory_excludes_preexisting': True, 'runtime_ddl_denied': True} + + +def restart(): + saved = json.loads((ROOT / 'directory.json').read_text()) + directory = restore_directory(saved['after']) + require(directory.ensure_service_groups(req()).resumed) + require(not directory.calls) + result = directory.revoke_service_groups(req(ensure_idempotency_key=req()['idempotency_key'], + ensure_correlation_id=req()['correlation_id'], delete_if_empty=True)) + require(result.status == 'groups_revoked_and_deleted') + require(snapshot(directory) == saved['before']) + foreign = store(replace(CONTEXT, principal='other-synthetic-principal')) + refused(lambda: foreign.get(req()['idempotency_key'], directory._group_binding(req()['groups'], req()['members'], req()['correlation_id']))) + return {'separate_process_postgres_restart_and_revoke': True, 'preexisting_shared_state_preserved': True, + 'foreign_context_receipt_refused': True} + + +def concurrency(): + binding = '4' * 64 + def reserve(_): + try: return store().reserve('synthetic-concurrent-reservation', binding) + except GroupProvenanceUnavailable: return None + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: + results = list(pool.map(reserve, range(24))) + created = [r[0] for r in results if r is not None and r[1]] + require(len(created) == 1) + receipt = created[0] + receipt['state'] = 'refused' + store().save(receipt) + stale = json.loads(json.dumps(receipt)) + store().save(receipt) + head = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: store().save(stale)) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == head) + with psycopg.connect(**info()) as db: + require(db.execute('SELECT count(*) FROM identity_provisioner.group_receipts WHERE key=%s', ('synthetic-concurrent-reservation',)).fetchone()[0] == 1) + return {'concurrent_original_reservation_exactly_once': True, 'stale_version_update_refused_without_head_advance': True} + + +def fork(): + # Clone actual PostgreSQL state at the exact same head/digest. Both callers + # must pass the old external check before either commits a divergent body. + with psycopg.connect(**{**info(admin=True), 'dbname': 'template1'}, autocommit=True) as db: + db.execute('CREATE DATABASE fixture_fork TEMPLATE postgres') + barrier = threading.Barrier(2) + class SimultaneousCheckpoint(FileCheckpoint): + def check(self, *args): + result = super().check(*args) + barrier.wait(timeout=10) + return result + def attempt(database): + candidate = store(database=database, checkpoint=SimultaneousCheckpoint()) + try: + record, fresh = candidate.reserve('synthetic-fork-' + database, '8' * 64) + require(fresh) + return database, record + except GroupProvenanceUnavailable: + return database, None + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(attempt, ('postgres', 'fixture_fork'))) + winners = [(database, record) for database, record in results if record is not None] + require(len(winners) == 1) + winner, record = winners[0] + loser = next(database for database, result in results if result is None) + markers = {} + for database in ('postgres', 'fixture_fork'): + with psycopg.connect(**info(admin=True, database=database)) as db: + markers[database] = db.execute('SELECT head,head_digest FROM identity_provisioner.group_journal_meta').fetchone() + require(markers[winner][0] == markers[loser][0] and markers[winner][1] != markers[loser][1]) + store(database=winner).check() + refused(lambda: store(database=loser).check()) + retained = json.loads((ROOT / 'checkpoint.json').read_text()) + require((retained['head'], retained['digest']) == markers[winner]) + # No directory call was attempted by this direct reservation fixture. Retain + # terminal refusal for that key before continuing against the winner only. + record['state'] = 'refused' + store(database=winner).save(record) + (ROOT / 'active-database').write_text(winner) + return {'concurrent_divergent_clones_same_counter_verified': True, + 'independent_exact_transition_cas_selects_one_winner': True, + 'reopened_loser_clone_refused_at_same_counter': True} + + +def restored(): + # Actual pg_restore has rolled SQL back while this checkpoint survived. + with psycopg.connect(**info()) as db: + epoch, directory, head, quarantine = db.execute('SELECT epoch::text,directory,head,quarantined FROM identity_provisioner.group_journal_meta').fetchone() + require(epoch == EPOCH and directory == CONTEXT.directory and quarantine is False) + require(head < json.loads((ROOT / 'checkpoint.json').read_text())['head']) + refused(lambda: store().check()) + refused(lambda: store().reserve('no-replay-after-restore', '6' * 64)) + return {'actual_pg_restore_behind_checkpoint_detected': True, 'restored_store_cannot_gain_replay_authority': True} + + +def lost_ack(): + class LoseAcknowledgment(FileCheckpoint): + def advance(self, *args): + super().advance(*args) + raise RuntimeError('synthetic-lost-ack-after-durable-checkpoint') + directory = Directory() + directory.group_provenance = store(checkpoint=LoseAcknowledgment()) + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(req(idempotency_key='synthetic-lost-sql-ack-key'))) + require(not directory.calls) + receipt = store().get('synthetic-lost-sql-ack-key', directory._group_binding(req()['groups'], req()['members'], req()['correlation_id'])) + require(receipt['state'] == 'pending') + refused(lambda: store().reserve('unrelated-after-unresolved', '7' * 64)) + # Definite no-directory-call refusal is disposed only inside this generated + # fixture, before moving to the next synthetic fault. No native recovery API. + receipt['state'] = 'refused' + store().save(receipt) + directory = Directory() + directory.group_provenance = store() + original = directory._gql + def lose_directory_response(token, query, variables): + result = original(token, query, variables) + if 'addUserToGroup' in query: + raise RuntimeError('synthetic-directory-response-lost') + return result + directory._gql = lose_directory_response + payload = req(idempotency_key='synthetic-lost-directory-response') + refused(lambda: directory.ensure_service_groups(payload)) + checked = snapshot(directory) + directory.group_provenance = store() + refused(lambda: directory.ensure_service_groups(payload)) + require(snapshot(directory) == checked) + record = store().get(payload['idempotency_key'], directory._group_binding(payload['groups'], payload['members'], payload['correlation_id'])) + require(record['state'] == 'uncertain' and record['intent']['operation'] == 'add_member') + class LostCommitReply: + def __init__(self): self.db = psycopg.connect(**info()) + def __enter__(self): self.db.__enter__(); return self + def __exit__(self, typ, value, traceback): + result = self.db.__exit__(typ, value, traceback) + if typ is None: raise OSError('synthetic-reply-lost-after-actual-sql-commit') + return result + def cursor(self): return self.db.cursor() + def close(self): self.db.close() + broken = PostgresGroupProvenance(connect=LostCommitReply, binding=CONTEXT, continuity=FileCheckpoint()) + checkpoint = json.loads((ROOT / 'checkpoint.json').read_text())['head'] + refused(lambda: broken.save(record)) + with psycopg.connect(**info()) as db: + committed_head = db.execute('SELECT head FROM identity_provisioner.group_journal_meta').fetchone()[0] + require(committed_head == checkpoint + 1) + require(json.loads((ROOT / 'checkpoint.json').read_text())['head'] == checkpoint) + refused(lambda: store().check()) + return {'lost_checkpoint_ack_retains_pending_reservation': True, 'reopened_uncertainty_refuses_ldap_effects': True, + 'active_fence_survives_process_reopen': True, 'lost_synthetic_directory_response_retains_postgres_intent': True, + 'lost_sql_commit_reply_retains_committed_head': True, 'reopened_stale_checkpoint_refuses_continuation': True} + + +def main(): + try: + require({p.name for p in Path('/sys/class/net').iterdir()} == {'lo'}) + require(len(Path('/proc/net/route').read_text().splitlines()) == 1) + checks = {'only_loopback_no_external_routes': True} + checks.update({'bootstrap': bootstrap, 'restart': restart, 'concurrency': concurrency, + 'fork': fork, 'restored': restored, 'lost-ack': lost_ack}[sys.argv[1]]()) + print(json.dumps({'status': 'passed', 'checks': checks, 'psycopg_version': psycopg.__version__, 'active_database': info()['dbname']})) + except Exception: + print(json.dumps({'status': 'failed', 'failure': 'synthetic_pg_oracle_failed', 'phase': sys.argv[1]})) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/producer-Containerfile.txt b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/producer-Containerfile.txt new file mode 100644 index 00000000..fe9b998b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/producer-Containerfile.txt @@ -0,0 +1,12 @@ +FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 + +RUN useradd --create-home --uid 10001 user-engine +WORKDIR /app +COPY pyproject.toml README.md /app/ +COPY src /app/src +RUN pip install --no-cache-dir ".[runtime]" + +USER 10001:10001 +EXPOSE 8080 +ENV PYTHONUNBUFFERED=1 +CMD ["python", "-m", "user_engine.runtime"] diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_driver.py new file mode 100644 index 00000000..8d486fde --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_driver.py @@ -0,0 +1,107 @@ +import contextlib +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import sys +import tarfile +import unittest +from unittest.mock import Mock, patch + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('group_fixture_driver', HERE / 'driver.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class PlanTests(unittest.TestCase): + def test_default_and_explicit_plan_do_not_load_containment_or_generate_material(self): + for args in ([], ['--plan']): + with patch.object(sys, 'argv', ['fixture', *args]), patch.object(f, 'load_containment') as load, patch.object(f, 'execute') as execute, patch.object(f.secrets, 'token_hex') as generate, contextlib.redirect_stdout(io.StringIO()) as out: + self.assertEqual(0, f.main()) + self.assertEqual('prepared_not_executed_not_approved', json.loads(out.getvalue())['status']) + load.assert_not_called(); execute.assert_not_called(); generate.assert_not_called() + + def test_wrong_manifest_digest_refuses_before_loading_helper(self): + with patch.object(sys, 'argv', ['fixture', '--execute-reviewed-fixture', '--expected-manifest-sha256', '0' * 64]), patch.object(f, 'load_containment') as load: + with self.assertRaises(RuntimeError): + f.main() + load.assert_not_called() + + def test_manifest_exact_source_closure_and_images(self): + manifest = f.reviewed_manifest(hashlib.sha256(f.MANIFEST.read_bytes()).hexdigest()) + self.assertEqual('unix:///var/run/docker.sock', manifest['fixed_endpoint']) + self.assertEqual('never', manifest['pull_policy']) + self.assertEqual('network-none', manifest['network']) + self.assertEqual(0, manifest['limits']['host_ports']) + self.assertEqual(0, manifest['limits']['host_mounts']) + self.assertEqual(2, manifest['limits']['max_service_containers']) + self.assertIn('sha256:', manifest['postgres_image']) + self.assertIn('sha256:', manifest['python_image']) + + def test_exact_fresh_names_no_host_network_ports_or_mounts(self): + manifest = json.loads(f.MANIFEST.read_text()) + session = f.session_plan(manifest, 'f' * 32) + self.assertEqual(2, len(set(session['containers']))) + for name in session['containers']: + args = f.base_args(session, name, 'none') + self.assertIn('--pull=never', args) + self.assertIn('--read-only', args) + self.assertIn('--log-driver', args) + self.assertIn('net-kingdom.recovery-run=' + 'f' * 32, args) + self.assertFalse({'--publish', '-p', '--volume', '-v', '--privileged', '--network=host'} & set(args)) + with self.assertRaises(RuntimeError): + f.session_plan(manifest, 'not-a-fresh-run') + + def test_tar_private_only_generated_named_material(self): + raw = f.tar_bytes({'password': b'synthetic-only', 'lldap.toml': b'synthetic-config'}) + with tarfile.open(fileobj=io.BytesIO(raw)) as archive: + self.assertTrue(all(member.uid == member.gid == 1000 and member.mode == 0o600 for member in archive)) + for path in ('../outside', '/absolute', 'path/../../escape'): + with self.assertRaises(RuntimeError): + f.tar_bytes({path: b'synthetic'}) + + def test_failure_still_invokes_existing_exact_cleanup(self): + manifest = json.loads(f.MANIFEST.read_text()) + adapter = Mock() + adapter.command.side_effect = RuntimeError('synthetic transport fault') + result = {'status': 'failed'} + with self.assertRaises(RuntimeError): + f.execute(manifest, adapter, result) + adapter.cleanup.assert_called_once() + session = adapter.cleanup.call_args[0][0] + self.assertEqual(session['containers'], result['session']['containers']) + self.assertTrue(result['daemon_request_uncertain']) + self.assertEqual('failed', result['status']) + + def test_every_declared_source_input_exists_and_schema_does_not_admit_epoch(self): + source = HERE.parent / 'postimage/identity-provisioner' + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', 'migrations/group_journal_postgres_v1.sql'): + self.assertTrue((source / name).is_file()) + self.assertTrue((HERE / 'test_service_groups.py').is_file()) + schema = (source / 'migrations/group_journal_postgres_v1.sql').read_text() + self.assertNotIn('INSERT INTO', schema) + self.assertIn('DEFAULT true', schema) + self.assertIn('active boolean UNIQUE', schema) + + def test_exact_producer_interpreter_has_no_path_or_venv_fallback(self): + manifest = json.loads(f.MANIFEST.read_text()) + self.assertEqual('/usr/local/bin/python3', f.helper_python(manifest)) + for value in ('python3', '/opt/venv/bin/python', '/usr/bin/python3', None): + with self.assertRaises(RuntimeError): + f.helper_python({**manifest, 'helper_interpreter': value}) + compile(f.INTERPRETER_PROBE, 'public-interpreter-probe', 'exec') + source = (HERE / 'driver.py').read_text() + self.assertIn("'helper_interpreter_verified_before_candidate_import'", source) + self.assertNotIn("'--entrypoint', 'python3'", source) + + def test_probe_syntax_and_no_network_expansion(self): + compile((HERE / 'probe.py').read_text(), str(HERE / 'probe.py'), 'exec') + compile(f.IMPORT, 'private-import', 'exec') + self.assertNotIn('subprocess', (HERE / 'probe.py').read_text()) + self.assertIn("'host': '127.0.0.1'", (HERE / 'probe.py').read_text()) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_service_groups.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_service_groups.py new file mode 100644 index 00000000..c6dd48bd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r2/test_service_groups.py @@ -0,0 +1,140 @@ +import unittest +import tempfile +from pathlib import Path + +from group_provenance import GroupProvenance + +from provisioner import LLDAPProvisioner, dispatch + + +class Directory(LLDAPProvisioner): + """In-memory LLDAP: users, groups and memberships.""" + + def __init__(self): + self.storage = tempfile.TemporaryDirectory() + self.group_provenance = GroupProvenance(str(Path(self.storage.name) / "groups.db"), initialize=True) + self.users = {"tegwick", "agent-codex"} + self.groups = {"lldap_admin": 1, "tenant:trial:one:users": 2, "forge-admins": 3} + self.members = {1: {"tegwick"}, 2: set(), 3: set()} + self.calls = [] + self.drop_add = False + + def _login(self): + return "test-token" + + def _directory(self, token): + return ([{"id": u} for u in sorted(self.users)], + [{"id": i, "displayName": n} for n, i in self.groups.items()]) + + def _user(self, token, subject): + return {"id": subject, "groups": [{"id": i, "displayName": n} for n, i in self.groups.items() + if subject in self.members[i]]} + + def _ensure_group(self, token, groups, name): + if name not in self.groups: + self.calls.append(("create", name)) + self.groups[name] = max(self.groups.values()) + 1 + self.members[self.groups[name]] = set() + return self.groups[name] + + def _add_group(self, token, username, group_id): + self.calls.append(("add", username, group_id)) + if not self.drop_add: + self.members[group_id].add(username) + + def _remove_group(self, token, username, group_id): + self.calls.append(("remove", username, group_id)) + self.members[group_id].discard(username) + + def _group_members(self, token, group_id): + return sorted(self.members[group_id]) + + def _gql(self, token, query, variables): + if "addUserToGroup" in query: + self._add_group(token, variables["userId"], variables["groupId"]) + return {"addUserToGroup": {"ok": True}} + if "deleteGroup" in query: + self.calls.append(("delete", variables["id"])) + name = next(n for n, i in self.groups.items() if i == variables["id"]) + del self.groups[name] + return {"deleteGroup": {"ok": True}} + raise AssertionError(query) + + +def req(**kw): + p = {"groups": ["forge-admins", "forge-coulomb-owners", "forge-coulomb-readers"], + "members": {"forge-admins": ["tegwick"], "forge-coulomb-owners": ["tegwick"]}, + "idempotency_key": "forgejo-netkingdom-oidc-1", "correlation_id": "test"} + p.update(kw) + return p + + +class EnsureTests(unittest.TestCase): + def test_creates_missing_groups_and_adds_members(self): + d = Directory() + r = dispatch(d, "/v1/groups/ensure", req()) + self.assertEqual("groups_ensured", r.status) + self.assertFalse(r.resumed) + self.assertEqual({"forge-coulomb-owners", "forge-coulomb-readers"}, + {c[1] for c in d.calls if c[0] == "create"}) + self.assertIn("tegwick", d.members[d.groups["forge-admins"]]) + self.assertIn("tegwick", d.members[d.groups["forge-coulomb-owners"]]) + + def test_idempotent_and_reports_resumed(self): + d = Directory() + dispatch(d, "/v1/groups/ensure", req()) + d.calls.clear() + r = dispatch(d, "/v1/groups/ensure", req()) + self.assertTrue(r.resumed) + self.assertEqual([], [c for c in d.calls if c[0] == "create"]) + + def test_never_removes_other_memberships(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + self.assertIn("agent-codex", d.members[3]) + self.assertEqual([], [c for c in d.calls if c[0] == "remove"]) + + def test_reserved_and_invalid_requests_change_nothing(self): + bad = [req(groups=["lldap_admin"]), req(groups=["tenant-x"]), req(groups=["netkingdom-suspended"]), + req(groups=["Forge Admins"]), req(groups=[]), + req(members={"not-listed": ["tegwick"]}), + req(members={"forge-admins": ["Not A User"]}), + req(members={"forge-admins": ["nobody"]})] + for payload in bad: + d = Directory() + with self.assertRaises(ValueError, msg=payload): + dispatch(d, "/v1/groups/ensure", payload) + self.assertEqual([], d.calls, payload) + + def test_failed_readback_never_reports_success(self): + d = Directory() + d.drop_add = True + with self.assertRaises(RuntimeError): + dispatch(d, "/v1/groups/ensure", req()) + + +class RevokeTests(unittest.TestCase): + def test_removes_only_listed_memberships_and_keeps_groups_by_default(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + r = dispatch(d, "/v1/groups/revoke", req(ensure_idempotency_key=req()["idempotency_key"], ensure_correlation_id=req()["correlation_id"])) + self.assertEqual("groups_revoked", r.status) + self.assertEqual({"agent-codex"}, d.members[d.groups["forge-admins"]]) + self.assertIn("forge-coulomb-owners", d.groups) + + def test_deletes_only_empty_listed_groups_when_asked(self): + d = Directory() + d.members[3].add("agent-codex") + dispatch(d, "/v1/groups/ensure", req()) + r = dispatch(d, "/v1/groups/revoke", req(delete_if_empty=True, ensure_idempotency_key=req()["idempotency_key"], ensure_correlation_id=req()["correlation_id"])) + self.assertEqual("groups_revoked_and_deleted", r.status) + self.assertIn("forge-admins", d.groups) + self.assertNotIn("forge-coulomb-owners", d.groups) + self.assertNotIn("forge-coulomb-readers", d.groups) + self.assertIn("lldap_admin", d.groups) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/README.md b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/README.md new file mode 100644 index 00000000..c41549bd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/README.md @@ -0,0 +1,93 @@ +# Prepared disposable PostgreSQL compatibility fixture + +Status: prepared and unit checked; not executed or approved. Default +`python3 driver.py --plan` reads only the manifest, without loading containment, +generating material or calling Docker. Execution requires independent review of +the exact manifest/source closure and explicit execution flag/digest. The flag +and fixture files confer no authority. + +The fixture reuses the existing `isolated-keycape.py` command/environment guard, +containment inspector and exact name/label cleanup. A network-none PostgreSQL +anchor and a pinned cached User Engine Python helper share only loopback. Both +are nonroot/read-only/capability-free, with private bounded tmpfs, no host ports, +mounts or Docker socket, and no pulls/builds/packages. Generated admin/runtime +passwords enter through stdin only. Image-specific nonroot UID999/Python/psycopg +compatibility is an explicit precondition checked by execution; no cache or +runtime claim is made during preparation. Any incompatible image/anonymous +volume/error fails closed and still invokes exact cleanup. + +Six sequential Python phases use the actual candidate SQL backend and its +normal provisioner path with the **existing synthetic LDAP Directory**. Bootstrap +runs the real migration on generated state, checks runtime DDL denial and exact +created-only inventories. A separate process reopens and replays/revokes while +preserving the complete synthetic prestate. Concurrent reservations must yield +one original receipt; stale row CAS cannot advance its head. Two actual database +clones then pass the same old checkpoint check concurrently and commit different +receipts at the same next counter. Only one external digest CAS may win; a fresh +backend on the losing clone must refuse even at that same counter. Lost checkpoint +acknowledgment, lost synthetic directory response and a lost reply after actual +SQL commit must retain pending/uncertain state and refuse continuation. An actual +pg_dump taken after bootstrap is then pg_restored after later journal changes; +the external test checkpoint survives. Runtime SQL SELECT must work and show an +older restored head before the candidate's restore-quarantine refusal oracle can +pass. This prevents an unrelated connection/ACL failure from masquerading as +restore protection. + +All fresh names/labels are registered before create. `finally` uses the existing +cleanup helper for both resources, requires successful exact absence inventories +and retains any Docker uncertainty as failed. PID1 lifetimes and RPC/execution +limits are bounded in the manifest. This is not a parent-death/reaper proof. +Only safe booleans, public image/source/version pins and cleanup duties leave in +a private `/tmp/nk48-postgres-native-review-*/result.json`; synthetic backup bytes +are bounded in host memory and never written as an archive file. + +The test checkpoint implementation belongs only to this fixture. Its independent +file survives PostgreSQL restore and uses file locking/fsync; it is not offered +as native custody, case authority, a journal-recovery API or an evidence broker. +The fixture explicitly disposes one definitely pre-LDAP synthetic reservation to +move between fault cases; it does not clear an unknown native mutation. All +source/fixture passes remain distinct from live CNPG/lease/TLS/backup continuity, +protected owner callbacks, actual authoritative tenant scope, native LDAP fencing +and production acceptance. + + +## R2 cached helper selection + +The reviewed R1 preflight failed before `execute`, session allocation or private +import because its pinned helper image 1e6547 was absent. No pull or override was +used. Frozen manifest/driver/probe/source and the coordinator's structured failure +are retained under `revisions/r1`. There is no raw stderr file; the original +traceback was returned in tool output only, so no byte-exact stderr artifact is +invented. + +The proposed R2 helper is the coordinator-observed cached f239 image, bound by +existing published receipts to User Engine source 1bb7cacc. That source uses +system pip on official Python 3.12, not a virtualenv. R2 therefore binds every +helper launch/exec to `/usr/local/bin/python3`; it has no PATH or alternate-venv +fallback. A closed in-container probe must verify the exact executable and import +psycopg 3 before candidate source import, reporting the bundled version. Missing +or mismatched interpreter/dependency refuses and preserves exact cleanup duties. +No container count, limit, privilege, network or cleanup expansion is proposed. +Historical producer or rollout acceptance is not a new image/dependency grant. +See `helper-selection-r2.json`, `manifest-delta-r2.json` and `preparation-r2.json`. + + +## R3 bounded failure diagnostics + +R2 executed and failed after containment and interpreter verification (bundled +psycopg 3.3.6); all phase checks were empty. Its unchanged receipt and manifest +are preserved in `revisions/r2`. The old driver discarded a failed helper result, +so the exact failing subphase is unknown. Bootstrap is not asserted as the cause. + +R3 adds fixed public driver/RPC markers and helper NDJSON markers, retained before +an oracle raises. PostgreSQL initdb now completes synchronously with only a fixed +boolean result before the detached server launch. Bootstrap separately identifies +connection readiness, migration, epoch insertion, role/grants, checkpoint and +ensure/readback. Failures expose only an allowlisted exception class and optional +five-character SQLSTATE, including a wrapped cause; never exception messages, +SQL/DSN/password values, command arguments or database logs. Public frames are +bounded to 32768 bytes and 96 lines; RPC trace is bounded to 192 fixed rows. The +safe result is checkpointed before/after each RPC and on helper failure. This +remains a sequential fixture, with unchanged images, authority, backend source, +oracles, container/network limits and exact cleanup requirements. No retry or +runtime execution is authorized by this diagnostic revision. diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/diagnostic_protocol.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/diagnostic_protocol.py new file mode 100644 index 00000000..d6f8107a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/diagnostic_protocol.py @@ -0,0 +1,86 @@ +"""Closed public fixture diagnostics. Never serialize exception text or SQL values.""" +import json +import re + +DRIVER_STAGES = ('session_registered', 'anchor_launch', 'anchor_containment', 'postgres_private_import', + 'postgres_initdb', 'postgres_launch', 'anchor_identity', 'helper_launch', 'full_containment', + 'helper_interpreter', 'candidate_import', 'bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', + 'restored', 'database_dump', 'database_restore', 'cleanup', 'complete') +PROBE_STAGES = ('probe_entry', 'loopback_check', 'phase_dispatch', 'bootstrap_connect_wait', + 'bootstrap_connected', 'bootstrap_schema', 'bootstrap_migration', 'bootstrap_epoch_insert', + 'bootstrap_role_create', 'bootstrap_runtime_grants', 'bootstrap_checkpoint_init', + 'bootstrap_directory_init', 'bootstrap_ensure', 'bootstrap_ensure_readback', + 'bootstrap_runtime_ddl_refusal', 'bootstrap_complete', 'phase_complete') +ERROR_CLASSES = ('GroupProvenanceUnavailable', 'RuntimeError', 'ValueError', 'TypeError', 'KeyError', + 'FileNotFoundError', 'PermissionError', 'OperationalError', 'ProgrammingError', 'IntegrityError', + 'InsufficientPrivilege', 'InvalidPassword', 'UndefinedTable', 'UndefinedColumn', 'SyntaxError', + 'DuplicateTable', 'DuplicateObject', 'InvalidSchemaName', 'DeadlockDetected', 'ConnectionException', + 'InvalidAuthorizationSpecification', 'OSError', 'TimeoutExpired', 'ImportError', + 'ModuleNotFoundError', 'JSONDecodeError', 'OtherError') + + +def error_metadata(error, stage): + if stage not in DRIVER_STAGES + PROBE_STAGES: + stage = 'phase_dispatch' + name = type(error).__name__ + name = name if name in ERROR_CLASSES else 'OtherError' + state = None + current = error + for _ in range(8): + if current is None: + break + try: + candidate = getattr(current, 'sqlstate', None) + if isinstance(candidate, str) and re.fullmatch('[0-9A-Z]{5}', candidate): + state = candidate + break + current = current.__cause__ or current.__context__ + except Exception: + break + return {'stage': stage, 'exception_class': name, 'sqlstate': state} + + +def emit_stage(stage): + if stage not in PROBE_STAGES: + raise RuntimeError('invalid_public_fixture_stage') + print(json.dumps({'event': 'stage', 'stage': stage}), flush=True) + + +def parse_frames(raw, *, require_final=True): + if isinstance(raw, bytes): + raw = raw.decode('utf-8') + if not isinstance(raw, str) or len(raw) > 32768: + raise RuntimeError('invalid_public_fixture_diagnostics') + stages, final = [], None + lines = raw.splitlines() + if len(lines) > 96: + raise RuntimeError('invalid_public_fixture_diagnostics') + for line in lines: + value = json.loads(line) + if not isinstance(value, dict) or final is not None: + raise RuntimeError('invalid_public_fixture_diagnostics') + if value.get('event') == 'stage': + if set(value) != {'event', 'stage'} or value['stage'] not in PROBE_STAGES: + raise RuntimeError('invalid_public_fixture_diagnostics') + stages.append(value['stage']) + elif value.get('status') in {'passed', 'failed'} and final is None: + if value['status'] == 'failed': + if set(value) != {'status', 'failure', 'diagnostic'} or value['failure'] != 'synthetic_pg_oracle_failed': + raise RuntimeError('invalid_public_fixture_diagnostics') + diagnostic = value['diagnostic'] + if set(diagnostic) != {'stage', 'exception_class', 'sqlstate'} or diagnostic['stage'] not in PROBE_STAGES or diagnostic['exception_class'] not in ERROR_CLASSES: + raise RuntimeError('invalid_public_fixture_diagnostics') + if diagnostic['sqlstate'] is not None and not re.fullmatch('[0-9A-Z]{5}', str(diagnostic['sqlstate'])): + raise RuntimeError('invalid_public_fixture_diagnostics') + elif (set(value) != {'status', 'checks', 'psycopg_version', 'active_database'} + or not isinstance(value['checks'], dict) + or not isinstance(value['psycopg_version'], str) + or not re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', value['psycopg_version']) + or value['active_database'] not in {'postgres', 'fixture_fork'}): + raise RuntimeError('invalid_public_fixture_diagnostics') + final = value + else: + raise RuntimeError('invalid_public_fixture_diagnostics') + if require_final and final is None: + raise RuntimeError('missing_public_fixture_result') + return stages, final diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/driver.py b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/driver.py new file mode 100644 index 00000000..23467e01 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/driver.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""Default is a read-only plan. Docker execution awaits independent exact review.""" +import argparse +import base64 +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import secrets +import tarfile +import tempfile +import time +import uuid + +HERE = Path(__file__).resolve().parent +MANIFEST = HERE / 'manifest.json' + +# Fixed local diagnostic parser; no dependency or credential discovery. +_spec = importlib.util.spec_from_file_location('fixture_diagnostic_protocol', HERE / 'diagnostic_protocol.py') +diagnostics = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(diagnostics) + + +def require(condition): + if not condition: + raise RuntimeError('bounded_synthetic_fixture_refused') + + +def reviewed_manifest(expected): + raw = MANIFEST.read_bytes() + require(expected == hashlib.sha256(raw).hexdigest()) + manifest = json.loads(raw) + for path, digest in manifest['source_sha256'].items(): + require(bool(re.fullmatch(r'sha256:[0-9a-f]{64}', digest))) + require('sha256:' + hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest) + return manifest + + +def load_containment(manifest): + spec = importlib.util.spec_from_file_location('reviewed_existing_containment', manifest['containment_helper']) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def session_plan(manifest, run_id): + require(bool(re.fullmatch(r'[a-f0-9]{32}', run_id))) + anchor = 'nk49-kc-' + run_id + '-journal-postgres' + probe = 'nk49-kc-' + run_id + '-journal-probe' + return {'run_id': run_id, 'containers': [anchor, probe], 'worker': anchor, 'issuer': probe, + 'images': {anchor: manifest['postgres_image'], probe: manifest['python_image']}, 'volumes': {}} + + +def base_args(session, name, network, user="1000:1000"): + uid, gid = user.split(":") + return ['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', network, + '--label', 'net-kingdom.recovery-run=' + session['run_id'], '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--user', user, '--memory', '384m', '--cpus', '0.5', + '--pids-limit', '64', '--log-driver', 'none', '--restart', 'no', + '--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=' + uid + ',gid=' + gid + ',mode=0700'] + + +def tar_bytes(files): + out = io.BytesIO() + with tarfile.open(fileobj=out, mode='w') as archive: + for name, value in files.items(): + require(bool(re.fullmatch(r'[a-zA-Z0-9._/-]+', name)) and not name.startswith('/') and '..' not in Path(name).parts) + entry = tarfile.TarInfo(name) + entry.size = len(value) + entry.mode = 0o600 + entry.uid = entry.gid = 1000 + archive.addfile(entry, io.BytesIO(value)) + return out.getvalue() + + +IMPORT = r''' +import base64,json,os,sys +from pathlib import Path +os.umask(0o077) +root=Path('/fixture') +(root/'source').mkdir(mode=0o700) +(root/'source/migrations').mkdir(mode=0o700) +(root/'source/tests').mkdir(mode=0o700) +for name,encoded in json.load(sys.stdin).items(): + if name not in ('source/provisioner.py','source/group_provenance.py','source/group_provenance_postgres.py','source/migrations/group_journal_postgres_v1.sql','source/tests/test_service_groups.py','probe.py','private.json','diagnostic_protocol.py'):raise ValueError() + fd=os.open(root/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as output:output.write(base64.b64decode(encoded,validate=True)) +''' + + +def helper_python(manifest): + # Exact producer source installs into official Python's system prefix. + # Never fall back to PATH or another interpreter/virtual environment. + require(manifest.get('helper_interpreter') == '/usr/local/bin/python3') + return manifest['helper_interpreter'] + + +INTERPRETER_PROBE = r""" +import json,sys +try: + import psycopg + valid=sys.executable=='/usr/local/bin/python3' and psycopg.__version__.split('.')[0]=='3' + print(json.dumps({'helper_ready':valid,'executable':sys.executable,'psycopg_version':psycopg.__version__})) +except Exception: + print(json.dumps({'helper_ready':False})) +""" + + +def preflight(adapter, manifest): + helper_python(manifest) + adapter.docker_environment() # Refuse ambient alternate routing before calls. + contexts = json.loads(adapter.command(['docker', 'context', 'inspect'], timeout=15)) + require(len(contexts) == 1 and contexts[0]['Endpoints']['docker']['Host'] == manifest['fixed_endpoint']) + facts = json.loads(adapter.command(['docker', 'info', '--format', + '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}'], timeout=15)) + require(facts == {'ID': manifest['engine_id'], 'Name': manifest['engine_name'], 'OSType': 'linux'}) + for image in (manifest['postgres_image'], manifest['python_image']): + value = adapter.command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image], timeout=15).decode().strip() + require(bool(re.fullmatch(r'sha256:[a-f0-9]{64}', value))) + + +def retain_helper_result(result, phase, raw, publish): + stages, value = diagnostics.parse_frames(raw) + safe = {'status': value['status'], 'stages': stages} + if value['status'] == 'failed': + safe['diagnostic'] = value['diagnostic'] + result.setdefault('helper_diagnostics', {})[phase] = safe + publish() # Persist failure evidence before the phase oracle can raise. + return value + + +def execute(manifest, adapter, result, checkpoint=None): + session = session_plan(manifest, uuid.uuid4().hex) + result['session'] = {'run_id': session['run_id'], 'containers': session['containers'], + 'label': 'net-kingdom.recovery-run=' + session['run_id']} + python = helper_python(manifest) + deadline = time.monotonic() + 210 + original_command = adapter.command + def publish(): + if checkpoint is not None: + try: checkpoint() + except OSError: result['receipt_checkpoint_failed'] = True + def mark(stage): + require(stage in diagnostics.DRIVER_STAGES) + result['driver_stage'] = stage + publish() + def event(argv): + require(len(result.setdefault('rpc_trace', [])) < 192) + operation = argv[1] if len(argv) > 1 and argv[1] in {'run','exec','rm','container','context','image','info','volume'} else 'other' + row = {'stage': result['driver_stage'], 'operation': operation, 'status': 'started'} + result['rpc_trace'].append(row) + publish() + return row + def run(argv, **kwargs): + row = event(argv) + remaining = deadline - time.monotonic() + require(remaining > 0) + try: + value = original_command(argv, timeout=min(50, remaining), **kwargs) + row['status'] = 'completed' + publish() + return value + except BaseException as error: + row['status'] = 'failed' + result['daemon_request_uncertain'] = True + result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage'])) + # Timeout output can contain complete public progress frames. Never + # retain raw stdout/stderr or command/exception text. + if result['driver_stage'] in manifest['phase_oracles']: + partial = getattr(error, 'stdout', None) + if partial: + try: + stages, _ = diagnostics.parse_frames(partial, require_final=False) + result.setdefault('helper_diagnostics', {})[result['driver_stage']] = {'status': 'interrupted', 'stages': stages} + except Exception: + pass + publish() + raise + mark('session_registered') + adapter.command = run + try: + anchor = session['worker'] + args = base_args(session, anchor, 'none', user='999:999') + [ + '--tmpfs', '/var/lib/postgresql:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=134217728,uid=999,gid=999,mode=0700', + '--tmpfs', '/run/fixture:rw,nosuid,nodev,noexec,size=1048576,uid=999,gid=999,mode=0700', + '--env', 'PGDATA=/var/lib/postgresql/data', '--entrypoint', '/bin/sh', + manifest['postgres_image'], '-c', 'sleep 240'] + mark('anchor_launch') + run(args) + mark('anchor_containment') + adapter.inspect_session({**session, 'containers': [anchor], 'issuer': anchor}, worker_only=True) + admin_password, runtime_password = secrets.token_hex(24), secrets.token_hex(24) + mark('postgres_private_import') + run(['docker', 'exec', '-i', anchor, 'tar', '-xf', '-', '-C', '/run/fixture'], + input=tar_bytes({'password': admin_password.encode()})) + mark('postgres_initdb') + initialized = json.loads(run(['docker', 'exec', anchor, '/bin/sh', '-c', + "if initdb -D /var/lib/postgresql/data --username=fixture_admin --auth-local=trust --auth-host=scram-sha-256 --pwfile=/run/fixture/password > /dev/null 2>&1; then echo '{\"postgres_initdb\":true}'; else echo '{\"postgres_initdb\":false}'; fi"], max_output=256)) + result['postgres_initdb_complete'] = initialized == {'postgres_initdb': True} + publish() + require(result['postgres_initdb_complete']) + mark('postgres_launch') + run(['docker', 'exec', '--detach', anchor, '/bin/sh', '-c', + 'exec postgres -D /var/lib/postgresql/data -c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/fixture > /dev/null 2>&1']) + mark('anchor_identity') + anchor_id = run(['docker', 'container', 'inspect', anchor, '--format', '{{.Id}}']).decode().strip() + require(bool(re.fullmatch(r'[a-f0-9]{64}', anchor_id))) + args = base_args(session, session['issuer'], 'container:' + anchor_id) + [ + '--tmpfs', '/fixture:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700', + '--entrypoint', python, manifest['python_image'], '-c', 'import time;time.sleep(240)'] + mark('helper_launch') + run(args) + mark('full_containment') + adapter.inspect_session(session, worker_only=True) + result['containment_verified_before_generated_private_import'] = True + mark('helper_interpreter') + interpreter = json.loads(run(['docker', 'exec', session['issuer'], python, '-c', INTERPRETER_PROBE], max_output=4096)) + require(interpreter.get('helper_ready') is True and interpreter.get('executable') == python) + require(isinstance(interpreter.get('psycopg_version'), str) and re.fullmatch(r'3\.[0-9]+\.[0-9]+(?:[a-z]+[0-9]+)?', interpreter['psycopg_version'])) + result['helper_interpreter_verified_before_candidate_import'] = True + result['helper_psycopg_version'] = interpreter['psycopg_version'] + files = {'source/' + name: (HERE.parent / 'postimage/identity-provisioner' / name).read_bytes() + for name in ('provisioner.py', 'group_provenance.py', 'group_provenance_postgres.py', + 'migrations/group_journal_postgres_v1.sql')} + files['source/tests/test_service_groups.py'] = (HERE / 'test_service_groups.py').read_bytes() + files.update({'probe.py': (HERE / 'probe.py').read_bytes(), 'diagnostic_protocol.py': (HERE / 'diagnostic_protocol.py').read_bytes(), 'private.json': json.dumps({ + 'admin_password': admin_password, 'runtime_password': runtime_password}).encode()}) + mark('candidate_import') + run(['docker', 'exec', '-i', session['issuer'], python, '-c', IMPORT], + input=json.dumps({k: base64.b64encode(v).decode() for k, v in files.items()}).encode()) + result['checks'] = {} + backup = None + active_database = 'postgres' + for phase in ('bootstrap', 'restart', 'concurrency', 'fork', 'lost-ack', 'restored'): + if phase == 'restored': + require(backup is not None) + mark('database_restore') + run(['docker', 'exec', '-i', anchor, 'pg_restore', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', active_database, '--clean', '--if-exists', '--single-transaction', '--exit-on-error', '--no-owner'], input=backup) + mark(phase) + raw = run(['docker', 'exec', session['issuer'], python, '/fixture/probe.py', phase], max_output=32768) + value = retain_helper_result(result, phase, raw, publish) + expected = set(manifest['phase_oracles'][phase]) + require(value.get('status') == 'passed' and set(value.get('checks', {})) == expected) + require(all(v is True for v in value['checks'].values())) + result['checks'].update(value['checks']) + result['psycopg_version'] = value['psycopg_version'] + require(value.get('active_database') in {'postgres', 'fixture_fork'}) + active_database = value['active_database'] + if phase == 'bootstrap': + mark('database_dump') + backup = run(['docker', 'exec', anchor, 'pg_dump', '--host', '/run/fixture', '-U', 'fixture_admin', + '--dbname', 'postgres', '--format=custom', '--no-owner'], max_output=4194304) + result['status'] = 'synthetic_postgres_journal_compatibility_passed' + mark('complete') + except BaseException as error: + result.setdefault('driver_failure', diagnostics.error_metadata(error, result['driver_stage'])) + publish() + raise + finally: + mark('cleanup') + def cleanup_command(argv, **kwargs): + row = event(argv) + try: + value = original_command(argv, timeout=15, **kwargs) + row['status'] = 'completed' + publish() + return value + except BaseException: + result['daemon_request_uncertain'] = True + row['status'] = 'failed' + publish() + raise + adapter.command = cleanup_command + try: + adapter.cleanup(session, result) + result['container_cleanup_verified'] = True + except BaseException: + result['status'] = 'failed' + result['container_cleanup_verified'] = False + adapter.command = original_command + publish() + if result.get('daemon_request_uncertain') or result.get('receipt_checkpoint_failed'): + result['status'] = 'failed' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--plan', action='store_true') + parser.add_argument('--execute-reviewed-fixture', action='store_true') + parser.add_argument('--expected-manifest-sha256') + args = parser.parse_args() + if not args.execute_reviewed_fixture: + print(MANIFEST.read_text()) + return 0 + require(not args.plan) + manifest = reviewed_manifest(args.expected_manifest_sha256) + adapter = load_containment(manifest) + preflight(adapter, manifest) + os.umask(0o077) + output = Path(tempfile.mkdtemp(prefix='nk48-postgres-native-review-', dir='/tmp')) / 'result.json' + result = {'schema': 'nk48.synthetic-postgres-journal-result.v1', 'status': 'failed', + 'manifest_sha256': args.expected_manifest_sha256, 'production_contacted': False, + 'actual_credentials_used': False, 'native_acceptance_claimed': False} + try: + execute(manifest, adapter, result, checkpoint=lambda: output.write_text(json.dumps(result, indent=2) + '\n')) + except BaseException: + result['status'] = 'failed' + finally: + output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({'status': result['status'], 'safe_result': str(output)})) + return int(result['status'] != 'synthetic_postgres_journal_compatibility_passed') + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/execution-result.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/execution-result.json new file mode 100644 index 00000000..1879da6d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/execution-result.json @@ -0,0 +1,195 @@ +{ + "schema": "nk48.synthetic-postgres-journal-result.v1", + "status": "failed", + "manifest_sha256": "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093", + "production_contacted": false, + "actual_credentials_used": false, + "native_acceptance_claimed": false, + "session": { + "run_id": "a20a5eed1ee54968b61e8a1cb2043b30", + "containers": [ + "nk49-kc-a20a5eed1ee54968b61e8a1cb2043b30-journal-postgres", + "nk49-kc-a20a5eed1ee54968b61e8a1cb2043b30-journal-probe" + ], + "label": "net-kingdom.recovery-run=a20a5eed1ee54968b61e8a1cb2043b30" + }, + "driver_stage": "cleanup", + "rpc_trace": [ + { + "stage": "anchor_launch", + "operation": "run", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "context", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "info", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "anchor_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "postgres_private_import", + "operation": "exec", + "status": "completed" + }, + { + "stage": "postgres_initdb", + "operation": "exec", + "status": "completed" + }, + { + "stage": "postgres_launch", + "operation": "exec", + "status": "completed" + }, + { + "stage": "anchor_identity", + "operation": "container", + "status": "completed" + }, + { + "stage": "helper_launch", + "operation": "run", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "context", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "info", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "full_containment", + "operation": "container", + "status": "completed" + }, + { + "stage": "helper_interpreter", + "operation": "exec", + "status": "completed" + }, + { + "stage": "candidate_import", + "operation": "exec", + "status": "completed" + }, + { + "stage": "bootstrap", + "operation": "exec", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "rm", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "rm", + "status": "completed" + }, + { + "stage": "cleanup", + "operation": "container", + "status": "completed" + } + ], + "postgres_initdb_complete": true, + "containment_verified_before_generated_private_import": true, + "helper_interpreter_verified_before_candidate_import": true, + "helper_psycopg_version": "3.3.6", + "checks": {}, + "helper_diagnostics": { + "bootstrap": { + "status": "failed", + "stages": [ + "probe_entry", + "loopback_check", + "phase_dispatch", + "bootstrap_connect_wait", + "bootstrap_connected", + "bootstrap_schema", + "bootstrap_migration", + "bootstrap_epoch_insert", + "bootstrap_role_create", + "bootstrap_runtime_grants", + "bootstrap_checkpoint_init", + "bootstrap_directory_init", + "bootstrap_ensure" + ], + "diagnostic": { + "stage": "bootstrap_ensure", + "exception_class": "RuntimeError", + "sqlstate": null + } + } + }, + "driver_failure": { + "stage": "bootstrap", + "exception_class": "RuntimeError", + "sqlstate": null + }, + "container_cleanup_obligations": [ + { + "container": "nk49-kc-a20a5eed1ee54968b61e8a1cb2043b30-journal-probe", + "status": "absence_verified" + }, + { + "container": "nk49-kc-a20a5eed1ee54968b61e8a1cb2043b30-journal-postgres", + "status": "absence_verified" + } + ], + "container_cleanup_verified": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/helper-selection-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/helper-selection-r2.json new file mode 100644 index 00000000..608d1952 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/helper-selection-r2.json @@ -0,0 +1,32 @@ +{ + "status": "fixture_helper_successor_proposed_not_executed", + "selected_cached_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "producer_revision": "1bb7cacc98479f56f6b1c28c000479efd260a944", + "producer_containerfile_sha256": "668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a", + "producer_containerfile_public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/1bb7cacc98479f56f6b1c28c000479efd260a944/Containerfile", + "provenance_sources": [ + { + "repo": "rapp-user-engine", + "path": "docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json", + "revision": "a42c68f8701880bfeb93e28dd812fcf3691568a3", + "sha256": "2f84959e36d442860fb2b5a5817bb707c4f8372797a3ef8dd0d5cab956af86ce", + "public_source": "https://forgejo.coulomb.social/coulomb/rapp-user-engine/src/commit/a42c68f8701880bfeb93e28dd812fcf3691568a3/docs/evidence/2026-10-11-published-readback-proposal/published-receipt.json" + }, + { + "repo": "user-engine", + "path": "docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json", + "revision": "9557d6f867fe03c546aa26655f84b4192109981d", + "sha256": "964cc3755e96330bf6d1ada63410f8adfdb3c45602e0446369d6dddace412405", + "public_source": "https://forgejo.coulomb.social/coulomb/user-engine/src/commit/9557d6f867fe03c546aa26655f84b4192109981d/docs/evidence/2026-10-11-one-app-catalogue-executor/isolated-postgres-receipt.json" + } + ], + "selection_reason": "coordinator reports exact image cached; published source receipt binds image to producer; prior owner fixture used exact image for psycopg dependencies", + "interpreter": "/usr/local/bin/python3", + "interpreter_binding": "producer officialPython3.12+systempip runtime, no virtualenv; execution must verify exact sys.executable/psycopg3 before candidate import, no PATHfallback", + "not_claimed": [ + "worker did not query Docker or create/run containers", + "bundled psycopg version not observed yet; must be reported by exact helper", + "historical image risk/production acceptance not transferred", + "cached helper is not a future identity-provisioner image/dependency admission" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r2.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r2.json new file mode 100644 index 00000000..bafc66af --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r2.json @@ -0,0 +1,28 @@ +{ + "from_manifest_sha256": "4a816f74f857dfbebb5eaa0de3d792c91cd5e0f52caf458c2f7789583507ee31", + "to_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "source_patch_unchanged": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "helper_image": { + "before": "forgejo.coulomb.social/coulomb/user-engine@sha256:1e6547c40c93c49f01438edcf76c20f561ccebc110220bae020723505aa56c70", + "after": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b" + }, + "helper_interpreter": { + "before": "PATH python3", + "after": "/usr/local/bin/python3", + "verification": "closed exact sys.executable/psycopg3 probe before candidate source import" + }, + "unchanged_contracts": { + "postgres_image": true, + "fixed_endpoint": true, + "engine_id": true, + "engine_name": true, + "limits": true, + "resources": true, + "phase_oracles": true, + "cleanup": true, + "network": true, + "pull_policy": true + }, + "worker_docker_calls": false, + "execution_requires_new_independent_review": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r3.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r3.json new file mode 100644 index 00000000..e56aad97 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest-delta-r3.json @@ -0,0 +1,29 @@ +{ + "schema": "nk48.fixture-manifest-delta.v1", + "previous_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "successor_manifest_sha256": "78f2b0bbd7639780ca3a3a689310527f31e7e14c1fdb497a48bac4f69bce4093", + "changed_top_level_fields": [ + "diagnostics", + "r2_outcome", + "revision", + "source_sha256", + "supersedes_manifest_sha256" + ], + "unchanged_contract_fields": [ + "candidate_patch_sha256", + "containment_helper", + "fixed_endpoint", + "engine_id", + "engine_name", + "postgres_image", + "python_image", + "helper_interpreter", + "limits", + "resources", + "phase_oracles", + "cleanup", + "network", + "pull_policy" + ], + "runtime_executed": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest.json b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest.json new file mode 100644 index 00000000..67751eec --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/revisions/r3/manifest.json @@ -0,0 +1,140 @@ +{ + "schema": "nk48.postgres-journal-fixture-proposal.v1", + "status": "prepared_not_executed_not_approved", + "source_sha256": { + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py": "sha256:04237939df1887df8a201181d87ebfacfa25855401b64f4998a3921111bd91eb", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/probe.py": "sha256:39eea88a246a799945da72d022200dd9e15662b765bbc896a4edbdc2f8bfd5a0", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_driver.py": "sha256:ab735b251ca70c709035b62d501dd46f842792fcb2c3b69888ae2aad698cff6e", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/source-manifest.json": "sha256:0c16e19415ad93b6ca73c92a05c205d263adde8a780c8c33119cd5f9d40f4752", + "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/provisioner.py": "sha256:19d1b837cbafeeacb1aa815856cd68091e6702281d72b1d860230fa320e1725b", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance.py": "sha256:405e08eaaab7c21d618dff4db5970607806f32ab6c461effb81c737e32182b10", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/group_provenance_postgres.py": "sha256:55282af26f95af66ff8d444d38d0bcbcf3ee58a615adedfff8da5db2b0716889", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/postimage/identity-provisioner/migrations/group_journal_postgres_v1.sql": "sha256:3cdf82194d6f1e2dd865b0c7e7b8a14447fda8c49151148134a0d40a80fb46ac", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/README.md": "sha256:b327ac993c72517b268850a9ca59344e73d825effabd3271d809243866dcd293", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/README.md": "sha256:4bf4678a1f1c70fbd3d4ffa77c70821be0372f03210b2164a2895fc99ca6a6b9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/helper-selection-r2.json": "sha256:ec5464c0d5310028ab91073f985183af01da2d194358fad412d197dbf90106d9", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/producer-Containerfile.txt": "sha256:668a90308bb6307f95a8c5439e5d751f81bf1f7126755e26373f3523f07c858a", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/diagnostic_protocol.py": "sha256:d141fc00e18906bcdbf6cf5542e219580668daaaced6c57cbab179132f7c6a61" + }, + "candidate_patch_sha256": "503e3d0217e6be25df8a58f3968cab7c00cdd9933d0fee2a66ef951fc4aae2b1", + "containment_helper": "/home/worsch/net-kingdom/tools/bootstrap-recovery/isolated-keycape.py", + "fixed_endpoint": "unix:///var/run/docker.sock", + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "postgres_image": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "python_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:f239bbf332b8eb87320c86390fddca1dcb4f7c628a7af75e79dee8058d446a7b", + "image_pin_origin": "PostgreSQL pin unchanged; cached f239 helper selected from coordinator inventory and tied to UserEngine producer1bb7cacc by existing published receipt/previous owner PostgreSQL fixture; fixture-only selection, no future image admission", + "pull_policy": "never", + "network": "network-none", + "scope": "real disposable PostgreSQL backend SQL/migration/concurrency/restore compatibility; LDAP is explicitly the existing synthetic test Directory, not actual native LDAP in this fixture", + "limits": { + "max_service_containers": 2, + "host_ports": 0, + "host_mounts": 0, + "named_or_anonymous_volumes": 0, + "memory_bytes_per_container": 402653184, + "cpus_per_container": 0.5, + "pids_per_container": 64, + "postgres_tmpfs_bytes": 153092096, + "helper_tmpfs_bytes": 33554432, + "backup_in_host_memory_max_bytes": 4194304, + "container_lifetime_seconds": 240, + "execution_deadline_seconds": 210, + "docker_rpc_timeout_seconds": 50, + "preflight_rpc_timeout_seconds": 15, + "cleanup_rpc_timeout_seconds": 15, + "cleanup_total_bound_seconds": 120, + "new_cloud_spend_eur": 0 + }, + "compatibility_preconditions": [ + "exact cached image/engine pins reverified by admitted preflight, never pulled", + "PostgreSQL image supports nonroot UID999 with initdb/postgres/pg_dump/pg_restore in PATH; failed compatibility refuses/cleans", + "User Engine f239 producer source uses official Python system prefix; exact /usr/local/bin/python3 and bundled psycopg3 must pass closed probe before candidate import, no PATH/venv fallback or downloads", + "all image-declared volumes must be masked by private tmpfs; existing containment guard refuses any anonymous volume before private import" + ], + "resources": { + "container_names": "nk49-kc--journal-{postgres,probe}", + "exact_label": "net-kingdom.recovery-run=", + "private_state": "generated admin/runtime passwords, source/migration, synthetic directory, external test checkpoint within private container tmpfs", + "safe_result": "private /tmp/nk48-postgres-native-review-/result.json" + }, + "phase_oracles": { + "bootstrap": [ + "only_loopback_no_external_routes", + "actual_postgresql_journal_ensure", + "explicit_scope_context_retained", + "confirmed_inventory_excludes_preexisting", + "runtime_ddl_denied" + ], + "restart": [ + "only_loopback_no_external_routes", + "separate_process_postgres_restart_and_revoke", + "preexisting_shared_state_preserved", + "foreign_context_receipt_refused" + ], + "concurrency": [ + "only_loopback_no_external_routes", + "concurrent_original_reservation_exactly_once", + "stale_version_update_refused_without_head_advance" + ], + "lost-ack": [ + "only_loopback_no_external_routes", + "lost_checkpoint_ack_retains_pending_reservation", + "reopened_uncertainty_refuses_ldap_effects", + "active_fence_survives_process_reopen", + "lost_synthetic_directory_response_retains_postgres_intent", + "lost_sql_commit_reply_retains_committed_head", + "reopened_stale_checkpoint_refuses_continuation" + ], + "restored": [ + "only_loopback_no_external_routes", + "actual_pg_restore_behind_checkpoint_detected", + "restored_store_cannot_gain_replay_authority" + ], + "fork": [ + "only_loopback_no_external_routes", + "concurrent_divergent_clones_same_counter_verified", + "independent_exact_transition_cas_selects_one_winner", + "reopened_loser_clone_refused_at_same_counter" + ] + }, + "restore": "actual generated pg_dump after bootstrap; winner database advances through reopen/concurrency/fork/uncertainty; actual pg_restore of original archive while independent content checkpoint survives; runtime SELECT must work and show older restored head before refusal can pass", + "cleanup": "finally reuses exact name/label cleanup for both preregistered containers, including failed/lost-create replies; successful explicit absence required; any Docker uncertainty remains failed; no prune/global removal", + "not_claimed": [ + "CNPG HA, native runtime storage/backup/restore admission", + "authoritative tenant/case/PDP/evidence/session owner callbacks", + "external LDAP writer fencing/CAS/ABA safety", + "production endpoint activation, credentials or owner assent", + "KeyCape hard-parent-loss reaper proof" + ], + "forbidden": [ + "Docker commands before exact independent review", + "pull/build/push, host networking/ports/mounts, production APIs", + "real database/LDAP, identity archives, credentials from environment/custody", + "clearing production restore quarantine or manufacturing scope/authority" + ], + "execution_command": [ + "python3", + "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/group-postgres-r1/native-fixture/driver.py", + "--execute-reviewed-fixture", + "--expected-manifest-sha256", + "" + ], + "fork": "actual PostgreSQL CREATE DATABASE TEMPLATE clone; both stores pass exact same old external marker check under test barrier, commit different bodies at same counter, race external strict CAS; exactly one wins and fresh loser store refuses; no directory effect in this direct reservation case", + "revision": "r3-bounded-stage-diagnostics", + "helper_interpreter": "/usr/local/bin/python3", + "supersedes_manifest_sha256": "8ac8547b43ac1679acadd422993bc3fbce842a6c315a728af4d452734910240b", + "r1_preflight": "failed missing helper before execute/session/private import; preserved structured failure, no raw stderr file exists", + "r2_outcome": "failed after containment/interpreter verification; checks empty, exact helper subphase unavailable; byte-exact safe receipt retained revisions/r2/execution-result.json; cleanup absence verified", + "diagnostics": { + "driver_stage": "fixed closed vocabulary before each RPC/bootstep, checkpointed result", + "probe_stage": "fixed closed vocabulary NDJSON before bootstrap substeps, failed final retained before oracle refusal", + "exceptions": "allowlisted class and optional five-character SQLSTATE only, never values/logs/exception messages", + "max_probe_output_bytes": 32768, + "max_probe_frames": 96, + "max_rpc_trace_rows": 192, + "initdb": "synchronous fixed boolean completion before detached server launch" + } +}