Record NK-WP-0048 1.2 adapter set coordinator review r2 (3 rejections; R1-F1 resolved)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Reject generated-only commits / generated-commit-guard (push) Successful in 8s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-10-17 11:04:53 +02:00
parent 94827c3de2
commit b9e9a8d836

View file

@ -0,0 +1,159 @@
{
"schema": "custodian.nk48-adapter-set-source-review.v1",
"revision": "r2",
"reviewed_on": "2026-10-17",
"reviewer": "custodian coordinator (Claude Code, third separate session; no context from the authoring session or the r1/fix session; nothing asked of either)",
"frozen_target": {
"repository": "net-kingdom",
"commit": "68f4bbb8ca613940505d72523319a2f6281067d3",
"base": "2019e82",
"commits_in_range": 24,
"commits_in_range_verified": "git rev-list --count 2019e82..68f4bbb = 24; diff 582ab9f..68f4bbb touches only mint-secret-adapter.py, compose-1.2-descriptors.py (comment), descriptor-composition-1.2.candidate.json, test_mint_secret_adapter.py, test_compose_1_2_descriptors.py, new test_admit_1_2_adapter_descriptors.py and the NK-WP-0048 workplan",
"kernel": "ops-warden a45cf47 src/warden/case_run.py, case_inputs.py, case_receipt_state.py (out of scope; used as the admission contract)"
},
"hashes_verified": {
"keycape-registration-adapter.py": "sha256:021eee262ea5d23d46258ed819da57b331f07b814adcafd60e0941214be02aea",
"ensure-groups-adapter.py": "sha256:19325fc599ae76e7ec2b54c132b1cf7dea1b5e931bb5263031dbdf8f4c0bcc7f",
"mint-secret-adapter.py": "sha256:b4b0cc7f6d7b922256aaa7b66b7a8c0b37cf916928ced4834c1beb7a21454955",
"deliver-secret-adapter.py": "sha256:b97df5710e388ca57b9f239a2900bc29e0b1c1fe1740a1f7987636b44343cf80",
"bind-env-adapter.py": "sha256:3f1c6426e31806d969336b6fdfecd8dd321ec0ca43b50b20213cc9f7637f7c7e",
"deliver-consumer-secret-adapter.py": "sha256:d046eed63090d4e0849a931e2650ccfd89d3b686843aed9e5cb35a0e18e513ee",
"handover-adapter.py": "sha256:0efc300a7b2765bce8da140d5c4666d9cb30daf02b4cbf0e0df96cba7c33d026",
"compose-1.2-descriptors.py": "sha256:8755f479f13f8978592a1346bcf8fdbb642eaece99ec29d31cc18091c655cffc",
"descriptor-composition-1.2.candidate.json": "sha256:9dc5606d6e823084b7d1c1ace2a06408197c23de5ed31a05ba21787e5458d2d2",
"test_admit_1_2_adapter_descriptors.py": "sha256:7ca824283564f3ac381b9145b0ca80a052dd9b70c2cce4a95c8dce544882a684"
},
"hashes_match": true,
"independent_reconstruction_passed": 102,
"independent_reconstruction_subtests_passed": 85,
"reconstruction": "git archive of net-kingdom 68f4bbb (full tree) and ops-warden a45cf47 src into a scratch directory; README-r2 pytest command run there: 102 passed, 85 subtests passed. The composer was rerun from the clean export with the artifact's own check_config_digest and params: exit 0, output byte-identical to the committed artifact.",
"admit_test_assessment": {
"fails_against_old_agent_revision": true,
"evidence": "test_admit_1_2_adapter_descriptors.py copied into a clean export of 582ab9f and run there: 2 failed, 3 passed (subtest mint-secret of test_every_1_2_descriptor_passes_kernel_admission refused 'agent mint is forbidden'; test_mint_secret_admits_only_a_human_actor failed). So the test detects NK48-R1-F1.",
"stand_in_adapters": "The stand-ins are built from each descriptor's own adapter and adapter_source, so the kernel's adapter source-pin check is a tautology in this test and would not detect a mismatch. No real mismatch is hidden at 68f4bbb: every build() returns Adapter(REFERENCE, source_digest()) from the same function its descriptor() uses, the composer refuses a descriptor whose adapter_source differs from the module's source_digest(), and the registration tests run the real adapter through Executor. The test proves descriptor shape, actor, lane, owner and target-parameter admission only, not source pins.",
"fixture_vs_owner_admission": "Correctly labelled fixture-only: the evidence verifier accepts four fixed strings regardless of purpose or source, the type's execution_descriptors map is injected one step at a time (the committed 1.2 type.yaml has none), and the case acceptance block says it is not an owner record. Nothing in it is, or can be mistaken for, owner admission or a pin.",
"verdict": "proves what its docstring claims (kernel admission of all seven descriptors under fixture trust, and the agent-mint and wrong-lane refusals); does not prove source pins or any run/rollback behaviour"
},
"nk48_r1_f1_disposition": {
"status": "resolved",
"basis": "Founder decision 2026-10-17 (actor human). mint-secret descriptor actor is 'human' at 68f4bbb; it passes Executor.admit under a45cf47 in the new test (fixture trust); an agent descriptor is refused 'agent mint is forbidden' and an agent caller on the human descriptor is refused 'principal actor or lane mismatch'. Kernel unchanged. complete_executable_descriptor_set: true now holds at kernel admission, not only at Descriptor.decode, for the descriptor bytes as committed.",
"consequence_noted": "With actor human and the kernel's WARDEN_AGENT_ID guard at both admit and _admission, no agent session (Codex, Claude Code with WARDEN_AGENT_ID set) can admit or run mint-secret. That is the intended effect of the founder decision."
},
"verdicts": {
"keycape-registration-adapter.py": {
"verdict": "approved",
"checks": "1: source digest covers adapter plus the full local import closure (keycape-client.py -> portal-client-rollout.py -> openbao-client-config.py, register-user-engine-portal.py) and the check-config binary digest; drift refused at build and per attempt; checker executed from a sealed memfd snapshot. 2: closed helper receipt, payload carries uid/resource_version only. 3: rollback only of a creation verified in this process, at the exact post-creation revision; pre-existing client gives NO_CHANGE with no created object. 4: revoke_session in finally on add, remove and reconcile. 5: step/owner/actor/lane, case pin, descriptor pin, acceptance digest and attempt binding checked. 7: reconcile_original keeps original intent separate from the reconcile attempt, never replays, refuses missing/ambiguous/unauthenticated/changed state, and post-restart rollback ends ROLLBACK_UNCERTAIN (kernel-level tests in test_registration_original_intent_reconcile.py)."
},
"ensure-groups-adapter.py": {
"verdict": "approved",
"checks": "1: source digest covers adapter plus identity-provisioner provisioner.py, group_provenance.py, group_provenance_postgres.py; that is the complete local closure (only stdlib and psycopg otherwise). 3: rollback delegated to the owner Postgres journal by ensure idempotency key, reversing only journal-confirmed creations; resumed or empty ensures give NO_CHANGE with no created objects; verify requires the owner journal state. 4: revoke_session in finally. 5: journal binding must name this case digest, reviewed source and principal; attempt binding checked. 6: version_root defaults to '1.1', '1.2' only when passed, unknown roots refused."
},
"mint-secret-adapter.py": {
"verdict": "rejected",
"finding": "NK48-R2-F1",
"check": 4,
"note": "Otherwise sound: descriptor actor human/effect mint/scopes parent-session+broker-grant admit under a45cf47; custody path derived from the admitted target as the type requires; confidential-only; closed value-free receipts; rollback destroys only the version minted and authenticated in this process."
},
"deliver-secret-adapter.py": {
"verdict": "rejected",
"finding": "NK48-R2-F2",
"check": 5
},
"bind-env-adapter.py": {
"verdict": "rejected",
"finding": "NK48-R2-F3",
"check": 1,
"note": "Otherwise sound: env binding derived from the case through the helper (KEYCAPE_<ID>_CLIENT_SECRET from sso/keycape-<id>-client, matching the 1.2 type), reference-only entries, CAS write with readback, pre-existing identical entry gives NO_CHANGE, rollback only of the in-process authenticated entry at its exact revision, allow_unimplemented refused, revoke_session in finally."
},
"deliver-consumer-secret-adapter.py": {
"verdict": "approved",
"checks": "1: single-file adapter, no local imports. 2: closed receipt, recursive value-key and length screen, published payload carries uids only. 3: only objects the owner reports created in this attempt, uid-bound, removed in reverse order after the consumer reference is reverted; pre-existing objects never removed. 4: revoke_session in finally. 5: owner and target bound to case consumer_owner_repo and client_id; case/descriptor pins and attempt binding checked. 6: accepts only a 1.2 case, the only type with this step."
},
"handover-adapter.py": {
"verdict": "approved",
"checks": "1: single-file adapter. 2: payload holds an observation digest and receipt reference only. 3: only a receipt created and authenticated in this process is removed; existing receipt gives NO_CHANGE. 4: revoke_session in finally. 5: target and (for 1.2) consumer_owner_repo bound to the admitted invocation; pins and attempt binding checked. 6: version_root defaults to '1.1', '1.2' explicit."
},
"compose-1.2-descriptors.py": {
"verdict": "approved",
"checks": "6: loads 1.2 only through an explicit {'1.2': ...} version root, refuses any other root map, refuses if 1.2 is reachable by default, and refuses if the canonical 1.1 type.yaml digest changes during composition; no --allow-unimplemented path. 8: refuses partial, duplicate, unknown and step-mismatched maps, kernel-invalid descriptors and owner/lane mismatches against the 1.2 follow_through; refuses a descriptor not pinning its own module's source digest. Fresh regeneration equals the committed artifact byte for byte."
},
"descriptor-composition-1.2.candidate.json": {
"verdict": "rejected",
"finding": ["NK48-R2-F1", "NK48-R2-F2", "NK48-R2-F3"],
"check": 8,
"note": "Correct and reproducible for 68f4bbb, and NK48-R1-F1 no longer applies. It is rejected only because three of its seven descriptors pin rejected sources (mint-secret, deliver-secret) or a source digest that does not cover the executed code (bind-env). It must be regenerated once those are fixed."
}
},
"findings": [
{
"id": "NK48-R2-F1",
"severity": "blocking",
"file": "mint-secret-adapter.py",
"title": "Broker-grant revocation is skipped when parent-session revocation fails",
"evidence": [
"mint-secret-adapter.py line 117, in the finally block: session, grant = revoke_session(inv), revoke_grant(inv). Both are evaluated in one expression inside contained_output().",
"Scratch demonstration against 68f4bbb: revoke_session raising made the attempt refuse 'owner callback failed' with zero revoke_grant calls."
],
"consequence": "For an attempt where session revocation raises, the broker grant for a mint is never revoked by the adapter. The kernel records UNCERTAIN, but nothing else ends the grant. The descriptor declares broker-grant as a revocation scope and the kernel requires grant-revocation proof for mint, so a minting grant can outlive a failed attempt.",
"r1_observation_decision": "Defect, not just an observation. No other adapter declares more than one revocation scope (only mint-secret uses broker-grant), so no other file has this pattern.",
"fix_direction": "Attempt each revocation independently (for example, revoke the grant in its own try/finally, or collect both results and their exceptions before raising), and add a test that a failing session revocation still calls revoke_grant and the reverse.",
"fix_venue": "net-kingdom new revision; not edited during review"
},
{
"id": "NK48-R2-F2",
"severity": "blocking",
"file": "deliver-secret-adapter.py",
"title": "Delivered Secret and custody reference are not bound to the admitted case",
"evidence": [
"build() takes namespace, name, custody_reference and key_names as free caller arguments and never receives case_source, so neither the case pin nor client_type is checked.",
"The 1.2 type fixes the step for the target: ExternalSecret sso/keycape-{client_id}-client (key client-secret) from bao:platform/workloads/keycape/{client_id}, only when client_type == confidential. mint-secret derives platform/workloads/keycape/<client_id> and bind-env derives sso/keycape-<client_id>-client from the case. deliver-secret derives neither.",
"The test fixture itself uses name keycape-client-forgejo and custody kv/keycape/forgejo, which contradict the type and the paths that bind-env and mint-secret use.",
"Scratch demonstration against 68f4bbb: an admission for target forgejo built and ran a delivery of sso/keycape-grafana-client from platform/workloads/keycape/grafana and returned SUCCESS."
],
"consequence": "A wrong target is not refused (check 5). Under one admitted case, the adapter can deliver another client's custody path to another Secret name, and a public-client case is not refused. The owner capability receives client_id in the binding, but the adapter does not enforce it.",
"fix_direction": "Bind case_source to admitted.pins.case like the other adapters, refuse non-confidential cases, and derive the Secret name, namespace, custody path and key name from the admitted target, as the type and the neighbouring adapters do. Leave no caller-chosen reference.",
"fix_venue": "net-kingdom new revision; not edited during review"
},
{
"id": "NK48-R2-F3",
"severity": "blocking",
"file": "bind-env-adapter.py",
"title": "bind-env source digest omits files it executes",
"evidence": [
"SOURCE_FILES = ('bind-env-adapter.py', 'keycape-client.py').",
"keycape-client.py loads portal-client-rollout.py at import (lane = module(...)), which loads openbao-client-config.py and register-user-engine-portal.py; client_from_params, used by bind-env's client_from_case, calls require from portal-client-rollout.py.",
"The registration adapter pins this same closure in five files; bind-env pins two.",
"Scratch demonstration: changing portal-client-rollout.py in a copy left bind-env's source_digest() unchanged."
],
"consequence": "Check 1 fails: drift in the executed validation code (for example a weakened require) is not refused before tooling, and the bind-env descriptor and artifact digest do not cover all the code the step runs.",
"fix_direction": "Pin the full local closure, the same five files the registration adapter uses, and add a closure-completeness test that compares each adapter's SOURCE_FILES with the files it actually loads.",
"fix_venue": "net-kingdom new revision; not edited during review"
}
],
"known_limits_assessed": {
"bind_env_no_kernel_level_fixture_tests": {
"verdict": "confirmed, and understated",
"detail": "Only the registration adapter has Executor run/rollback/reconcile tests. ensure-groups, mint-secret, deliver-secret, bind-env, deliver-consumer-secret and hand-over are covered by direct-call unit tests plus the new fixture admission test, which dispatches nothing. The limit applies to five adapters besides bind-env."
},
"ensure_groups_parity_copy": {
"verdict": "confirmed",
"detail": "group_binding() copies LLDAPProvisioner._group_binding; test_public_group_binding_matches_provisioner checks equal output on one input (functional, not byte, parity). Acceptable, because provisioner.py is inside ensure-groups' source digest, so any provisioner change already changes the pin and forces re-review."
},
"owner_capabilities_injected_stand_ins": {
"verdict": "confirmed",
"detail": "Custody mint/destroy, ESO delivery, the group provisioner journal (its type is checked, its contents are not), consumer apply/remove, consumer verifier, hand-over receipt store, evidence publish/authenticate and both revocation callbacks are injected in every adapter, and stubbed in every test. No real owner implementation is reviewed or present."
}
},
"observations_not_findings": [
"complete_executable_descriptor_set in the composer is still computed from Descriptor.decode plus type owner/lane matching. Kernel admission is proved only by the separate fixture test, which also does not check source pins. A future r3 could have the composer test admit each descriptor against the real adapter build() source.",
"The registration and bind-env adapters execute helper modules loaded at import time, but re-read source digests from disk at build time and per attempt. Drift between import and build is not detected. This is the pattern T13 had when the earlier r3 approved it, and it is noted here only for owner review.",
"mint-secret, deliver-secret, bind-env and registration do not select a type version themselves (the helper accepts oidc-browser-relying-party@1.x). Version selection relies on the kernel's case/type pin at admission. That is consistent with check 6, but it is a kernel dependency, not an adapter refusal.",
"The 1.2 type marks deliver-secret, bind-env and hand-over (and per its text deliver-consumer-secret) warden_executes: false, yet the set gives each an execution descriptor. Whether Warden should be admitted to dispatch these steps at all is a type-semantics question for owner review under T16, not a source defect.",
"registration reconcile_original refuses an original NO_CHANGE record and keeps the step uncertain. That is conservative and as tested."
],
"canonical_integration": "not approved",
"native_acceptance": false,
"grants": "none: no owner admission, pin or native authority; those stay with NK-WP-0048-T16 and the founder",
"hard_limits_observed": "no live or native action, no kubectl, SSH or secret access; net-kingdom and ops-warden not edited (read via git archive into scratch only; demonstrations ran on scratch copies with stub callbacks)",
"remaining": "Fix NK48-R2-F1 (independent grant revocation), NK48-R2-F2 (deliver-secret derives its Secret and custody reference from the admitted confidential case) and NK48-R2-F3 (bind-env pins its full import closure) in one net-kingdom revision, each with a regression test. Regenerate the artifact and rerun the admission test, then do the r3 review of the changed files plus the artifact. After that come owner review and admission under T16 (including the warden_executes question), then native acceptance."
}