diff --git a/docs/changes/netkingdom-target-2026-10-17/nk48-r5-source-followthrough-r1/workplan.preimage.md b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-source-followthrough-r1/workplan.preimage.md new file mode 100644 index 00000000..8c3529a6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/nk48-r5-source-followthrough-r1/workplan.preimage.md @@ -0,0 +1,1519 @@ +--- +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 owner-integration refinement: T23 records the completed source/package +placement review, distinct from T21's source/disposable-SQL proof. Its dedicated +`rapp-identity-provisioner` recommendation is draft and unbound, not an accepted +package or existing repository. The full six-consumer rapp-postgres renderer +accepts the inert candidate on platform-pg-2 as its third of four declared +consumers; first-cell placement is refused as a fifth. Neither the estimated +four-consumer ceiling nor source requests establish native headroom or backup. + +Next independent owner contracts: NetKingdom/package/substrate owners accept +exact deployment/rollback fate; rapp-postgres admits the dedicated consumer, +roles, five-connection budget and actual backup/restore; Platform admits exact +renewable runtime/migration delivery and TLS/CA/SAN; network owners scope SQL, +broker and checkpoint paths without assuming a narrow policy subtracts additive +same-namespace access. Protected caller owners must provide authoritative +GroupJournalBinding construction and current authorization; an independent +existing holder must authenticate and atomically advance the exact content +checkpoint, not a counter or ordinary Audit receipt. Directory/runtime owners +must select and prove either an exclusive all-writer maintenance barrier or +provider-supported CAS/generation, including established sessions, lifecycle +cascades and remove/readd ABA. No source review establishes those owner facts. + +The default PostgreSQL activation and destructive native rollback remain refused. +Storage/lease acceptance can be proposed separately from LDAP mutation only after +exact owner source/target/custody/verification/recovery and cost/risk review; +State Hub guard approval grants none of it. Source-only integration review and +native acceptance matrix: Custodian native-storage-r1/OWNER-REVIEW.md. T14's +existing status, UUID, dependencies and native completion gate are retained. + +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: wait +depends_on: [NK-WP-0048-T31] +blocking_reason: "Source candidate 061461e is merged. Waiting on the owner to select the real authenticated original-attempt holder and native readback, and on independent re-review of the changed T13 adapter source." +priority: high +state_hub_task_id: "1b825705-4848-5edc-832f-b1622cd9bdb7" +``` + +2026-10-17 source part merged (061461e via 65559ef). `build()` takes optional +paired `holder`/`observe` capabilities and installs `reconcile_original`; the T31 +fixtures now run through the adapter's own path. Rollback after restart is still +refused, and there is no add replay. This changes `keycape-registration-adapter.py`, +so the revision reviewed under Custodian `browser-adapter-next/coordinator-review-r3.json` +is superseded. The new revision has no independent review yet and must not be +pinned on the strength of the old one. + +2026-10-17: T31 fixtures done (74adcd7). What remains for T15: the owner +selects the real authenticated original-attempt holder, and the T13 adapter +gains the holder capability, `reconcile_original` and reconcile-aware `verify` +described in T31. + +Owner: NetKingdom for registration state, with Warden owning kernel evidence +admission. T22 records the completed source review and exact existing-seam gaps; +T15 retains implementation and authenticated restart/lost-receipt acceptance. +Next action: select and admit the actual owner publisher/authenticator and bounded +original-attempt readback contract for the merged holder/observe adapter seam. +Warden's original-intent persistence/interface (`a45cf475`) and additive validation +(`117fec9241e7b985dfb187b992e173d0a5dc3700`) are now published and projected. +The local Journal retains admitted nonsecret original context, validates original +actor/attempt and metadata linkage to the original running receipt, and supplies +it separately from the new reconciliation attempt. This local consistency is not +independent original-holder authorization or creation provenance; neither the +journal nor a current matching registry entry reconstructs creation authority. + +The owner contract must bind the original Invocation, case/type/descriptor/catalog/ +acceptance pins, source, principal and admitted tenant/realm provenance, separately +from the new reconciliation attempt and its independently revoked parent session. +Use an existing authenticated holder/readback; Audit Core's custody API is a +candidate only until its actual registration source/class, load-bearing emission +contract and separate scoped reader are admitted. No new broker or receipt trust +root is proposed. A missing original record, ambiguous publication, NO_CHANGE, +changed native revision or lost readback stays uncertain; no add replay or undo. + +Completion requires restart/lost-receipt fixtures against the selected actual owner +contract: independently authenticated original created-only provenance, original +exact client entry and unchanged current postcreation UID/resourceVersion before +any undo, with wrong attempt/case/source/principal and unavailable-holder refusals. +The merged T13 adapter exposes reconciliation only through paired injected +holder/observe capabilities and still refuses post-restart rollback. Existing +source review, JSON receipts and synthetic trust do not satisfy independent +holder authorization/provenance or native admission, or authorize attended recovery. +Exact remaining owner facts: Custodian native-registration-r1/original-attempt-review.json. + +2026-10-17 check (net-kingdom-worker): Warden's original-intent lookup interface +is not published — ops-warden head is `d97652a`, with no WARDEN-WP-0044-T03 +commit, and the net-kingdom inbox holds no announcement. No restart/lost-receipt +fixture or readback seam was added this session; the work is deferred behind +T16's decomposition under the same token budget. Remains todo, gated on Warden +publishing the reviewed lookup/context interface and on owner selection of the +authenticated original-attempt holder. + +2026-10-17 current-source correction: the earlier same-day `d97652a` lookup +checkpoint and T31 adapter-next-action note above are historical and superseded +by the published Warden interface/validation and merged `061461e` adapter seam. +The 54 focused Warden checks establish source behavior only. Current admission +leaves tenant/realm empty; no admitted tenant/realm provenance is inferred. +T15 remains wait for actual independently authenticated owner-holder/readback +selection, independent review of the changed adapter, and native restart/lost- +receipt acceptance. Existing task metadata, owner gates and historical reviews +are unchanged. + +## T16 — Select and review the actual machine descriptor composition + +```task +id: NK-WP-0048-T16 +status: todo +depends_on: [NK-WP-0048-T24, NK-WP-0048-T25, NK-WP-0048-T26, NK-WP-0048-T27, NK-WP-0048-T28, NK-WP-0048-T29, NK-WP-0048-T30] +priority: high +state_hub_task_id: "200bf500-5084-5ae7-be26-3033fe1f296e" +``` + +2026-10-17 coordinator review r4 approved the 1.3 candidate at 1920b13 with no +findings (the-custodian `nk48-adapters-1.2-review/coordinator-review-r4.json`). The founder chose +option A of the T16 owner-review packet (`nk48-t16-owner-review/founder-option-decision.md`): +1.3 with execution descriptors for all seven steps. Owner reviews for A are open +in the-custodian `docs/changes/netkingdom-target-2026-10-17/nk48-1.3-owner-review/` (net-kingdom, +railiance-platform, railiance-apps, ops-warden, key-cape). The owner-review packet +supersedes the r1 composition pointer: T16's exact composition is now +`sso-mfa/k8s/keycape/descriptor-composition-1.3.candidate.json`. + +2026-10-17 successor candidate prepared (source only, `proposed`): +`security-case-type-revisions/1.3/oidc-browser-relying-party/` is the 1.2 package plus +an `execution_descriptors` map of the seven composed descriptors, with examples +pinned to `@1.3` and no acknowledgment. ensure-groups, hand-over, +deliver-consumer-secret and the composer accept 1.3 (835680c, merged acda26c). +`test_admit_1_3_type.py` runs Warden's real admission against the committed 1.3 type: +the proposed type is refused; an acknowledged copy is refused because ops-warden +pins parameter fingerprints only for 1.1 and 1.2; and with only a 1.3 pin (equal to +1.2's fingerprint) all seven are admitted. This is a major change under canon +section 6. Next: r4 source review of the changed adapters and the map, then owner +reviews, including the ops-warden 1.3 pin, the mint-secret warden_executes/human +question and the owner-run steps question, then the founder's Decision Memo. + +Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden +independently owns global provenance/admission. T22 records the completed seven-step +source1.2 inventory and regenerated register-client descriptor using the delivered +checker and current sealed-snapshot source; T16 retains exact owner review, +complete descriptor composition and admission. Next action: bind actual native +publisher/authenticator, original/current session verification and revocation, +executable storage/custody and config pins, then obtain the six missing fixed +owner descriptors/adapters for ensure-groups, mint-secret, deliver-secret, +bind-env, deliver-consumer-secret and hand-over. The inventory identifies each +existing owner seam, exact missing contract and created-only rollback requirement; +it is not an executable bundle. T14 and T15 remain independent prerequisites. + +Review the dedicated consumer source1.2 composition only through the existing +explicit version-root selection. Keep canonical1.1/default and its accepted case +unchanged, and the source-only acknowledged1.2 reference case proposed without +acceptance fields. New descriptor bytes require a reviewed exact successor +revision/version under canon section6; they do not inherit historical source +acknowledgment. Changed authentication/materialization/trust needs the prescribed +informed-decision acknowledgment. Do not insert a partial descriptor map or use +--allow-unimplemented preparation as ordinary case/native admission. The registry +helper reports restart_required; separately review quiet-window restart/readiness +and actual consumer verification rather than infer activation from registry CAS. +Exact composition: Custodian native-registration-r1/descriptor-composition.json. +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. + +2026-10-17 budget checkpoint (net-kingdom-worker): no source change made. The +T13 registration adapter alone is 217 source and 317 test lines plus an +independent review; six further owner descriptors/adapters cannot be completed +within one per-task token budget (soft 8000 / hard 10000), so the task was +stopped below 50% and is decomposed rather than pushed through. Proposed split, +one source-candidate task each with positive/refusal fixtures, canonical1.1 +default unchanged and no --allow-unimplemented admission: (a) ensure-groups, +bound to T14's journal/provenance seam; (b) mint-secret and (c) deliver-secret, +Platform-owned evidence seams; (d) bind-env and (e) deliver-consumer-secret; +(f) hand-over, Apps-owned consumer verification; then (g) descriptor composition +over the reviewed set. Each candidate needs its owner's source review before +pinning; none is authored here as owner-accepted. Status stays todo. + +## 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 +state_hub_task_id: "afe78e4d-408a-516a-ab92-e212e8a2d11d" +``` + +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. + + +## T22 — Establish registration recovery source gaps and complete descriptor review inventory + +```task +id: NK-WP-0048-T22 +status: done +priority: high +state_hub_task_id: "ea50d93b-0d04-55d2-98c4-9b6c3fe925b1" +``` + +Completed 2026-10-17: source-only owner-contract review and complete review +inventory. Inspected the actual registration adapter, Warden Journal/Executor, +NetKingdom evidence compatibility checker and Audit Core custody/read surfaces. +No existing admitted holder/readback supplies authenticated original-attempt +recovery: payload/creation are process-local, persisted evidence references are +hashed, and reconcile receives a new attempt without original context. The exact +four-owner contract requirements and non-replayable missing/ambiguous cases are +recorded; no recovery callback, publisher, credential grant or native trust root +was installed. T15 retains implementation and actual acceptance. + +The complete source1.2 inventory maps all seven follow-through steps to their +existing owners, missing exact contracts and created-only rollback requirements. +The actual maintained adapter regenerated the sole existing register-client +machine descriptor against the delivered 13,633,409-byte checker and current +sealed-memfd snapshot source. The checker rehash and Warden Descriptor.decode +round-trip passed; six owner descriptors remain absent. Actual source commands +render canonical1.1 and explicitly selected1.2 with execution.permitted false; +ordinary1.2 case admission still refuses its acknowledged/unimplemented type. +Canonical type/case/acknowledgment bytes were hash-checked unchanged. No tests were +added or rerun, no native configuration or effect occurred, and no owner assent +or complete machine bundle is claimed. T16 retains native executable custody, +complete exact descriptors, independent source review and normal version/type +admission. This extracted outcome supplies no native gate completion credit. + +Evidence in the-custodian: +`docs/changes/netkingdom-target-2026-10-17/native-registration-r1/`, especially +original-attempt-review.json, descriptor-composition.json, source-verification.json, +source-manifest.json and manifest.json. The prior sixteen-gate audit remains +immutable. Publication, normal task UUID allocation and projection are separate +coordinator follow-through; no Hub UUID is assigned by this source record. + + +## T23 — Review native journal package assignment and storage integration boundaries + +```task +id: NK-WP-0048-T23 +status: done +priority: high +state_hub_task_id: "6e7cd483-c7af-53a2-9d0a-3d76b6fb49ee" +``` + +Completed 2026-10-17: source/package placement and protected integration review +only, separate from T21's completed PostgreSQL source and disposable-SQL proof. +The owner review recommends a dedicated independently rolled-back +`rapp-identity-provisioner`, owned by NetKingdom on rail-kubernetes, with a +schema-valid draft declaration and no reef binding. No repository, registry +entry, native package assignment or owner assent was created. Existing +rapp-user-engine ownership/token delivery does not assign it the provisioner. + +The complete current rapp-postgres catalog has six declared consumers: four on +platform-pg and two on platform-pg-2. The inert renderer-valid provisioner +candidate fits platform-pg-2 at three of four, requesting one migration plus +four runtime connections; placing it on the first cell is refused as a fifth. +Normative package schema and full-catalog capacity/render checks passed. The +four-consumer ceiling is estimated, not measured native headroom, and no current +SQL session, backup or restore acceptance is claimed. + +The exact review identifies remaining package/substrate, SQL role/lease/TLS, +network and backup/recovery contracts; independently authenticated protected +scope callbacks; an external content-bound checkpoint holder with atomic +advance/loss handling; and all-LDAP-writer exclusion or provider CAS/generation, +including lifecycle cascades and remove/readd ABA. Generic Audit custody is not +that checkpoint service. A database-only fence, final reread or one replica +cannot exclude foreign directory writers. The native acceptance/recovery matrix +keeps storage/lease admission separate from separately approved directory +mutation; the production activation remains disabled pending trusted integration. + +Evidence in the-custodian: +`docs/changes/netkingdom-target-2026-10-17/native-storage-r1/OWNER-REVIEW.md`, +source-validation.json and hash-bound source-references.json, plus the inert +proposed declaration/consumer and renderer outputs. No native resource, lease, +credential, deployment, destructive effect, independent holder or native owner +acceptance was delivered. T14 retains these live gates with its original UUID +and progress status. This completed review adds no duplicate T21 acceptance +credit; publication, task UUID allocation and projection remain coordinator +follow-through and no UUID is hand-assigned here. + +## T24 — Ensure-groups adapter source candidate + +```task +id: NK-WP-0048-T24 +status: done +priority: high +state_hub_task_id: "1a3931ee-3b5f-52fc-9295-4073804f4bd2" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): ensure-groups-adapter.py, 8 tests (commit 5a2e7d6). Binds to the T14 PostgreSQL journal and refuses SQLite or a foreign binding. It accepts only canonical 1.1 cases, so T30 must add an explicit 1.2 path. It calls the provisioner's private `_group_binding`; a public helper is the cleaner shared edit. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Bind group creation to T14's journal/provenance seam; refuse without trusted journal callbacks. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. NetKingdom (directory) owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T25 — Mint-secret adapter source candidate + +```task +id: NK-WP-0048-T25 +status: done +priority: high +state_hub_task_id: "0de2a7d9-874d-578b-a076-f653fe42e30b" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): mint-secret-adapter.py, 9 tests (commit 558c5bc). Value-free receipts only. The receipt schema `railiance-platform.custody-mint.v1` and the mint/destroy capability interface are proposals that Platform must confirm. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Platform-owned custody evidence seam: the adapter records value-free mint evidence only and never handles secret values. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. Platform owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T26 — Deliver-secret adapter source candidate + +```task +id: NK-WP-0048-T26 +status: done +priority: high +state_hub_task_id: "94b3cbf0-c908-5ac6-b0db-67385c3e021c" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): deliver-secret-adapter.py, 8 tests (commit 1b60f6e). The lane `platform-secret-delivery` and the schema `netkingdom.deliver-secret.v1` are proposals that Platform must confirm. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Platform-owned delivery evidence seam (ESO/custody), value-free. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. Platform owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T27 — Bind-env adapter source candidate + +```task +id: NK-WP-0048-T27 +status: done +priority: high +state_hub_task_id: "e56365f8-29b5-5c89-a329-8115f9f3b2a7" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): bind-env-adapter.py, 5 tests (commit 6fbce73). Binds references only. To fit the budget it does not yet wrap a full Warden `Adapter` like T13 (source-digest pin, authenticated evidence, parent-session revocation); it returns `(reference, run, rollback)`. That wrapping is a prerequisite for T30. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Bind the issuer Deployment env reference to the delivered secret reference, without values. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. NetKingdom runtime owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T28 — Deliver-consumer-secret adapter source candidate + +```task +id: NK-WP-0048-T28 +status: done +priority: high +state_hub_task_id: "6dab281c-ea9a-5a17-a83f-e8e9a4d4484f" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): deliver-consumer-secret-adapter.py, 5 tests (commit 650c1ac). Type 1.2 only. It assumes the owner has validated ESO TokenRequest and the controller-RBAC admission; that stays an owner gate. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Consumer-side delivery evidence seam, value-free. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. Platform and Apps owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T29 — Hand-over adapter source candidate + +```task +id: NK-WP-0048-T29 +status: done +priority: high +state_hub_task_id: "129db41d-4560-5e20-9f3b-c3c91e0a5601" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): handover-adapter.py, 8 tests (commit ec63539). The owner `apps` and lane `apps-consumer-handover` are proposals that the Apps owner must confirm. It has no reconcile path; restart handling follows T15. + +Split from T16 on 2026-10-17 so each candidate fits one per-task budget. Apps-owned consumer verification and hand-over receipt seam. +Source candidate only, modelled on the T13 registration adapter +(`sso-mfa/k8s/keycape/keycape-registration-adapter.py`, its tests and +`registration-adapter.md`). Requires positive and refusal fixtures and +created-only rollback. Keeps canonical 1.1 as the default and uses no +`--allow-unimplemented` admission. Done means the candidate and its tests are +committed. Apps owner source review before pinning, and any native action, +stay separate gates and are not supplied by this task. + +## T30 — Compose the descriptor set over the reviewed candidates + +```task +id: NK-WP-0048-T30 +status: done +depends_on: [NK-WP-0048-T24, NK-WP-0048-T25, NK-WP-0048-T26, NK-WP-0048-T27, NK-WP-0048-T28, NK-WP-0048-T29] +priority: high +state_hub_task_id: "6f6073ac-52dd-5b70-8874-6d76d4c072e4" +``` + +2026-10-17 done: coordinator review r3 (the-custodian `nk48-adapters-1.2-review/coordinator-review-r3.json`, +fd07eaa) approved all nine files at ff8f597, with R2-F1..F3 resolved, an independent +reconstruction of 117 passing and no open findings. This is a source review only: +it grants no owner admission, pin or native authority. Owner review and admission +continue under T16, which also takes the r2 observations, including the +`warden_executes: false` steps that carry execution descriptors. + +2026-10-17 coordinator review r2 (the-custodian `nk48-adapters-1.2-review/coordinator-review-r2.json`) +approved five files and rejected three, plus the artifact. The fixes are merged in +2ee4ed0: R2-F1, mint-secret revokes the session and the broker grant +independently (eeed8e1); R2-F2, deliver-secret binds to the admitted case and +derives its Secret and custody path, with no caller reference (640292c); R2-F3, +bind-env pins its five-file closure, and a new closure-completeness test checks +every adapter (d16db30). Each regression test failed on 68f4bbb. The artifact is +regenerated, and the full suite including kernel admission passed 117. Next is +the r3 review. Open for owner review under T16: the type marks four steps +`warden_executes: false`, yet each has an execution descriptor. + +2026-10-17 founder confirmed every proposed lane and schema name for T25-T29 +as-is. The artifact records them as `names_confirmed` (by founder, 2026-10-17), +and no step is still `owner_confirmation_required`. Next: independent source +review of the seven adapters. + +2026-10-17 founder decision (supersedes the `agent` decision below): the +mint-secret actor is `human`. Coordinator review r1 (the-custodian +`docs/changes/netkingdom-target-2026-10-17/nk48-adapters-1.2-review/coordinator-review-r1.json`, +finding NK48-R1-F1) showed that unchanged kernel a45cf47 refuses every +`effect: mint` admission whose actor is not `human` ("agent mint is +forbidden"), so the `agent` descriptor could never be admitted. Adapter, tests +and composer comment updated and the artifact regenerated (mint-secret +descriptor digest now `sha256:8c40c2884f8ba15e8cb6c98f33419c395e54fce8ef33d6e1a34782cf3f1fcfa9`); +98 tests pass against a45cf47. The set needs an independent r2 review. + +2026-10-17 `test_admit_1_2_adapter_descriptors.py` runs Warden's own +`Executor.admit` on all seven composed 1.2 descriptors, with the real 1.2 +type, the 1.2 forgejo example case, the reviewed `BrowserInputs` validator and +fixture-only trust (stand-in adapters, nothing dispatched). It also pins that +an agent mint descriptor is refused ("agent mint is forbidden") and that a +wrong lane is refused for every step. Against the old `agent` revision +(582ab9f) it fails as expected. Combined suite: 102 passed on ops-warden a45cf47 +and on current ops-warden. Fixture admission is not owner admission or a pin. + +2026-10-17 founder decision: the mint-secret actor is `agent`, not `warden`. +The kernel is unchanged. The adapter, its test and the composer were updated and +the artifact regenerated; all seven steps pass `Descriptor.decode` and +`complete_executable_descriptor_set` is true (98 tests passed). The artifact +stays `not_an_admission`, with no owner review, admission or dispatch permission. + +2026-10-17 composer merged (60b41a1 via 3c62bec) with follow-up 376b44b (merged; +combined suite 98 passed). `compose-1.2-descriptors.py` loads 1.2 only through an +explicit version root and refuses partial, duplicate, unknown or mismatched maps. +`descriptor-composition-1.2.candidate.json` is a reviewable candidate, marked +`not_an_admission`, with canonical 1.1 unchanged. ensure-groups and hand-over now +carry and honour `parent-session` revocation, and the hand-over owner is +parameterised (`railiance-apps` for Forgejo). Six of the seven steps pass +`Descriptor.decode`. mint-secret is still refused: its actor `warden` is outside +the kernel's human/agent set, and Platform (with ops-warden, if the kernel should +change) must decide. Five steps carry `owner_confirmation_required` names. The +set is complete only after both. Owner review, the canon section 6 successor +revision and admission remain T16's. + +2026-10-17 prerequisites merged (65559ef; combined suite 78 passed): +ensure-groups accepts 1.2 via an explicit `version_root='1.2'` (531151b), with +1.1 still the default. bind-env is now a full Warden v1 `Adapter` (7442d21); its +kernel-level fixture tests like T13's are still missing, and its `build()` +signature changed. Composition is unblocked in source. Owner confirmation of +the proposed step, lane and schema names remains before pinning. + +2026-10-17 inputs from T24–T29 that composition must resolve: T24 accepts +only 1.1 while T28 accepts only 1.2, so add an explicit 1.2 ensure-groups path; +T27 still needs full Warden `Adapter` wrapping; the lane, schema and owner names +proposed in T25, T26 and T29 need owner confirmation before pinning. + +Split from T16 on 2026-10-17. Compose the seven-step source 1.2 descriptor set +through the existing explicit version-root selection over T13 and T24–T29. No +partial descriptor map, canonical 1.1 stays the default, and new descriptor bytes +need a reviewed successor revision under canon section 6. Owner review and +admission remain T16's. + +## T31 — Restart and lost-receipt fixtures against Warden's original-intent interface + +```task +id: NK-WP-0048-T31 +status: done +priority: high +state_hub_task_id: "ed1ccd87-89a0-5e69-885c-0eaf5ab0bb5a" +``` + +2026-10-17 done (source candidate only; merged in 7dbe75f; combined suite 69 passed): test_registration_original_intent_reconcile.py, 8 tests (commit 74adcd7), runs against the unmodified ops-warden a45cf47. The holder is a test-only in-memory store. For real T15 support the T13 adapter's `build()` needs an injected holder/readback capability, a `reconcile_original` callback, and a `verify` that accepts reconcile results. Rollback after restart stays refused until there is an owner contract. + +Split from T15 on 2026-10-17. ops-warden published the original-intent +lookup/context interface in `src/warden/case_run.py` (`OriginalIntent`, +`Journal.original_intent`, `Adapter.reconcile_original`; commit `a45cf47`). +Build T15's restart/lost-receipt fixtures for the registration adapter against +it: the original intent is kept separate from the reconcile attempt; wrong +attempt/case/source/principal and unavailable-holder cases refuse; and a missing +or ambiguous record stays uncertain, with no add replay or undo. The interface +is Warden's own dispatch record, not owner-authenticated created-only provenance. +Selecting the actual authenticated holder stays a T15 owner gate. diff --git a/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/owner-source-state.json b/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/owner-source-state.json new file mode 100644 index 00000000..143e0dd5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/owner-source-state.json @@ -0,0 +1,29 @@ +{ + "owners": [ + { + "owner": "ops-warden", + "published_commit": "b593c879831378db7dc62adfabdfedf2d44ddd38" + }, + { + "owner": "net-kingdom", + "published_commit": "befb0bc68cd25b625804fce559476eefd702521d" + }, + { + "owner": "railiance-platform", + "published_commit": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9" + } + ], + "native_target_gates_closed": 0, + "all16_native_gates_remain_open": true, + "NK48_task_count": 35, + "NK48_new_tasks": { + "T32": "done: independent mint guard regression", + "T33": "done: exact proposed issuer-source reference", + "T34": "progress: real Apps inverse backend absent", + "T35": "done: authenticated catalog fixtures" + }, + "safe_ordinary_unit_result": "785 passed,4 deselected", + "native_or_credential_actions": false, + "PARAMETER_PINS_or_draft_promotions": false, + "failed_and_unknown_outcomes_retained": true +} diff --git a/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/prepare.py b/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/prepare.py new file mode 100644 index 00000000..13904115 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/source-corrections-publication-r1/prepare.py @@ -0,0 +1,36 @@ +from pathlib import Path +import hashlib,json,subprocess +r=Path('/home/worsch/the-custodian');p=Path(__file__).resolve().parent;base=p.parent +def git(root,*a):return subprocess.check_output(['git','-C',str(root),*a],text=True).strip() +expected={'ops-warden':'b593c879831378db7dc62adfabdfedf2d44ddd38','net-kingdom':'befb0bc68cd25b625804fce559476eefd702521d','railiance-platform':'104ed90f42a9a86bf9fda343b49b65a85c9ee4b9'} +owner_state=[] +for owner,want in expected.items(): + owner_root=r.parent/owner;assert git(owner_root,'rev-parse','HEAD')==want + assert git(owner_root,'ls-remote','origin','refs/heads/main').split()[0]==want + owner_state.append({'owner':owner,'published_commit':want}) +receipt=json.loads((base/'warden-safe-unit-integration-r1/projection.json').read_text()) +assert receipt['ok'] and receipt['status']=='applied' and receipt['commit']==expected['ops-warden'] +assert receipt['receipt']['instance_role']=='primary' and receipt['receipt']['instance_label']=='railiance01' +assert receipt['receipt']['outcome']['counts']['retired']==0 +names=['nk48-r5-f1-implementation-r1','nk48-r5-f2-implementation-r1','nk48-r5-f2-review-r1','nk48-r5-f2-publication-r1','nk48-r5-f3-implementation-r1','nk48-r5-f3-review-r1','nk48-r5-f3-implementation-r2','warden-1.3-kernel-fixes-r1','warden-1.3-kernel-review-r2','warden-kernel-source-integration-r1','nk48-r5-source-followthrough-r1','nk48-catalog-fixture-update-r1','nk48-catalog-fixture-publication-r1','nk48-catalog-fixture-publication-r2','nk48-issuer-reference-integration-r1','nk48-current-source-publication-r3','key39-infra-finalization-r1','warden-safe-unit-runner-r1','warden-safe-unit-runner-review-r1','warden-safe-unit-integration-r1'] +owned_docs=['docs/netkingdom-target-security-architecture.md','docs/netkingdom-target-implementation.md','workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md'] +state={'owners':owner_state,'native_target_gates_closed':0,'all16_native_gates_remain_open':True,'NK48_task_count':35,'NK48_new_tasks':{'T32':'done: independent mint guard regression','T33':'done: exact proposed issuer-source reference','T34':'progress: real Apps inverse backend absent','T35':'done: authenticated catalog fixtures'},'safe_ordinary_unit_result':'785 passed,4 deselected','native_or_credential_actions':False,'PARAMETER_PINS_or_draft_promotions':False,'failed_and_unknown_outcomes_retained':True} +(p/'owner-source-state.json').write_text(json.dumps(state,indent=2)+'\n') +paths=[str((base/n).relative_to(r)) for n in names]+owned_docs+[str((p/'owner-source-state.json').relative_to(r)),str((p/'prepare.py').relative_to(r))] +new=git(r,'ls-files','--others','--exclude-standard','--',*paths).splitlines() +changed=git(r,'diff','--name-only','HEAD','--',*paths).splitlines() +files=[] +for name in sorted(set(new+changed)): + f=r/name + if any(x in {'publication-backup','consistency-backup','backup','test-home','synthetic-parent-home'} for x in f.parts) or name.endswith('.backup') or f.name.startswith('WORK-RECORDS.md'):continue + assert not f.is_symlink() + files.append({'path':name,'sha256':hashlib.sha256(f.read_bytes()).hexdigest()}) +assert len(files)>200 +protected_names=git(r,'diff','--name-only','HEAD').splitlines();protected_names=[n for n in protected_names if n not in owned_docs] +assert len(protected_names)==8,'Unexpected unrelated source changes; inspect before proceeding' +protected=[{'path':n,'sha256':hashlib.sha256((r/n).read_bytes()).hexdigest(),'index':git(r,'ls-files','--stage','--',n)} for n in protected_names] +(p/'protected-state.json').write_text(json.dumps(protected,indent=2)+'\n') +head=git(r,'rev-parse','HEAD');assert git(r,'ls-remote','origin','refs/heads/main').split()[0]==head +cfg={'owner':'the-custodian','base_revision':head,'message':'Record reviewed case admission fixes, exact issuer source and isolated unit checks','files':files} +(p/'publication-inputs.json').write_text(json.dumps(cfg,indent=2)+'\n') +print(json.dumps({'base':head,'files':len(files),'bytes':sum((r/x['path']).stat().st_size for x in files),'protected_unrelated_paths':len(protected),'source_only':True})) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate.patch b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate.patch new file mode 100644 index 00000000..ecc627fb --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate.patch @@ -0,0 +1,314 @@ +diff --git a/src/warden/case_run.py b/src/warden/case_run.py +index 2f66583..60692b8 100644 +--- a/src/warden/case_run.py ++++ b/src/warden/case_run.py +@@ -587,7 +587,18 @@ def _catalog_lane(catalog_source: bytes, descriptor: Descriptor): + raise Refused("descriptor owner does not fit catalog lane owner_repo") + if descriptor.effect in ("mint", "change") and entry.risk != "high": + raise Refused("catalog lane risk does not cover the descriptor effect") +- return entry ++ return entry, entries ++ ++ ++def _attended_admission(descriptor, actor, entry, entries): ++ from .routing.models import attended_lane_issue ++ ++ if (actor != "human" or "WARDEN_AGENT_ID" in os.environ ++ or "parent-session" not in descriptor.revocation_scopes): ++ raise Refused("attended step requires a human parent session") ++ issue = attended_lane_issue(entry, entries.get) ++ if issue: ++ raise Refused(issue[1]) + + + def _step_mode(case_type: dict, case: dict, descriptor: Descriptor) -> str: +@@ -711,14 +722,10 @@ class Executor: + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") +- entry = _catalog_lane(catalog_source, descriptor) ++ entry, entries = _catalog_lane(catalog_source, descriptor) + mode = _step_mode(case_type, case, descriptor) +- if mode == "attended" and ( +- actor != "human" +- or os.getenv("WARDEN_AGENT_ID") +- or "parent-session" not in descriptor.revocation_scopes +- ): +- raise Refused("attended step requires a human parent session") ++ if mode == "attended": ++ _attended_admission(descriptor, actor, entry, entries) + if mode == "owner" and ( + entry.effective_delegation.mode != "native" + or entry.owner_repo != descriptor.owner +@@ -796,7 +803,11 @@ class Executor: + descriptor.allowed_created, + descriptor.retry_safe, + ) +- self._admitted[invocation] = (descriptor, initial, session_evidence) ++ # Keep the mode and exact authenticated catalog bytes with this admission; ++ # dispatch must not substitute a later ambient catalog or lose its mode. ++ self._admitted[invocation] = ( ++ descriptor, initial, session_evidence, mode, catalog_source ++ ) + return descriptor, invocation, initial + + def _result( +@@ -861,7 +872,7 @@ class Executor: + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") +- session = self._admitted[invocation][2] ++ _, _, session, mode, catalog_source = self._admitted[invocation] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", +@@ -873,6 +884,9 @@ class Executor: + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") ++ if mode == "attended": ++ entry, entries = _catalog_lane(catalog_source, descriptor) ++ _attended_admission(descriptor, invocation.actor, entry, entries) + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): +diff --git a/src/warden/case_type.py b/src/warden/case_type.py +index 986d74f..1454cd3 100644 +--- a/src/warden/case_type.py ++++ b/src/warden/case_type.py +@@ -24,6 +24,7 @@ from typing import Iterable, List, Optional + import yaml + + from warden.routing.catalog import Catalog ++from warden.routing.models import attended_lane_issue + + VALUE_HANDLING = ("none", "generated_in_custody") + REQUIRED_STEP_FIELDS = ("id", "owner", "entry_point", "auth", "warden_executes", "attended", "value_handling") +@@ -138,13 +139,10 @@ def check_follow_through(doc: dict, catalog: Optional[Catalog] = None) -> Follow + findings.append(Finding("catalog", "LANE-MISSING", where, f"lane '{lane}' is not in the catalog")) + elif not entry.is_active or not entry.reviewed: + findings.append(Finding("catalog", "LANE-UNREVIEWED", where, f"lane '{lane}' is not active and reviewed")) +- elif attended and entry.lane == "attended-change": +- session = catalog.get(entry.session_lane or "") +- if session is None or not session.is_active or not session.reviewed or session.lane != "login": +- findings.append(Finding("catalog", "LANE-KIND", where, +- f"attended-change lane '{lane}' session_lane '{entry.session_lane}' does not resolve to an active, reviewed login lane")) +- elif attended and entry.lane != "login": +- findings.append(Finding("catalog", "LANE-KIND", where, f"attended step's lane '{lane}' is not a login or attended-change lane")) ++ elif attended: ++ issue = attended_lane_issue(entry, catalog.get) ++ if issue: ++ findings.append(Finding("catalog", issue[0], where, issue[1])) + + if executes: + warden_steps.append(sid) +diff --git a/src/warden/routing/models.py b/src/warden/routing/models.py +index 21abe64..c691b33 100644 +--- a/src/warden/routing/models.py ++++ b/src/warden/routing/models.py +@@ -8,7 +8,7 @@ pointers only (the no-double-source rule, enforced in `catalog.py`). + from __future__ import annotations + + from dataclasses import dataclass, field +-from typing import List, Optional ++from typing import Callable, List, Optional + + + # Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including +@@ -324,3 +324,20 @@ class RouteEntry: + elif any(t in h or h in t for h in haystack): + score += 1 + return score ++ ++ ++def attended_lane_issue( ++ entry: RouteEntry, get_entry: Callable[[str], Optional[RouteEntry]] ++) -> Optional[tuple[str, str]]: ++ """Shared planner/executor boundary, using one parsed catalog snapshot.""" ++ if not entry.is_active or not entry.reviewed: ++ return ("LANE-UNREVIEWED", f"lane '{entry.id}' is not active and reviewed") ++ if entry.lane == "attended-change": ++ session = get_entry(entry.session_lane or "") ++ if (session is None or not session.is_active or not session.reviewed ++ or session.lane != "login"): ++ return ("LANE-KIND", f"attended-change lane '{entry.id}' session_lane " ++ f"'{entry.session_lane}' does not resolve to an active, reviewed login lane") ++ elif entry.lane != "login": ++ return ("LANE-KIND", f"attended step's lane '{entry.id}' is not a login or attended-change lane") ++ return None +diff --git a/tests/test_case_1_3_integration.py b/tests/test_case_1_3_integration.py +index 342bdda..bb13c09 100644 +--- a/tests/test_case_1_3_integration.py ++++ b/tests/test_case_1_3_integration.py +@@ -1,10 +1,15 @@ + """Integration: the real net-kingdom 1.3 candidate against this kernel and catalog. + + Reads the actual 1.3 type, its forgejo example and the committed composition +-artifact from net-kingdom, plus this branch's real routing catalog. Only two things +-are simulated, both in memory: draft lanes flipped to active (promotion needs owner +-confirmation) and a 1.3 PARAMETER_PINS entry (PIN-1 waits for the founder's +-acknowledgment). Trust is fixture-only, and nothing is dispatched. ++artifact from net-kingdom, plus this branch's real routing catalog. The descriptor ++map and metadata come from those committed files. All authority is fixture-only: ++we change the proposed type to acknowledged, change the example case to ++materialized and replace its acceptance record, promote draft lanes in memory, ++and temporarily install the 1.3 parameter pin. Stub adapters replace native ++adapters; an unconditional evidence verifier simulates every acceptance and caller ++session. WARDEN_AGENT_ID is removed to simulate a human runtime. No adapter is ++dispatched. This proves conditional source admission, not authenticated owner ++acceptance, native execution or readiness; unresolved Apps sessions stay refused. + """ + from dataclasses import asdict + import hashlib +@@ -99,12 +104,15 @@ def test_artifact_descriptors_are_the_type_map(): + assert {s: digest(encode(d)) for s, d in descs.items()} == type_doc()["spec"]["execution_descriptors"] + + +-def test_all_seven_admitted_once_promoted_and_pinned(tmp_path): ++def test_conditional_admission_keeps_unresolved_apps_sessions_refused(tmp_path): + for step, desc in descriptors().items(): +- admitted, invocation, _ = admit(tmp_path, desc) +- assert admitted == desc +- assert (invocation.step, invocation.actor, invocation.target) == (step, "human", "forgejo") +- ++ if step in ("deliver-consumer-secret", "hand-over"): ++ with pytest.raises(Refused, match="does not resolve to an active, reviewed login lane"): ++ admit(tmp_path, desc) ++ else: ++ admitted, invocation, _ = admit(tmp_path, desc) ++ assert admitted == desc ++ assert (invocation.step, invocation.actor, invocation.target) == (step, "human", "forgejo") + + def test_draft_lane_steps_refused_until_promoted(tmp_path): + for step, desc in descriptors().items(): +diff --git a/tests/test_case_admission_modes.py b/tests/test_case_admission_modes.py +index e425020..5069491 100644 +--- a/tests/test_case_admission_modes.py ++++ b/tests/test_case_admission_modes.py +@@ -53,6 +53,7 @@ def test_attended_mode_admits_human_parent_session(rig, monkeypatch): + admitted = admit( + rig, + step=mode_step(False, True), ++ entries=[dict(lane_entry(), lane="login")], + desc=dict(effect="change", revocation_scopes=("parent-session",)), + ) + assert admitted[1].actor == "human" +diff --git a/tests/test_case_attended_boundary.py b/tests/test_case_attended_boundary.py +new file mode 100644 +index 0000000..ba07b33 +--- /dev/null ++++ b/tests/test_case_attended_boundary.py +@@ -0,0 +1,113 @@ ++"""Synthetic attended admission/dispatch regressions; no owner/native actions.""" ++from dataclasses import replace ++ ++import pytest ++ ++from tests.test_case_admission_modes import admit, mode_step ++from tests.test_case_run import fixture_catalog, lane_entry, make_rig ++from warden.case_receipt_state import Outcome, Refused ++from warden.case_type import check_follow_through ++from warden.routing.catalog import CatalogError, _parse_entry, load_catalog ++ ++ ++def attended(rig, entries): ++ return admit(rig, step=mode_step(False, True), entries=entries, ++ desc=dict(effect="change", revocation_scopes=("parent-session",))) ++ ++ ++@pytest.mark.parametrize("operation", ["run", "rollback", "reconcile"]) ++@pytest.mark.parametrize("marker", ["agent-1", "", None]) ++def test_identity_change_refuses_before_any_write_or_callback(tmp_path, monkeypatch, operation, marker): ++ monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) ++ rig = make_rig(tmp_path) ++ executor = rig[0] ++ admitted = attended(rig, [dict(lane_entry(), lane="login")]) ++ executor.evidence_verifier = lambda *_: True ++ if operation == "rollback": ++ executor.run(*admitted) ++ elif operation == "reconcile": ++ old = executor.adapters[admitted[0].adapter] ++ def lost(_): ++ raise RuntimeError("synthetic lost reply") ++ executor.adapters[old.reference] = replace(old, invoke=lost) ++ executor.run(*admitted) ++ calls = [] ++ old = executor.adapters[admitted[0].adapter] ++ callback = getattr(old, "invoke" if operation == "run" else operation) ++ def observed(*args): ++ calls.append(args) ++ return callback(*args) ++ executor.adapters[old.reference] = replace(old, **{ ++ "invoke" if operation == "run" else operation: observed, ++ }) ++ def rows(): ++ return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] ++ for table in ("intents", "receipts")) ++ before = rows() ++ if marker is None: ++ result = getattr(executor, operation)(*admitted) ++ assert result.last.outcome is {"run": Outcome.SUCCESS, ++ "rollback": Outcome.ROLLED_BACK, ++ "reconcile": Outcome.NO_CHANGE}[operation] ++ assert len(calls) == 1 ++ assert rows() != before ++ return ++ monkeypatch.setenv("WARDEN_AGENT_ID", marker) ++ with pytest.raises(Refused, match="attended step requires a human parent session"): ++ getattr(executor, operation)(*admitted) ++ assert rows() == before ++ assert not calls ++ ++ ++def entries_for(case): ++ change = dict(lane_entry(), lane="attended-change", session_lane="login-lane") ++ login = dict(lane_entry("login-lane"), lane="login") ++ if case == "missing": ++ return [change] ++ if case == "draft-login": ++ login["status"] = "draft" ++ elif case == "unreviewed-login": ++ login["reviewed"] = "" ++ elif case == "wrong-kind-login": ++ login["lane"] = "secret" ++ elif case == "draft-change": ++ change["status"] = "draft" ++ elif case == "unreviewed-change": ++ change["reviewed"] = "" ++ elif case == "missing-session-field": ++ del change["session_lane"] ++ elif case in ("secret", "ceremony"): ++ change["lane"] = case ++ del change["session_lane"] ++ return [change, login] ++ ++ ++@pytest.mark.parametrize("case, code", [ ++ ("valid", None), ("missing", "LANE-KIND"), ("draft-login", "LANE-KIND"), ++ ("unreviewed-login", "LANE-KIND"), ("wrong-kind-login", "LANE-KIND"), ++ ("draft-change", "LANE-UNREVIEWED"), ("unreviewed-change", "LANE-UNREVIEWED"), ++ ("secret", "LANE-KIND"), ("ceremony", "LANE-KIND"), ++]) ++def test_attended_change_planner_and_executor_agree(tmp_path, monkeypatch, case, code): ++ monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) ++ entries = entries_for(case) ++ path = tmp_path / "catalog.yaml" ++ path.write_bytes(fixture_catalog(*entries)) ++ catalog = load_catalog(path) ++ step = dict(mode_step(False, True)[0], attended_justification="synthetic test", ++ entry_point="owner/test", auth="synthetic session", value_handling="none") ++ doc = {"spec": {"follow_through": [step], "rollback": [{"reverses": step["id"]}]}} ++ report = check_follow_through(doc, catalog) ++ assert [f.code for f in report.findings] == ([] if code is None else [code]) ++ rig = make_rig(tmp_path) ++ if code: ++ with pytest.raises(Refused): ++ attended(rig, entries) ++ assert rig[0].journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone()[0] == 0 ++ else: ++ assert attended(rig, entries)[1].lane == "issuer-lane" ++ ++ ++def test_attended_change_missing_session_is_parser_rejection(): ++ with pytest.raises(CatalogError, match="must name session_lane"): ++ _parse_entry(entries_for("missing-session-field")[0], 0) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_run.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_run.py new file mode 100644 index 00000000..60692b85 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_run.py @@ -0,0 +1,1072 @@ +"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. + +An owner must acknowledge each descriptor and provide an independent provenance +verifier. Descriptive case types remain plan-only. Nothing executes shell prose. +""" + +from __future__ import annotations + +from contextlib import contextmanager, redirect_stderr, redirect_stdout +from dataclasses import asdict, dataclass, replace +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import sqlite3 +import threading +import uuid +from typing import Callable + +import yaml +from .case_inputs import BrowserInputs +from .case_receipt_state import ( + Outcome, + Pins, + Receipt, + Refused, + StepState, + begin, + finish, + recover, + begin_rollback, + finish_rollback, +) + +IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") +MAX_SOURCE_BYTES = 262144 +MAX_HISTORY = 128 +OUTPUT_LOCK = threading.RLock() + + +def bounded_source(value: bytes): + if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: + raise Refused("source exceeds bounded input contract") + + +@contextmanager +def contained_output(): + """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" + with OUTPUT_LOCK: + saved = [] + null = os.open(os.devnull, os.O_WRONLY) + try: + for target in (1, 2): + saved.append((target, os.dup(target))) + os.dup2(null, target) + with redirect_stdout(_Discard()), redirect_stderr(_Discard()): + try: + yield + except Exception: + raise Refused("owner callback failed") from None + except BaseException: + raise OwnerInterrupted() from None + finally: + for target, original in saved: + os.dup2(original, target) + os.close(original) + os.close(null) + + +DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def digest(value: bytes) -> str: + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@dataclass(frozen=True, slots=True) +class Descriptor: + version: str + step: str + owner: str + adapter: str + adapter_source: str + actor: str + lane: str + target_parameter: str + effect: str + allowed_created: tuple[str, ...] + retry_safe: bool + revocation_scopes: tuple[str, ...] + + @classmethod + def decode(cls, source: bytes) -> Descriptor: + bounded_source(source) + try: + obj = json.loads(source) + if set(obj) != set(cls.__dataclass_fields__): + raise ValueError + if not isinstance(obj["allowed_created"], list): + raise ValueError + obj["allowed_created"] = tuple(obj["allowed_created"]) + if not isinstance(obj["revocation_scopes"], list): + raise ValueError + obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) + result = cls(**obj) + identifiers = ( + result.step, + result.owner, + result.adapter, + result.lane, + result.target_parameter, + *result.allowed_created, + ) + if result.version != "warden.case.execution.v1" or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers + ): + raise ValueError + if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( + "human", + "agent", + ): + raise ValueError + if ( + result.effect not in ("read", "change", "mint") + or type(result.retry_safe) is not bool + ): + raise ValueError + scopes = set(result.revocation_scopes) + if ( + len(scopes) != len(result.revocation_scopes) + or not scopes <= {"parent-session", "broker-grant"} + or "parent-session" not in scopes + ): + raise ValueError + if result.effect == "mint" and "broker-grant" not in scopes: + raise ValueError + if len(result.allowed_created) > 32: + raise ValueError + if len(set(result.allowed_created)) != len(result.allowed_created): + raise ValueError + return result + except (ValueError, TypeError, KeyError): + raise Refused("invalid closed execution descriptor") from None + + +@dataclass(frozen=True, slots=True) +class OwnerResult: + outcome: Outcome + evidence: str + created: tuple[str, ...] = () + revoked: bool = False + session_revocation_evidence: str | None = None + grant_revocation_evidence: str | None = None + attempt_id: str = "" + operation: str = "" + + +@dataclass(frozen=True, slots=True) +class Invocation: + step: str + owner: str + target: str + actor: str + principal: str + lane: str + pins: Pins + attempt_id: str = "" + operation: str = "admission" + tenant: str = "" + realm: str = "" + + +INTENT_SCHEMA = "warden.case.original-intent.v1" + + +@dataclass(frozen=True, slots=True) +class OriginalIntent: + """Durable record of the original mutating dispatch, written before dispatch. + + Binds the original Invocation (attempt, step, owner, target, actor, principal, + lane), all pins and the admitted adapter source. Current admit leaves tenant + and realm empty; their reserved fields are not admitted provenance. This is + local dispatch correspondence, not owner proof of creation. Owners still + authenticate original created-only provenance independently. + """ + + binding: str + invocation: Invocation + source: str + + def validate(self) -> None: + """Fixed bounded metadata only; tenant/realm are not authority claims. + + Current admit leaves tenant/realm empty. Nonempty future coordinates need + separate owner provenance; serializability alone must never supply it. + """ + if ( + type(self.invocation) is not Invocation + or type(self.invocation.pins) is not Pins + ): + raise Refused("exact original invocation required") + i = self.invocation + fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id) + if ( + not all( + type(value) is str and IDENTIFIER.fullmatch(value) for value in fields + ) + or type(i.actor) is not str + or i.actor not in ("human", "agent") + or type(i.operation) is not str + or i.operation not in ("run", "rollback") + or not all( + type(value) is str and (not value or IDENTIFIER.fullmatch(value)) + for value in (i.tenant, i.realm) + ) + or type(self.source) is not str + or not DIGEST.fullmatch(self.source) + or type(self.binding) is not str + or self.binding != f"{i.step}:{i.target}" + ): + raise Refused("invalid original intent metadata") + if set(Invocation.__dataclass_fields__) != { + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "pins", + "attempt_id", + "operation", + "tenant", + "realm", + }: + raise Refused("original invocation field contract changed") + if not all( + type(v) is str and DIGEST.fullmatch(v) + for v in ( + i.pins.case, + i.pins.case_type, + i.pins.adapter, + i.pins.catalog, + i.pins.acceptance, + i.pins.run_sheet, + ) + ): + raise Refused("invalid original intent pins") + + def payload(self) -> str: + self.validate() + return json.dumps( + {"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source} + | asdict(self.invocation), + separators=(",", ":"), + sort_keys=True, + ) + + @classmethod + def decode(cls, raw: str) -> OriginalIntent: + def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise Refused("duplicate original intent field") + result[key] = value + return result + + try: + if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: + raise Refused("original intent input exceeds bound") + obj = json.loads(raw, object_pairs_hook=unique) + fields = { + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "pins", + "attempt_id", + "operation", + "tenant", + "realm", + } + if ( + type(obj) is not dict + or set(obj) != fields | {"schema", "binding", "source"} + or obj["schema"] != INTENT_SCHEMA + ): + raise Refused("unknown original intent schema") + inv = {k: obj[k] for k in fields} + inv["pins"] = Pins(**inv["pins"]) + result = cls(obj["binding"], Invocation(**inv), obj["source"]) + result.validate() + return result + except (TypeError, ValueError, KeyError): + raise Refused("invalid original intent record") from None + + +@dataclass(frozen=True, slots=True) +class ReconcileContext: + """Original intent and the new reconciliation attempt, kept separate.""" + + original: OriginalIntent + attempt: Invocation + + +@dataclass(frozen=True, slots=True) +class Adapter: + reference: str + source: str + invoke: Callable[[Invocation], OwnerResult] + verify: Callable[[Invocation, OwnerResult], bool] + rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None + reconcile: Callable[[Invocation], OwnerResult] | None = None + reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None + + +class Journal: + """Private SQLite journal plus process lock; transaction commits before dispatch.""" + + def __init__(self, directory: Path): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + if ( + directory.is_symlink() + or directory.stat().st_uid != os.getuid() + or directory.stat().st_mode & 0o077 + ): + raise Refused("journal directory is not private") + self.directory = directory + for name in ("journal.sqlite", "execution.lock"): + path = directory / name + fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + try: + if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: + raise Refused("journal file is not private") + finally: + os.close(fd) + self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) + self.db.execute("PRAGMA synchronous=FULL") + self.db.execute( + "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + self.db.execute( + "CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)" + ) + + def original_intent( + self, binding: str, current: Invocation, source: str + ) -> OriginalIntent: + """Latest original run intent for binding, refused unless it matches exactly. + + Missing or mismatched records refuse; the step stays uncertain. + """ + row = self.db.execute( + "SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + raise Refused("original intent record missing; step stays uncertain") + intent = OriginalIntent.decode(row[1]) + o = intent.invocation + if row[0] != o.attempt_id: + raise Refused("original intent attempt identity differs") + if intent.binding != binding or o.operation != "run": + raise Refused("original intent binding mismatch") + if o.pins != current.pins: + raise Refused("original intent pins mismatch") + if intent.source != source: + raise Refused("original intent source mismatch") + for name in ( + "step", + "owner", + "target", + "actor", + "principal", + "lane", + "tenant", + "realm", + ): + if getattr(o, name) != getattr(current, name): + raise Refused(f"original intent {name} mismatch") + # Match the persisted original running receipt. An orphan context or a + # payload from another attempt cannot become the current lookup key. + receipts = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?", + (binding, MAX_HISTORY + 1), + ).fetchall() + if len(receipts) > MAX_HISTORY: + raise Refused("original intent receipt history exceeds bound") + matched = False + try: + for (raw,) in receipts: + if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: + raise Refused("original receipt input exceeds bound") + payload = json.loads(raw) + if ( + payload.get("attempt_id") != o.attempt_id + or payload.get("operation") != "run" + ): + continue + last = payload.get("receipts", [])[-1] + if last.get("outcome") != "running": + continue + if ( + payload.get("journal_schema") != "warden.case.step-journal.v1" + or payload.get("step_id") != o.step + or payload.get("owner") != o.owner + or payload.get("pins") != asdict(o.pins) + ): + raise Refused("original running receipt binding differs") + matched = True + break + except (TypeError, ValueError, KeyError, IndexError, AttributeError): + raise Refused("invalid original running receipt") from None + if not matched: + raise Refused("original running receipt missing; no backfill") + return intent + + @contextmanager + def locked(self): + with (self.directory / "execution.lock").open("rb") as fh: + try: + fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused("case execution already active") from None + try: + yield + finally: + fcntl.flock(fh, fcntl.LOCK_UN) + + def load(self, binding: str, initial: StepState) -> StepState: + row = self.db.execute( + "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", + (binding,), + ).fetchone() + if row is None: + return initial + obj = json.loads(row[0]) + expected = { + "step_id", + "owner", + "pins", + "allowed_created", + "retry_safe", + "receipts", + "journal_schema", + "evidence_digest", + "session_revoked", + "session_revocation_digest", + "grant_revocation_digest", + "attempt_id", + "operation", + } + if ( + set(obj) != expected + or obj["journal_schema"] != "warden.case.step-journal.v1" + ): + raise Refused("unknown journal receipt schema") + state = StepState( + obj["step_id"], + obj["owner"], + Pins(**obj["pins"]), + tuple(obj["allowed_created"]), + obj["retry_safe"], + tuple( + Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) + for r in obj["receipts"] + ), + ) + if ( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ) != ( + initial.step_id, + initial.owner, + initial.pins, + initial.allowed_created, + initial.retry_safe, + ): + raise Refused("journal pins changed") + return state + + def save( + self, + binding: str, + state: StepState, + evidence: OwnerResult | None = None, + invocation: Invocation | None = None, + intent: OriginalIntent | None = None, + ): + if intent is not None: + if type(intent) is not OriginalIntent: + raise Refused("exact original intent required") + intent.validate() + expected = ( + Outcome.RUNNING + if intent.invocation.operation == "run" + else Outcome.ROLLBACK_RUNNING + ) + if ( + intent.binding != binding + or intent.invocation != invocation + or intent.invocation.pins != state.pins + or not state.last + or state.last.outcome is not expected + ): + raise Refused("original intent and running receipt differ") + self.db.execute("BEGIN IMMEDIATE") + try: + if intent is not None: + self.db.execute( + "INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)", + (intent.invocation.attempt_id, binding, intent.payload()), + ) + self.db.execute( + "INSERT INTO receipts(binding,payload) VALUES (?,?)", + ( + binding, + json.dumps( + asdict(state) + | { + "journal_schema": "warden.case.step-journal.v1", + "attempt_id": invocation.attempt_id if invocation else None, + "operation": invocation.operation if invocation else None, + "evidence_digest": digest(evidence.evidence.encode()) + if evidence + else None, + "session_revoked": evidence.revoked if evidence else None, + "session_revocation_digest": digest( + evidence.session_revocation_evidence.encode() + ) + if evidence and evidence.session_revocation_evidence + else None, + "grant_revocation_digest": digest( + evidence.grant_revocation_evidence.encode() + ) + if evidence and evidence.grant_revocation_evidence + else None, + }, + separators=(",", ":"), + ), + ), + ) + self.db.execute("COMMIT") + except BaseException: + self.db.execute("ROLLBACK") + raise + + +def _catalog_lane(catalog_source: bytes, descriptor: Descriptor): + """KERNEL-2: the descriptor's lane must be an active, fitting catalog entry. + + catalog-routing-acceptance still covers the whole catalog digest; this + ties the one lane the descriptor names to an entry inside that digest. + """ + from .routing.catalog import CatalogError, _parse_entry + + try: + raw = yaml.safe_load(catalog_source) + items = raw["entries"] + if not isinstance(items, list): + raise TypeError + entries = {} + for index, item in enumerate(items): + parsed = _parse_entry(item, index) + if parsed.id in entries: + raise ValueError + entries[parsed.id] = parsed + except (yaml.YAMLError, CatalogError, KeyError, TypeError, ValueError): + raise Refused("invalid catalog source shape") from None + entry = entries.get(descriptor.lane) + if entry is None: + raise Refused("descriptor lane is not in the catalog") + if not entry.is_active: + raise Refused("descriptor lane is not active in the catalog") + if entry.delegation is None: + raise Refused("descriptor lane has no catalog delegation record") + if not entry.is_graded: + raise Refused("descriptor lane carries no risk grade") + if descriptor.owner not in entry.owner_repo.split("/"): + raise Refused("descriptor owner does not fit catalog lane owner_repo") + if descriptor.effect in ("mint", "change") and entry.risk != "high": + raise Refused("catalog lane risk does not cover the descriptor effect") + return entry, entries + + +def _attended_admission(descriptor, actor, entry, entries): + from .routing.models import attended_lane_issue + + if (actor != "human" or "WARDEN_AGENT_ID" in os.environ + or "parent-session" not in descriptor.revocation_scopes): + raise Refused("attended step requires a human parent session") + issue = attended_lane_issue(entry, entries.get) + if issue: + raise Refused(issue[1]) + + +def _step_mode(case_type: dict, case: dict, descriptor: Descriptor) -> str: + """KERNEL-3: warden, attended or owner, from the type step's own text.""" + steps = [ + step + for step in case_type.get("follow_through") or [] + if isinstance(step, dict) and step.get("id") == descriptor.step + ] + step = steps[0] if len(steps) == 1 else {} + executes, attended = step.get("warden_executes"), step.get("attended") + if ( + type(executes) is not bool + or type(attended) is not bool + or (executes and attended) + or not isinstance(step.get("lane"), str) + ): + raise Refused("type step does not declare how the kernel dispatches it") + owner = step.get("owner") + if isinstance(owner, str) and re.fullmatch(r"\{[a-z_]+\}", owner): + owner = (case.get("parameters") or {}).get(owner[1:-1]) + if step["lane"] != descriptor.lane or owner != descriptor.owner: + raise Refused("type step lane or owner disagrees with descriptor") + return "warden" if executes else "attended" if attended else "owner" + + +class Executor: + """Injected fixed owner adapters and provenance verifier; none ship by default. + + evidence verifier must authenticate owner custody/provenance or explicitly + attest attended manual evidence; a bare JSON reference is never sufficient. + It receives purpose, exact source digest, and named evidence reference. + """ + + def __init__( + self, + journal: Journal, + adapters: dict[str, Adapter], + evidence_verifier: Callable[[str, str, str], bool], + *, + browser_inputs: BrowserInputs | None = None, + ): + self.journal, self.adapters, self.evidence_verifier = ( + journal, + dict(adapters), + evidence_verifier, + ) + self._admitted = {} + self.browser_inputs = browser_inputs + + def admit( + self, + case_source: bytes, + type_source: bytes, + descriptor_source: bytes, + catalog_source: bytes, + *, + principal: str, + actor: str, + lane: str, + acceptance_evidence: str, + materialization_evidence: str, + descriptor_evidence: str, + session_evidence: str, + ) -> tuple[Descriptor, Invocation, StepState]: + for source in (case_source, type_source, descriptor_source, catalog_source): + bounded_source(source) + descriptor = Descriptor.decode(descriptor_source) + try: + case = yaml.safe_load(case_source)["spec"] + case_type = yaml.safe_load(type_source)["spec"] + if not isinstance(case, dict) or not isinstance(case_type, dict): + raise ValueError + except (yaml.YAMLError, KeyError, TypeError, ValueError): + raise Refused("invalid case or type source shape") from None + if case.get("status") != "materialized" or not case.get("acceptance"): + raise Refused("case is not accepted and structurally materialized") + if case_type.get("status") not in ("acknowledged", "implemented"): + raise Refused("type is not acknowledged") + if case.get("type") != f"{case_type['id']}@{case_type['version']}": + raise Refused("case type pin mismatch") + if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( + descriptor_source + ): + raise Refused( + "type has no exact acknowledged machine descriptor; plan only" + ) + for purpose, source, evidence in ( + ("case-acceptance", digest(case_source), acceptance_evidence), + ( + "ops-mason-materialization", + digest(case_source), + materialization_evidence, + ), + ( + "owner-descriptor-acceptance:" + descriptor.owner, + digest(descriptor_source), + descriptor_evidence, + ), + ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), + ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), + ( + f"caller-session:{actor}:{principal}:{lane}", + digest(case_source), + session_evidence, + ), + ): + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, source, evidence) is not True + ): + raise Refused("independent owner evidence not verified") + if ( + not IDENTIFIER.fullmatch(principal) + or actor != descriptor.actor + or lane != descriptor.lane + ): + raise Refused("principal actor or lane mismatch") + if descriptor.effect == "mint" and ( + actor != "human" or os.getenv("WARDEN_AGENT_ID") + ): + raise Refused("agent mint is forbidden") + entry, entries = _catalog_lane(catalog_source, descriptor) + mode = _step_mode(case_type, case, descriptor) + if mode == "attended": + _attended_admission(descriptor, actor, entry, entries) + if mode == "owner" and ( + entry.effective_delegation.mode != "native" + or entry.owner_repo != descriptor.owner + or entry.warden_executes + or descriptor.effect == "mint" + ): + raise Refused("owner step lane is not the owner's native path") + parameters = case.get("parameters", {}) + declared = { + item["name"]: item + for item in case_type.get("parameters", []) + if isinstance(item, dict) and isinstance(item.get("name"), str) + } + if ( + not isinstance(parameters, dict) + or descriptor.target_parameter not in declared + ): + raise Refused("target parameter is not declared by acknowledged type") + if case_type.get("id") == "oidc-browser-relying-party": + if type(self.browser_inputs) is not BrowserInputs: + raise Refused("browser metadata validator is not configured") + target = self.browser_inputs.target( + yaml.safe_load(case_source), + yaml.safe_load(type_source), + descriptor.target_parameter, + ) + else: + for name in parameters: + definition = declared.get(name) + if ( + definition is None + or definition.get("sensitivity") not in ("public", "operational") + or definition.get("case_settable") is False + ): + raise Refused("unknown, fixed or secret-sensitivity case parameter") + target = parameters.get(descriptor.target_parameter) + if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): + raise Refused("target must be exact case metadata") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("unknown adapter or owner source pin") + sheet = json.dumps( + [ + descriptor.step, + descriptor.owner, + target, + actor, + principal, + lane, + acceptance_evidence, + materialization_evidence, + descriptor_evidence, + session_evidence, + ], + separators=(",", ":"), + ).encode() + pins = Pins( + digest(case_source), + digest(type_source), + digest(descriptor_source), + digest(catalog_source), + digest(acceptance_evidence.encode()), + digest(sheet), + ) + invocation = Invocation( + descriptor.step, descriptor.owner, target, actor, principal, lane, pins + ) + initial = StepState( + descriptor.step, + descriptor.owner, + pins, + descriptor.allowed_created, + descriptor.retry_safe, + ) + # Keep the mode and exact authenticated catalog bytes with this admission; + # dispatch must not substitute a later ambient catalog or lose its mode. + self._admitted[invocation] = ( + descriptor, initial, session_evidence, mode, catalog_source + ) + return descriptor, invocation, initial + + def _result( + self, adapter: Adapter, invocation: Invocation, result: OwnerResult + ) -> OwnerResult: + if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): + raise Refused("unknown owner result shape") + if type(result.created) is not tuple or not all( + isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created + ): + raise Refused("owner created-object result invalid") + if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( + result.evidence + ): + raise Refused("owner evidence identifier invalid") + if adapter.verify(invocation, result) is not True or result.revoked is not True: + raise Refused("owner result or session revocation unverified") + if (result.attempt_id, result.operation) != ( + invocation.attempt_id, + invocation.operation, + ): + raise Refused("owner receipt belongs to another attempt") + descriptor = self._admitted[ + replace(invocation, attempt_id="", operation="admission") + ][0] + proof_fields = { + "parent-session": ( + f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.session_revocation_evidence, + ), + "broker-grant": ( + f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", + result.grant_revocation_evidence, + ), + } + proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] + for purpose, evidence in proofs: + if ( + not isinstance(evidence, str) + or not IDENTIFIER.fullmatch(evidence) + or self._verify_evidence(purpose, invocation.pins.case, evidence) + is not True + ): + raise Refused("independent grant or parent-session revocation absent") + return result + + def _verify_evidence(self, purpose, source, evidence): + try: + with contained_output(): + return self.evidence_verifier(purpose, source, evidence) is True + except Exception: + raise Refused("independent evidence verification failed") from None + + def _admission( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ): + if self._admitted.get(invocation, ())[:2] != (descriptor, initial): + raise Refused("execution requires verified admission on this executor") + adapter = self.adapters.get(descriptor.adapter) + if adapter is None or (adapter.reference, adapter.source) != ( + descriptor.adapter, + descriptor.adapter_source, + ): + raise Refused("adapter changed after admission") + _, _, session, mode, catalog_source = self._admitted[invocation] + if ( + self._verify_evidence( + f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", + invocation.pins.case, + session, + ) + is not True + ): + raise Refused("caller session no longer verified") + if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): + raise Refused("agent mint is forbidden") + if mode == "attended": + entry, entries = _catalog_lane(catalog_source, descriptor) + _attended_admission(descriptor, invocation.actor, entry, entries) + return adapter + + def _attempt(self, invocation: Invocation, state: StepState, operation: str): + identity = json.dumps( + [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], + separators=(",", ":"), + ).encode() + return replace( + invocation, + attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), + operation=operation, + ) + + def run( + self, + descriptor: Descriptor, + invocation: Invocation, + initial: StepState, + *, + retry=False, + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + recovered = recover(state) + if recovered != state: + self.journal.save(binding, recovered) + state = recovered + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin(state, invocation.pins, retry=retry) + dispatched = self._attempt(invocation, running, "run") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, dispatched, adapter.invoke(dispatched) + ) + completed = finish( + running, invocation.pins, result.outcome, created=result.created + ) + evidence = result + except Exception: + completed = finish(running, invocation.pins, Outcome.UNCERTAIN) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def rollback( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + adapter = self._admission(descriptor, invocation, initial) + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = self.journal.load(binding, initial) + if recover(state) != state: + self.journal.save(binding, recover(state)) + raise Refused("lost rollback result requires owner reconciliation") + if adapter.rollback is None: + raise Refused("owner has no reviewed undo adapter") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + running = begin_rollback(state, invocation.pins) + dispatched = self._attempt(invocation, running, "rollback") + self.journal.save( + binding, + running, + invocation=dispatched, + intent=OriginalIntent(binding, dispatched, adapter.source), + ) + evidence = None + try: + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.rollback(dispatched, running.last.created), + ) + verified = result.outcome is Outcome.ROLLED_BACK and not result.created + evidence = result if verified else None + except Exception: + verified = False + completed = finish_rollback(running, invocation.pins, verified=verified) + self.journal.save(binding, completed, evidence, dispatched) + return completed + + def reconcile( + self, descriptor: Descriptor, invocation: Invocation, initial: StepState + ) -> StepState: + """Explicit owner-native state inspection; never redispatches the mutation.""" + adapter = self._admission(descriptor, invocation, initial) + if adapter.reconcile is None and adapter.reconcile_original is None: + raise Refused("owner reconciliation adapter absent") + with self.journal.locked(): + binding = f"{invocation.step}:{invocation.target}" + state = recover(self.journal.load(binding, initial)) + if not state.last or state.last.outcome not in ( + Outcome.UNCERTAIN, + Outcome.FAILED_AFTER, + ): + raise Refused("no uncertain mutation to reconcile") + if len(state.receipts) > MAX_HISTORY - 4: + raise Refused("bounded journal history requires owner archival") + state = replace( + state, + receipts=( + *state.receipts, + Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), + ), + ) + original = ( + self.journal.original_intent(binding, invocation, adapter.source) + if adapter.reconcile_original is not None + else None + ) + dispatched = self._attempt(invocation, state, "reconcile") + self.journal.save(binding, state, invocation=dispatched) + with contained_output(): + result = self._result( + adapter, + dispatched, + adapter.reconcile_original(ReconcileContext(original, dispatched)) + if original is not None + else adapter.reconcile(dispatched), + ) + if result.outcome not in ( + Outcome.NO_CHANGE, + Outcome.SUCCESS, + Outcome.FAILED_BEFORE, + ): + raise Refused("owner did not reconcile target state") + # Validate created-object scope using the same closed result reducer. + finish( + begin( + StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ), + state.pins, + ), + state.pins, + result.outcome, + created=result.created, + ) + reconciled = StepState( + state.step_id, + state.owner, + state.pins, + state.allowed_created, + state.retry_safe, + ( + *state.receipts, + Receipt(len(state.receipts) + 1, result.outcome, result.created), + ), + ) + self.journal.save(binding, reconciled, result, dispatched) + return reconciled + + +class OwnerInterrupted(KeyboardInterrupt): + """Sanitized interruption preserves persisted running/crash recovery semantics.""" + + def __init__(self): + super().__init__("owner execution interrupted") + + +class _Discard: + """Never retain or forward adapter output, including unbounded malicious text.""" + + def write(self, text): + return len(text) + + def flush(self): + pass diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_type.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_type.py new file mode 100644 index 00000000..1454cd36 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/case_type.py @@ -0,0 +1,173 @@ +"""SecurityCaseType follow-through check (WARDEN-WP-0044-T01). + +Checks the ``follow_through`` section of a NetKingdom SecurityCaseType against +the rules ops-warden needs before it can plan a run (canon +``security-case-types_v0.1`` section on follow_through; ops-warden review +``history/2026-10-08-oidc-rp-follow-through-review.md``, R1-R3, R5-R7). + +Two kinds of finding: + +* ``schema`` — the type itself is wrong; net-kingdom fixes the type. +* ``catalog`` — the type is right but a lane it names is not yet a reviewed, + active catalog entry; the lane's owner supplies it. ``warden case plan`` + (T02) refuses to plan while any catalog finding remains. + +Read-only: it parses YAML and the catalog, executes nothing, and never handles a +secret value. +""" +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, List, Optional + +import yaml + +from warden.routing.catalog import Catalog +from warden.routing.models import attended_lane_issue + +VALUE_HANDLING = ("none", "generated_in_custody") +REQUIRED_STEP_FIELDS = ("id", "owner", "entry_point", "auth", "warden_executes", "attended", "value_handling") + + +@dataclass(frozen=True) +class Finding: + kind: str # "schema" | "catalog" + code: str + where: str + message: str + + def as_dict(self) -> dict: + return {"kind": self.kind, "code": self.code, "where": self.where, "message": self.message} + + +@dataclass(frozen=True) +class FollowThroughReport: + type_id: str + version: str + findings: List[Finding] + attended_lanes: List[str] # one attended session per lane, in step order + warden_steps: List[str] + + @property + def schema_ok(self) -> bool: + return not any(f.kind == "schema" for f in self.findings) + + @property + def plannable(self) -> bool: + return not self.findings + + def as_dict(self) -> dict: + return { + "type": self.type_id, + "version": self.version, + "schema_ok": self.schema_ok, + "plannable": self.plannable, + "warden_steps": self.warden_steps, + "attended_sessions": self.attended_lanes, + "findings": [f.as_dict() for f in self.findings], + } + + +def load_type(path: Path) -> dict: + with open(path) as fh: + doc = yaml.safe_load(fh) + if not isinstance(doc, dict) or doc.get("kind") != "SecurityCaseType": + raise ValueError(f"{path}: not a SecurityCaseType document") + return doc + + +def _bool(step: dict, key: str) -> Optional[bool]: + value = step.get(key) + return value if isinstance(value, bool) else None + + +def check_follow_through(doc: dict, catalog: Optional[Catalog] = None) -> FollowThroughReport: + spec = doc.get("spec") or {} + steps = spec.get("follow_through") or [] + findings: List[Finding] = [] + attended_lanes: List[str] = [] + warden_steps: List[str] = [] + + def schema(code: str, where: str, message: str) -> None: + findings.append(Finding("schema", code, where, message)) + + if not steps: + schema("FT-EMPTY", "follow_through", "type declares no follow-through steps") + + seen: set = set() + for index, step in enumerate(steps): + sid = step.get("id") if isinstance(step, dict) else None + where = f"follow_through[{sid or index}]" + if not isinstance(step, dict): + schema("FT-SHAPE", where, "step is not a mapping") + continue + for key in REQUIRED_STEP_FIELDS: + if key not in step or step[key] in (None, ""): + schema("FT-FIELD", where, f"missing required field '{key}'") + if sid in seen: + schema("FT-DUP", where, "duplicate step id") + seen.add(sid) + + executes = _bool(step, "warden_executes") + attended = _bool(step, "attended") + for key, value in (("warden_executes", executes), ("attended", attended)): + if key in step and value is None: + schema("FT-BOOL", where, f"'{key}' must be a boolean") + + # R3: values never pass through the run. + handling = step.get("value_handling") + if handling is not None and handling not in VALUE_HANDLING: + schema("FT-VALUE", where, f"value_handling '{handling}' not in {list(VALUE_HANDLING)}") + if handling == "generated_in_custody" and attended: + schema("FT-VALUE", where, "a generated-in-custody step may not run in an attended session") + + # Attended steps: justified, and never executed by warden itself. + if attended: + if not step.get("attended_justification"): + schema("FT-ATTEND", where, "attended step carries no attended_justification") + if executes: + schema("FT-ATTEND", where, "a step is either warden_executes or attended, not both") + + # R1: a lane for every step warden executes or groups into a session. + lane = step.get("lane") + if (executes or attended) and not lane: + schema("FT-LANE", where, "warden_executes or attended step names no catalog lane") + if lane and catalog is not None: + entry = catalog.get(lane) + if entry is None: + findings.append(Finding("catalog", "LANE-MISSING", where, f"lane '{lane}' is not in the catalog")) + elif not entry.is_active or not entry.reviewed: + findings.append(Finding("catalog", "LANE-UNREVIEWED", where, f"lane '{lane}' is not active and reviewed")) + elif attended: + issue = attended_lane_issue(entry, catalog.get) + if issue: + findings.append(Finding("catalog", issue[0], where, issue[1])) + + if executes: + warden_steps.append(sid) + if attended and lane and lane not in attended_lanes: + attended_lanes.append(lane) + + # R7: verification never reads a value. + for index, item in enumerate(spec.get("verification") or []): + if not isinstance(item, dict) or item.get("reads_secret_value") is not False: + vid = item.get("id", index) if isinstance(item, dict) else index + schema("VER-VALUE", f"verification[{vid}]", "reads_secret_value must be explicitly false") + + # Every step must be reversible (or explicitly the consumer's to undo). + reversed_ids = {r.get("reverses") for r in spec.get("rollback") or [] if isinstance(r, dict)} + for sid in sorted(s for s in seen if s and s not in reversed_ids): + schema("RB-MISSING", f"follow_through[{sid}]", "no rollback entry reverses this step") + + return FollowThroughReport( + type_id=str(spec.get("id", "")), + version=str(spec.get("version", "")), + findings=findings, + attended_lanes=attended_lanes, + warden_steps=warden_steps, + ) + + +def summarize(findings: Iterable[Finding]) -> str: + return "\n".join(f"[{f.kind}] {f.code} {f.where}: {f.message}" for f in findings) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/routing/models.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/routing/models.py new file mode 100644 index 00000000..c691b33b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/src/warden/routing/models.py @@ -0,0 +1,343 @@ +"""Data model for routing catalog entries. + +A `RouteEntry` is a pointer: it names the owner and the authoritative doc for a +credential need. Only the SSH lane (`warden_executes: true`) may carry an authored +`steps` block and a `cert_command` pattern — every other entry is identifiers and +pointers only (the no-double-source rule, enforced in `catalog.py`). +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable, List, Optional + + +# Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including +# the "ungraded" default and any value from a newer catalog — are treated as +# high by is_high_risk, so the read-boundary fails safe in both directions. +LOW_RISK_GRADES = frozenset({"standard", "low", "accepted"}) +GRADED_RISK = frozenset({"standard", "low", "accepted", "high", "critical"}) + + +@dataclass +class RotationGuide: + """Structured-but-advisory renewal guidance for a lane (WARDEN-WP-0026 T06). + + Held in the ops-warden registry, never in OpenBao. ``steps`` are authored + advisory prose (screened for secret material like every catalog string) — they + tell an operator *how* to renew, they are not executed here. ``method`` is + ``rotate`` (provider re-mints the same kind of credential) or ``re-establish`` + (regenerate from source, e.g. a new age keypair + re-encrypt). ``automatable`` + is a hint for a future Strand-B executable driver (WARDEN-WP-0027). + """ + method: str # "rotate" | "re-establish" + steps: List[str] + owner: str + automatable: bool = False + + +# Risk classes for agent read-boundary (WARDEN-WP-0026 T04). +# high — recovery escrow, upload tokens, admin PATs, high-spend provider keys. +# Agent identities must not hold raw data-read (metadata/capabilities only). +# standard — ordinary workload secrets (ESO-fed, non-escrow); normal least-privilege. +VALID_RISK = ("standard", "high") + +# Delegation modes (WARDEN-WP-0030). Absence of a block is implicit interim. +# native — intended owner already fronts the lane (route-primary / pointer) +# interim — ops-warden covers a gap; intended_owner + blocked_on required +# permanent — ops-warden is the designed owner of this front door (SSH only today) +VALID_DELEGATION_MODES = ("native", "interim", "permanent") +VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable") + +IMPLICIT_DELEGATION_BLOCKED_ON = ( + "unclassified — no delegation block; treat as a question, not a settlement" +) + + +@dataclass +class Delegation: + """Who should own this front door, and what is missing (WARDEN-WP-0030). + + Pointer-layer only: names the intended owner and the blocker. Does not + restate how that owner will implement their front door. + """ + + mode: str # native | interim | permanent + intended_owner: Optional[str] = None + blocked_on: Optional[str] = None + reviewed: Optional[str] = None + verified: Optional[str] = None + implicit: bool = False + + #: How `reviewed` was established. The distinction exists because a date + #: bumped by editing the entry looks identical to one bumped by re-checking + #: the blocker, and on 2026-08-21 six lanes read as freshly reviewed when + #: only some had actually been re-verified (WARDEN-WP-0033-T05). + #: + #: `asked-and-waiting` deliberately does NOT count as verification: it is the + #: state the secrets-engine lanes sat in for ten days while looking fresh. + VERIFICATION_METHODS = ( + "owner-confirmed", # the intended owner stated the blocker's status + "source-read", # re-derived from the owner's code, canon, or CCR + "asked-and-waiting", # a question is outstanding — NOT verification + "unverified", # carried forward without a check + ) + #: Methods that mean the claim was actually re-established. + VERIFYING_METHODS = ("owner-confirmed", "source-read") + + @property + def is_verified(self) -> bool: + """True only when the blocker was re-established, not merely re-edited.""" + return self.verified in self.VERIFYING_METHODS + + def to_dict(self) -> dict: + return { + "mode": self.mode, + "intended_owner": self.intended_owner, + "blocked_on": self.blocked_on, + "reviewed": self.reviewed, + "verified": self.verified, + "is_verified": self.is_verified, + "implicit": self.implicit, + } + + +@dataclass(frozen=True) +class WorkloadReference: + """Authoritative workload join for a catalog lane (WARDEN-WP-0032). + + Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple. + Independently governed operational workloads use ``name`` plus an owner + declaration reference. An applicable lane whose owner has not published an + identity remains explicitly ``unknown``; it is never inferred from the + credential path or repository name. + """ + + applicability: str # applicable | not-applicable + rapp_id: Optional[str] = None + name: Optional[str] = None + deployable: Optional[str] = None + declaration_ref: Optional[str] = None + reason: Optional[str] = None + unknown_reason: Optional[str] = None + + @property + def resolution(self) -> str: + if self.applicability == "not-applicable": + return "not-applicable" + if self.unknown_reason: + return "unknown" + return "resolved" + + def to_dict(self) -> dict: + return { + "applicability": self.applicability, + "rapp_id": self.rapp_id, + "name": self.name, + "deployable": self.deployable, + "declaration_ref": self.declaration_ref, + "reason": self.reason, + "unknown_reason": self.unknown_reason, + "resolution": self.resolution, + } + + +@dataclass +class RouteEntry: + id: str + title: str + need_keywords: List[str] + owner_repo: str + subsystem: str + warden_executes: bool + wiki_ref: str + canon_ref: str + reviewed: str + status: str # "active" | "draft" + # Explicit workload applicability and authoritative join. Never inferred. + workload_ref: Optional[WorkloadReference] = None + # SSH lane only — None/empty for routed (non-executed) needs. + steps: List[str] = field(default_factory=list) + cert_command: Optional[str] = None + # Structured handoff (WP-0014) — optional, allowed on any lane. These are + # *templates and pointers* the `warden access` assist layer renders (and, for + # exec_capable lanes, proxies). They are NOT authored procedure prose and they + # never carry a secret value — only placeholders (`<...>`) and field names. + # Validation in catalog.py enforces the no-secret-material rule on every one. + auth_method: Optional[str] = None # how the caller authenticates to the owner + path_template: Optional[str] = None # owner-side path with `<...>` placeholders + fetch_command: Optional[str] = None # command skeleton run *as the caller* + exec_capable: bool = False # may `warden access --fetch/--exec` proxy it + policy_ref: Optional[str] = None # flex-auth check the fetch path runs first + # Proxy lane semantics (WP-0014 T4): + # "secret" — read a value (gated by flex-auth secret-read; caller must already + # be authenticated; value transits via inherit-stdout or child env). + # "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have + # no identity yet), no caller-auth precheck (the point is to get one), + # run interactively as the caller; warden never captures the token. + # "ceremony" — attended owner operation such as Shamir seal/unseal. It is a + # pointer plus approval boundary, never an executable access lane. + lane: str = "secret" + session_lane: Optional[str] = None # attended-change: login lane whose session establishes it + # Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem + # (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the + # catalog routes to it and keeps ops-warden's own --fetch/--exec proxy as a transparent + # fallback (route-primary, proxy-fallback). Pointers/templates only — never a value. + exec_owner: Optional[str] = None # subsystem owning the native exec (e.g. secrets-engine) + exec_command: Optional[str] = None # e.g. "secrets-engine exec --catalog -- " + pointer_command: Optional[str] = None # e.g. "secrets-engine route --json" + # Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values. + rotation: Optional[RotationGuide] = None + # Agent read-boundary risk class (WP-0026 T04). high → agents use wrap/out/exec only. + # Default is "ungraded", which FAILS SAFE: it is treated as high. Before + # ADR-0007 this defaulted to "standard", so a lane that simply omitted the + # field was silently placed outside the read-boundary (RISK-F-0003) — the + # control was never relaxed by decision, it was never reached. + risk: str = "ungraded" # "standard" | "high" | "ungraded" + # Delegation register (WP-0030). None → implicit interim with unknown owner. + delegation: Optional[Delegation] = None + + @property + def is_active(self) -> bool: + return self.status == "active" + + @property + def is_graded(self) -> bool: + """False when this lane carries no explicit risk grade (ADR-0007).""" + return self.risk in GRADED_RISK + + @property + def is_high_risk(self) -> bool: + """True when this lane is on the agent raw-read deny list (WP-0026 T04). + + Anything not explicitly graded low is high. An ungraded lane, or one + carrying a grade this version does not recognise, is treated as high + rather than waved through — ADR-0007: absence is not a grade. + """ + return self.risk not in LOW_RISK_GRADES + + def risk_for_zone( + self, + *, + effective_zone: str = "unknown", + admission: str = "unknown", + synthetic_only: bool = False, + ) -> str: + """Resolve an absent grade using security-zones_v0.1 section 5.1. + + Explicit grades always win. The sole lower default is a satisfied + ``z0-experimental`` workload proven synthetic-only. Every other zone, + failed/unknown admission, and missing context fails safe to at least + ``high``; z3 reports ``critical`` (which the read boundary treats as + high). Catalog CI still requires explicit grades, so this is the safe + runtime behavior for malformed or newer external catalogs. + """ + if self.is_graded: + return self.risk + if ( + effective_zone == "z0-experimental" + and admission == "satisfied" + and synthetic_only + ): + return "standard" + if effective_zone == "z3-critical" and admission == "satisfied": + return "critical" + return "high" + + @property + def has_rotation(self) -> bool: + """True when this lane carries renewal guidance (WP-0026 T06).""" + return self.rotation is not None + + @property + def vends_secret(self) -> bool: + """True when this lane hands back a rotatable static secret value. + + Rotation guidance (WP-0026 T06) applies to these. It excludes the SSH lane + (short-lived certs — renewal is re-issuance), ``login`` lanes (re-auth, no + stored value), and pure routing pointers with no secret path (tunnel, + principals, emission sinks, policy checks). + """ + if self.warden_executes or self.lane != "secret": + return False + return bool(self.path_template or self.fetch_command or self.exec_owner) + + @property + def has_native_exec(self) -> bool: + """True when an owner-native exec front door is the primary path for this lane.""" + return bool(self.exec_owner and self.exec_command) + + @property + def effective_delegation(self) -> Delegation: + """Declared delegation, or implicit interim with an unknown owner. + + Absence of a ``delegation:`` block is a question (WP-0030), not a + settlement that ops-warden owns the front door. + """ + if self.delegation is not None: + return self.delegation + return Delegation( + mode="interim", + intended_owner=None, + blocked_on=IMPLICIT_DELEGATION_BLOCKED_ON, + reviewed=None, + implicit=True, + ) + + @property + def is_interim(self) -> bool: + """True when this lane is a tracked gap (explicit or implicit).""" + return self.effective_delegation.mode == "interim" + + @property + def has_handoff(self) -> bool: + """True when structured assist fields are present (advisory richness).""" + return any((self.auth_method, self.path_template, self.fetch_command)) + + @property + def resolvable(self) -> bool: + """True when `warden access --fetch` can run this lane with no further input. + + A resolvable lane is active, exec_capable, and its fetch command (with the path + inlined) carries no unresolved ``<...>`` placeholder. Template lanes — like the + generic ``openbao-api-key`` or the ````-parameterized login — are *not* + resolvable until an owner ships concrete names. Lets an automated caller know + whether ``--fetch`` will work *before* attempting it (whynot-design request). + """ + if not (self.is_active and self.exec_capable and self.fetch_command): + return False + blob = f"{self.fetch_command} {self.path_template or ''}" + return "<" not in blob and ">" not in blob + + def match_score(self, tokens: List[str]) -> int: + """Keyword-overlap score against need_keywords, title, and id. + + Pure ranking helper — no I/O, no external calls. + """ + haystack = set(k.lower() for k in self.need_keywords) + haystack.update(self.id.lower().replace("-", " ").split()) + haystack.update(self.title.lower().replace("-", " ").split()) + score = 0 + for tok in tokens: + t = tok.lower() + if t in haystack: + score += 2 + elif any(t in h or h in t for h in haystack): + score += 1 + return score + + +def attended_lane_issue( + entry: RouteEntry, get_entry: Callable[[str], Optional[RouteEntry]] +) -> Optional[tuple[str, str]]: + """Shared planner/executor boundary, using one parsed catalog snapshot.""" + if not entry.is_active or not entry.reviewed: + return ("LANE-UNREVIEWED", f"lane '{entry.id}' is not active and reviewed") + if entry.lane == "attended-change": + session = get_entry(entry.session_lane or "") + if (session is None or not session.is_active or not session.reviewed + or session.lane != "login"): + return ("LANE-KIND", f"attended-change lane '{entry.id}' session_lane " + f"'{entry.session_lane}' does not resolve to an active, reviewed login lane") + elif entry.lane != "login": + return ("LANE-KIND", f"attended step's lane '{entry.id}' is not a login or attended-change lane") + return None diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_1_3_integration.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_1_3_integration.py new file mode 100644 index 00000000..bb13c095 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_1_3_integration.py @@ -0,0 +1,151 @@ +"""Integration: the real net-kingdom 1.3 candidate against this kernel and catalog. + +Reads the actual 1.3 type, its forgejo example and the committed composition +artifact from net-kingdom, plus this branch's real routing catalog. The descriptor +map and metadata come from those committed files. All authority is fixture-only: +we change the proposed type to acknowledged, change the example case to +materialized and replace its acceptance record, promote draft lanes in memory, +and temporarily install the 1.3 parameter pin. Stub adapters replace native +adapters; an unconditional evidence verifier simulates every acceptance and caller +session. WARDEN_AGENT_ID is removed to simulate a human runtime. No adapter is +dispatched. This proves conditional source admission, not authenticated owner +acceptance, native execution or readiness; unresolved Apps sessions stay refused. +""" +from dataclasses import asdict +import hashlib +import json +import os +from pathlib import Path +from unittest import mock + +import pytest +import yaml + +from warden import case_inputs as inputs +from warden.case_receipt_state import Refused +from warden.case_run import Adapter, Descriptor, Executor, Journal, digest +from warden.case_type import check_follow_through +from warden.routing.catalog import load_catalog + +NK = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT", + Path(__file__).resolve().parents[2] / "net-kingdom")) +TYPE_DIR = NK / "security-case-type-revisions/1.3/oidc-browser-relying-party" +ARTIFACT = NK / "sso-mfa/k8s/keycape/descriptor-composition-1.3.candidate.json" +CATALOG = Path(__file__).resolve().parents[1] / "registry/routing/catalog.yaml" + +pytestmark = pytest.mark.skipif(not (TYPE_DIR / "type.yaml").is_file() or not ARTIFACT.is_file(), + reason="net-kingdom 1.3 candidate not available") + +DRAFT_LANES = {"keycape-attended-case-change", "platform-secret-delivery", + "consumer-owner-deployment", "apps-consumer-handover"} + + +def encode(desc): # net-kingdom compose-1.2-descriptors.py encode() + return json.dumps(asdict(desc), sort_keys=True, separators=(",", ":")).encode() + + +def type_doc(status="acknowledged"): + doc = yaml.safe_load((TYPE_DIR / "type.yaml").read_bytes()) + doc["spec"]["status"] = status + return doc + + +def catalog_bytes(promote): + raw = yaml.safe_load(CATALOG.read_bytes()) + if promote: + for entry in raw["entries"]: + if entry["id"] in DRAFT_LANES and entry.get("status") == "draft": + entry["status"] = "active" + return yaml.safe_dump(raw).encode() + + +def descriptors(): + art = json.loads(ARTIFACT.read_text()) + out = {} + for row in art["steps"]: + d = dict(row["descriptor"]) + for key in ("allowed_created", "revocation_scopes"): + d[key] = tuple(d[key]) + out[row["step"]] = Descriptor(**d) + return out + + +def pin_13(): + params = type_doc()["spec"]["parameters"] + fp = hashlib.sha256(json.dumps(params, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + return mock.patch.dict(inputs.PARAMETER_PINS, {"1.3": fp}) + + +def unused(*_): + raise AssertionError("admission must not dispatch") + + +def admit(tmp_path, desc, *, promote=True, pinned=True, status="acknowledged"): + case = yaml.safe_load((TYPE_DIR / "examples/forgejo.yaml").read_bytes()) + case["spec"].update(status="materialized", acceptance={ + "at": "2026-10-17", "record": "fixture-only acceptance, not an owner record", + "by": [{"name": "fixture", "role": "owner"}, {"name": "fixture", "role": "founder"}]}) + adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused) + executor = Executor(Journal(tmp_path / desc.step), {adapter.reference: adapter}, + lambda purpose, source, evidence: True, + browser_inputs=inputs.BrowserInputs(NK)) + with mock.patch.dict(os.environ), (pin_13() if pinned else mock.patch.dict({})): + os.environ.pop("WARDEN_AGENT_ID", None) + return executor.admit(yaml.safe_dump(case).encode(), yaml.safe_dump(type_doc(status)).encode(), + encode(desc), catalog_bytes(promote), principal="founder", + actor=desc.actor, lane=desc.lane, + acceptance_evidence="e-acceptance", materialization_evidence="e-mason", + descriptor_evidence="e-descriptor", session_evidence="e-session") + + +def test_artifact_descriptors_are_the_type_map(): + descs = descriptors() + assert len(descs) == 7 + assert {s: digest(encode(d)) for s, d in descs.items()} == type_doc()["spec"]["execution_descriptors"] + + +def test_conditional_admission_keeps_unresolved_apps_sessions_refused(tmp_path): + for step, desc in descriptors().items(): + if step in ("deliver-consumer-secret", "hand-over"): + with pytest.raises(Refused, match="does not resolve to an active, reviewed login lane"): + admit(tmp_path, desc) + else: + admitted, invocation, _ = admit(tmp_path, desc) + assert admitted == desc + assert (invocation.step, invocation.actor, invocation.target) == (step, "human", "forgejo") + +def test_draft_lane_steps_refused_until_promoted(tmp_path): + for step, desc in descriptors().items(): + if desc.lane in DRAFT_LANES: + with pytest.raises(Refused, match="descriptor lane is not active in the catalog"): + admit(tmp_path, desc, promote=False) + else: + admit(tmp_path, desc, promote=False) # mint-secret's lane is already active + + +def test_unpinned_1_3_refused_by_browser_validation(tmp_path): + with pytest.raises(Refused, match="failed exact-version validation"): + admit(tmp_path, descriptors()["mint-secret"], pinned=False) + + +def test_proposed_type_refused(tmp_path): + with pytest.raises(Refused, match="type is not acknowledged"): + admit(tmp_path, descriptors()["mint-secret"], status="proposed") + + +def test_type_check_findings_against_the_real_catalog(tmp_path): + def findings(promote): + path = tmp_path / f"catalog-{promote}.yaml" + path.write_bytes(catalog_bytes(promote)) + report = check_follow_through(type_doc("proposed"), load_catalog(path)) + return {(f.code, f.where) for f in report.findings} + + draft = findings(False) + assert {code for code, _ in draft} == {"LANE-UNREVIEWED"} + assert len(draft) == 6 # six steps use the four draft lanes; mint-secret's lane is active + + promoted = findings(True) + # Only the two railiance-apps lanes remain: their session_lane awaits owner confirmation. + assert {code for code, _ in promoted} == {"LANE-KIND"} + assert len(promoted) == 2 + assert all("deliver-consumer-secret" in where or "hand-over" in where for _, where in promoted) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_admission_modes.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_admission_modes.py new file mode 100644 index 00000000..5069491d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_admission_modes.py @@ -0,0 +1,136 @@ +"""KERNEL-2 lane resolution and KERNEL-3 mode-aware admission (fixture only).""" +from dataclasses import asdict, replace +import json + +import pytest +import yaml + +from warden.case_receipt_state import Refused +from warden.case_run import Descriptor, digest +from tests.test_case_run import MINT_STEP, fixture_catalog, lane_entry, make_rig + + +@pytest.fixture +def rig(tmp_path): + return make_rig(tmp_path) + +EVIDENCE = {"acceptance", "materialization", "descriptor", "session"} + + +def admit(rig, *, step=None, entries=None, catalog=None, desc=None, **over): + executor, _, _, sources, kwargs = rig + case, typ, descriptor, _ = sources + if desc is not None: + base = Descriptor.decode(descriptor) + descriptor = json.dumps(asdict(replace(base, **desc))).encode() + doc = yaml.safe_load(typ) + doc["spec"]["execution_descriptors"] = {"mint-secret": digest(descriptor)} + if step is not None: + doc["spec"]["follow_through"] = step + typ = yaml.safe_dump(doc).encode() + if catalog is None: + catalog = fixture_catalog(*(entries or [lane_entry()])) + executor.evidence_verifier = lambda purpose, source, evidence: evidence in EVIDENCE + return executor.admit(case, typ, descriptor, catalog, **{**kwargs, **over}) + + +def mode_step(executes, attended): + return [dict(MINT_STEP, warden_executes=executes, attended=attended)] + + +def refused(message, rig, **kw): + with pytest.raises(Refused, match=message): + admit(rig, **kw) + + +# Positive modes +def test_warden_mode_admits(rig): + assert admit(rig)[1].lane == "issuer-lane" + + +def test_attended_mode_admits_human_parent_session(rig, monkeypatch): + monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) + admitted = admit( + rig, + step=mode_step(False, True), + entries=[dict(lane_entry(), lane="login")], + desc=dict(effect="change", revocation_scopes=("parent-session",)), + ) + assert admitted[1].actor == "human" + + +def test_owner_mode_admits_native_owner_lane(rig): + admitted = admit(rig, step=mode_step(False, False), desc=dict(effect="change")) + assert admitted[1].owner == "owner" + + +# KERNEL-2 +@pytest.mark.parametrize( + "entries, catalog, message", + [ + (None, b"not: [a catalog", "invalid catalog source shape"), + ([lane_entry("other-lane")], None, "descriptor lane is not in the catalog"), + ([lane_entry(status="draft")], None, "descriptor lane is not active in the catalog"), + ([{k: v for k, v in lane_entry().items() if k != "delegation"}], None, + "descriptor lane has no catalog delegation record"), + ([{k: v for k, v in lane_entry().items() if k != "risk"}], None, + "descriptor lane carries no risk grade"), + ([lane_entry(owner="someone-else")], None, + "descriptor owner does not fit catalog lane owner_repo"), + ([lane_entry(risk="standard")], None, + "catalog lane risk does not cover the descriptor effect"), + ], +) +def test_catalog_lane_refusals(rig, entries, catalog, message): + refused(message, rig, entries=entries, catalog=catalog) + + +# KERNEL-3: the step must declare dispatch and agree with the descriptor +@pytest.mark.parametrize( + "step, message", + [ + ([], "type step does not declare how the kernel dispatches it"), + (mode_step(True, True), "type step does not declare how the kernel dispatches it"), + ([{k: v for k, v in MINT_STEP.items() if k != "lane"}], + "type step does not declare how the kernel dispatches it"), + ([dict(MINT_STEP, warden_executes="yes")], + "type step does not declare how the kernel dispatches it"), + ([dict(MINT_STEP, lane="other-lane")], "type step lane or owner disagrees with descriptor"), + ([dict(MINT_STEP, owner="other")], "type step lane or owner disagrees with descriptor"), + ], +) +def test_step_declaration_refusals(rig, step, message): + refused(message, rig, step=step) + + +def test_attended_refuses_agent_session(rig, monkeypatch): + monkeypatch.setenv("WARDEN_AGENT_ID", "agent-1") + refused("attended step requires a human parent session", rig, + step=mode_step(False, True), desc=dict(effect="change")) + + +def test_attended_refuses_agent_actor(rig): + refused("attended step requires a human parent session", rig, + step=mode_step(False, True), desc=dict(effect="change", actor="agent"), + actor="agent") + + +def test_attended_refuses_caller_lane_other_than_step_lane(rig): + refused("principal actor or lane mismatch", rig, + step=mode_step(False, True), desc=dict(effect="change"), lane="login-lane") + + +@pytest.mark.parametrize( + "entry, desc", + [ + (lane_entry(delegation={"mode": "interim", "intended_owner": "owner", + "blocked_on": "fixture", "reviewed": "2026-10-17"}), + dict(effect="change")), + (lane_entry(owner="owner/other"), dict(effect="change")), + (lane_entry(warden_executes=True), dict(effect="change")), + (lane_entry(), {}), # owner step may not be a Warden-brokered mint + ], +) +def test_owner_mode_refusals(rig, entry, desc): + refused("owner step lane is not the owner's native path", rig, + step=mode_step(False, False), entries=[entry], desc=desc) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_attended_boundary.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_attended_boundary.py new file mode 100644 index 00000000..ba07b331 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/candidate/tests/test_case_attended_boundary.py @@ -0,0 +1,113 @@ +"""Synthetic attended admission/dispatch regressions; no owner/native actions.""" +from dataclasses import replace + +import pytest + +from tests.test_case_admission_modes import admit, mode_step +from tests.test_case_run import fixture_catalog, lane_entry, make_rig +from warden.case_receipt_state import Outcome, Refused +from warden.case_type import check_follow_through +from warden.routing.catalog import CatalogError, _parse_entry, load_catalog + + +def attended(rig, entries): + return admit(rig, step=mode_step(False, True), entries=entries, + desc=dict(effect="change", revocation_scopes=("parent-session",))) + + +@pytest.mark.parametrize("operation", ["run", "rollback", "reconcile"]) +@pytest.mark.parametrize("marker", ["agent-1", "", None]) +def test_identity_change_refuses_before_any_write_or_callback(tmp_path, monkeypatch, operation, marker): + monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) + rig = make_rig(tmp_path) + executor = rig[0] + admitted = attended(rig, [dict(lane_entry(), lane="login")]) + executor.evidence_verifier = lambda *_: True + if operation == "rollback": + executor.run(*admitted) + elif operation == "reconcile": + old = executor.adapters[admitted[0].adapter] + def lost(_): + raise RuntimeError("synthetic lost reply") + executor.adapters[old.reference] = replace(old, invoke=lost) + executor.run(*admitted) + calls = [] + old = executor.adapters[admitted[0].adapter] + callback = getattr(old, "invoke" if operation == "run" else operation) + def observed(*args): + calls.append(args) + return callback(*args) + executor.adapters[old.reference] = replace(old, **{ + "invoke" if operation == "run" else operation: observed, + }) + def rows(): + return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] + for table in ("intents", "receipts")) + before = rows() + if marker is None: + result = getattr(executor, operation)(*admitted) + assert result.last.outcome is {"run": Outcome.SUCCESS, + "rollback": Outcome.ROLLED_BACK, + "reconcile": Outcome.NO_CHANGE}[operation] + assert len(calls) == 1 + assert rows() != before + return + monkeypatch.setenv("WARDEN_AGENT_ID", marker) + with pytest.raises(Refused, match="attended step requires a human parent session"): + getattr(executor, operation)(*admitted) + assert rows() == before + assert not calls + + +def entries_for(case): + change = dict(lane_entry(), lane="attended-change", session_lane="login-lane") + login = dict(lane_entry("login-lane"), lane="login") + if case == "missing": + return [change] + if case == "draft-login": + login["status"] = "draft" + elif case == "unreviewed-login": + login["reviewed"] = "" + elif case == "wrong-kind-login": + login["lane"] = "secret" + elif case == "draft-change": + change["status"] = "draft" + elif case == "unreviewed-change": + change["reviewed"] = "" + elif case == "missing-session-field": + del change["session_lane"] + elif case in ("secret", "ceremony"): + change["lane"] = case + del change["session_lane"] + return [change, login] + + +@pytest.mark.parametrize("case, code", [ + ("valid", None), ("missing", "LANE-KIND"), ("draft-login", "LANE-KIND"), + ("unreviewed-login", "LANE-KIND"), ("wrong-kind-login", "LANE-KIND"), + ("draft-change", "LANE-UNREVIEWED"), ("unreviewed-change", "LANE-UNREVIEWED"), + ("secret", "LANE-KIND"), ("ceremony", "LANE-KIND"), +]) +def test_attended_change_planner_and_executor_agree(tmp_path, monkeypatch, case, code): + monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) + entries = entries_for(case) + path = tmp_path / "catalog.yaml" + path.write_bytes(fixture_catalog(*entries)) + catalog = load_catalog(path) + step = dict(mode_step(False, True)[0], attended_justification="synthetic test", + entry_point="owner/test", auth="synthetic session", value_handling="none") + doc = {"spec": {"follow_through": [step], "rollback": [{"reverses": step["id"]}]}} + report = check_follow_through(doc, catalog) + assert [f.code for f in report.findings] == ([] if code is None else [code]) + rig = make_rig(tmp_path) + if code: + with pytest.raises(Refused): + attended(rig, entries) + assert rig[0].journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone()[0] == 0 + else: + assert attended(rig, entries)[1].lane == "issuer-lane" + + +def test_attended_change_missing_session_is_parser_rejection(): + with pytest.raises(CatalogError, match="must name session_lane"): + _parse_entry(entries_for("missing-session-field")[0], 0) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/run.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/run.py new file mode 100644 index 00000000..869be8d9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/run.py @@ -0,0 +1,12 @@ +import os +from pathlib import Path +import subprocess +import sys +root=Path('/tmp/warden-kernel-fix-r1') +tree=root/os.environ.get('FIX_TEST_TREE', 'ops-warden') +env={'HOME':str(root/'home'),'PATH':'/usr/bin:/bin','PYTHONPATH':str(root/'guard')+':'+str(tree/'src')+':/home/worsch/phase-memory/src','PYTHONDONTWRITEBYTECODE':'1','WARDEN_TEST_NETKINGDOM_ROOT':str(root/'net-kingdom'),'LANG':'C.UTF-8'} +python='/tmp/warden-kernel-review-r1/final/.venv/bin/python' +with (root/sys.argv[1]).open('w') as out: + r=subprocess.run([python,*sys.argv[2:]],cwd=tree,env=env,stdout=out,stderr=subprocess.STDOUT) +print(r.returncode) +sys.exit(r.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/sitecustomize.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/sitecustomize.py new file mode 100644 index 00000000..fd8113a8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/evidence/sitecustomize.py @@ -0,0 +1,21 @@ +import sys + +bound_sockets = [] + +def deny_network(event, args): + if event == 'socket.bind': + bound_sockets.append(args[0]) + if event == 'socket.getaddrinfo' and args[0] not in {'localhost', '127.0.0.1', '::1'}: + raise OSError('independent review: network disabled') + if event == 'socket.connect': + address = args[1] + for server in bound_sockets: + try: + bound = server.getsockname() + if address == bound and bound[0] in {'127.0.0.1', '::1'}: + return + except OSError: + pass + raise OSError('independent review: network disabled') + +sys.addaudithook(deny_network) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/implementation-r1.json b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/implementation-r1.json new file mode 100644 index 00000000..36eef527 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/implementation-r1.json @@ -0,0 +1,93 @@ +{ + "schema": "custodian.warden-1.3-kernel-fixes.v1", + "date": "2026-10-17", + "role": "implementation successor; not independent review or owner/native acceptance", + "status": "candidate prepared; no canonical apply/commit/push", + "base_ops_warden": "5abde817775d6304e3cf5a46948717a1606981bf", + "net_kingdom": "001a38ca2aa85155ce4840c4da22aca6e2346171", + "accepted_kernel_1": "117fec9241e7b985dfb187b992e173d0a5dc3700", + "existing_review": { + "path": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review/kernel-review-r1.json", + "sha256": "sha256:43c96da94605082b4924ccd8ef41a1f8737d7e9ada95047ff16415b4614905dc", + "disposition": "retained unchanged; reuse existing KERNEL-1 validation; its initial ambient-lookup boundary deviation remains adverse/unknown and is not counted as compliant success" + }, + "patch": { + "file": "candidate.patch", + "sha256": "sha256:1a3530a1e469fc03c2c29f1b66fa3ba1904e56bf2abcfb242c7a2ed015fb3e74", + "applies_to_base": "git apply --check succeeded against frozen base archive" + }, + "files": { + "src/warden/case_run.py": { + "base": "sha256:e071b485162b2db5c4a9da5821917b7ff81a07980270530ed91e987796591ddd", + "candidate": "sha256:1bc73e30ac718fd75bbaff2a739495fc051aeaec73802ac71f0583cb2d53ab0a" + }, + "src/warden/case_type.py": { + "base": "sha256:3ee1004b7c0ac3975be157408376cf14acc0c1e1e1623d79d3179540040f40ce", + "candidate": "sha256:7b659b8d682863cd28df24025be4b10dd7ca3df724350a22d360c6af9615bca7" + }, + "src/warden/routing/models.py": { + "base": "sha256:2da3bc90c451ed9b66c2d43035951c2a777c2b896201e9a87966a7a8f7ead426", + "candidate": "sha256:b85372b2572a49af62e9534f26a33963b894633c7cd43dd2a28416b8959cba3e" + }, + "tests/test_case_1_3_integration.py": { + "base": "sha256:591cc4e5859e1cebce29912b19b7ac1479bf49373fe04425542d1ecd32c74192", + "candidate": "sha256:4862c5a004075eb18733d354bee7dda3f55e04a6ee8ecb44b76166347c121158" + }, + "tests/test_case_admission_modes.py": { + "base": "sha256:3f5c648cec9e075818a616207aff508bdbc9ae95b100f5b8400a86fcae826182", + "candidate": "sha256:9110d6b6286cf0f85268d4970d1fd403250da3f04351d70b1fd2d9d7a93e2d89" + }, + "tests/test_case_attended_boundary.py": { + "base": null, + "candidate": "sha256:4615e653f06144305ea1a84e98bc6260e16ed41ef9e5db2d0f162ef9f723ef2d" + } + }, + "unchanged_pins": { + "src/warden/case_inputs.py": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7", + "registry/routing/catalog.yaml": "sha256:ba9634a92e584dac8c42e8624ab52adce06e79a285209cb64c0daa4dac9cd7d8", + "registry/generated/high-risk-data-paths.yaml": "sha256:5cd9c03b86751f7197cb5661a53a86d59765137958b215ec14bb343156f216d0" + }, + "changes": { + "KR1-F1": "Retain mode with admission. Recheck human actor, absence of WARDEN_AGENT_ID (including empty marker), and parent-session scope in common run/rollback/reconcile guard, after caller-session revalidation and before journal writes/owner callbacks.", + "KR1-F2": "Retain exact authenticated whole-catalog bytes. Parse and validate that snapshot at admission and attended dispatch; share attended lane active/reviewed/login-or-attended-change/session-login rule with planner. Pending Apps sessions stay refused, even under hypothetical in-memory draft promotion.", + "KR1-F3": "Integration docstring enumerates type acknowledgment, case materialization/replacement acceptance, promotion, pin, stub adapters, unconditional evidence verifier and human environment simulation. Positive expectation covers five resolvable steps; two Apps steps refused.", + "KR1-F4": "Sibling-independent 19-test matrix: six marked runtime negatives, three unmarked run/rollback/reconcile positive controls, nine planner/executor lane cases, one missing-session parser rejection." + }, + "validation": { + "baseline_regressions": "Final 19 new tests on unmodified frozen production source:13 failed,6 passed. Six runtime identity and seven invalid lane/session/review negatives fail because original code does not refuse. Six controls pass.", + "initial_test_authoring_errors": "First run:16 failures, due fixture lane_entry argument misuse; second:15 failures/1 pass, including planner schema omissions. These are invalid setup attempts, retained in logs. Corrected meaningful pre-expansion baseline:13 failed/3 passed; final with positive controls:13 failed/6 passed.", + "candidate_new_regressions": "19 passed in 1.57s", + "focused": "99 passed in 13.95s; expanded:129 passed,2 skipped in 16.40s (home-relative canonical lookups; later supplied frozen NetKingdom link)", + "full_suite": "765 passed,4 integration-marked deselected in 68.11s; uninterrupted final suite includes resolved home-relative lookups", + "command": "python3 /tmp/warden-kernel-fix-r1/run.py candidate-full.log -m pytest -q", + "lint": "python -m ruff check .: All checks passed; git diff --check passed", + "isolation": "Allowlisted environment; HOME=/tmp/warden-kernel-fix-r1/home with only frozen NetKingdom symlink and three public workload declarations. No ambient credentials copied or inventoried. Python socket audit guard denies all remote connections and allows only loopback endpoints bound by same test process. PATH=/usr/bin:/bin. Reused existing scratch venv interpreter/dependencies read-only; no live or native owner callback used.", + "limits": "Four repository integration-marked tests remain deselected. No new native/provenance claim; independent successor review still required.", + "patch_check_attempts": "First check incorrectly used the regression base tree, where the new test already existed; refused with already exists. Fresh untouched archive check then passed; no source conflict." + }, + "separate_test_isolation_proposal": { + "file": "test-isolation-proposal.patch", + "sha256": "sha256:0f6c46303ec24f337e4190ec84773ccefb4833241233b4b7d56bd1927eba838c", + "included_in_candidate": false, + "description": "Autouse fixture only in tests/test_access.py replaces ambient identity helper with deterministic no-token metadata. Existing explicit identity-display test overrides it. Production advisory helper unchanged; separately subprocess-mocked helper tests retained.", + "regression": "Synthetic BAO_TOKEN sentinel plus forbidden subprocess callback:1 failed against base, with no subprocess/native lookup permitted. Proposed fixture:26 access/helper tests passed in1.87s.", + "limit": "This narrow fixture is not a whole-suite sandbox; allowlisted HOME/environment and network guard remain required. Root to create live owner handoff for broader isolation work." + }, + "boundaries": { + "canonical_owner_edits": false, + "credentials_or_secret_reads": false, + "native_actions": false, + "grant": "none", + "catalog_promotion": false, + "owner_login_selection": false, + "PARAMETER_PINS_changed": false, + "draft_lanes_changed": false + }, + "follow_up": [ + "Independent review of candidate patch and exact file pins before owner application.", + "Owner confirmation/login choices and catalog promotion remain separate gates.", + "Founder exact-type acknowledgment and PIN-1 remain separate; no pin added.", + "Native WARDEN-WP-0044-T03 evidence and downstream acceptance remain outside source tests.", + "Root handles progress logging, publication and live test-isolation handoff." + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/test-isolation-proposal.patch b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/test-isolation-proposal.patch new file mode 100644 index 00000000..4b447e0b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1/test-isolation-proposal.patch @@ -0,0 +1,48 @@ +diff --git a/tests/test_access.py b/tests/test_access.py +index c4286aa..1736a45 100644 +--- a/tests/test_access.py ++++ b/tests/test_access.py +@@ -4,6 +4,8 @@ from __future__ import annotations + import json + from pathlib import Path + ++import pytest ++ + from typer.testing import CliRunner + + from warden.access import agent_read_boundary_identity, expand_handoff, policy_gate_status +@@ -13,6 +15,34 @@ from warden.routing.models import RouteEntry + runner = CliRunner() + + ++@pytest.fixture(autouse=True) ++def synthetic_openbao_advisory_identity(monkeypatch): ++ """Advisory CLI tests must not inspect or use workstation credentials. ++ ++ Identity-display tests override this stub explicitly. The real helper has ++ separate subprocess-mocked unit coverage in test_openbao_caller.py. ++ """ ++ monkeypatch.setattr( ++ "warden.openbao_caller.describe_ambient_openbao_caller", ++ lambda *args, **kwargs: { ++ "source": "none", "entity_id": None, "display_name": None, ++ "auth_path": None, "policies": [], "oidc_role": None, "ttl": None, ++ "error": "synthetic test: no ambient OpenBao token", ++ }, ++ ) ++ ++ ++def test_advisory_does_not_invoke_ambient_lookup(monkeypatch): ++ monkeypatch.setenv("BAO_TOKEN", "fixture-only-never-a-real-token") ++ def forbidden(*args, **kwargs): ++ raise AssertionError("advisory unit tests must not launch a token lookup") ++ monkeypatch.setattr("warden.openbao_caller.subprocess.run", forbidden) ++ result = runner.invoke(app, ["access", "npm token", "--json"]) ++ assert result.exit_code == 0 ++ assert json.loads(result.stdout)["openbao_caller"]["source"] == "none" ++ ++ ++ + def _repo_catalog() -> Path: + return Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml" + diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/integration_runner.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/integration_runner.py new file mode 100644 index 00000000..7b6001e0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/integration_runner.py @@ -0,0 +1,18 @@ +"""Invoke only pinned, offline validator child; keep socket/native denial in child.""" +from pathlib import Path +import sys +import subprocess +import warden.case_inputs as inputs +import pytest + +PREAMBLE = '''import sys as _review_sys\ndef _review_guard(event,args):\n if event.startswith('socket.') or event in {'subprocess.Popen','os.system','os.exec','os.posix_spawn'}:\n raise RuntimeError('review validator: network/native disabled')\n if event=='open' and isinstance(args[0],str) and (args[0].startswith('/home/') or args[0].startswith('/root/')):\n raise RuntimeError('review validator: ambient home denied')\n_review_sys.addaudithook(_review_guard)\n''' +old_run = subprocess.run + +def run_pinned(args,*pos,**kw): + if (type(args) is not list or len(args)!=5 or args[:3]!=[sys.executable,'-I','-c'] + or args[3]!=inputs._VALIDATE or not str(args[4]).startswith('/tmp/warden-case-validator-') + or kw.get('env')!={}): + raise RuntimeError('review validator: unexpected process refused') + return old_run([*args[:3],PREAMBLE+args[3],args[4]],*pos,**kw) +inputs.subprocess.run=run_pinned +raise SystemExit(pytest.main(['-q','tests/test_case_1_3_integration.py'])) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/kernel-review-r2.json b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/kernel-review-r2.json new file mode 100644 index 00000000..883b31ea --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/kernel-review-r2.json @@ -0,0 +1,128 @@ +{ + "schema": "custodian.warden-1.3-kernel-review-r2.v1", + "date": "2026-10-17", + "role": "independent successor source reviewer; not candidate author", + "disposition": "accept candidate source delta for KR1-F1, KR1-F2, KR1-F3 and KR1-F4", + "scope": "Exact candidate.patch on frozen 5abde817775d6304e3cf5a46948717a1606981bf; separate test-isolation-proposal.patch reviewed and accepted as narrow test hygiene. No native acceptance, external owner assent, catalog promotion, founder acknowledgment or PARAMETER_PINS authorization.", + "inputs": { + "accepted_kernel_1": "117fec9241e7b985dfb187b992e173d0a5dc3700", + "frozen_branch": "5abde817775d6304e3cf5a46948717a1606981bf", + "net_kingdom": "001a38ca2aa85155ce4840c4da22aca6e2346171", + "candidate.patch": "sha256:1a3530a1e469fc03c2c29f1b66fa3ba1904e56bf2abcfb242c7a2ed015fb3e74", + "implementation-r1.json": "sha256:b9ee843bba417c82ad77ac8a221cf64dd5088aa640528524b2e0ef0c129d3184", + "test-isolation-proposal.patch": "sha256:0f6c46303ec24f337e4190ec84773ccefb4833241233b4b7d56bd1927eba838c", + "retained_kernel-review-r1.json": "sha256:43c96da94605082b4924ccd8ef41a1f8737d7e9ada95047ff16415b4614905dc", + "candidate_files_verified": { + "src/warden/case_run.py": "sha256:1bc73e30ac718fd75bbaff2a739495fc051aeaec73802ac71f0583cb2d53ab0a", + "src/warden/case_type.py": "sha256:7b659b8d682863cd28df24025be4b10dd7ca3df724350a22d360c6af9615bca7", + "src/warden/routing/models.py": "sha256:b85372b2572a49af62e9534f26a33963b894633c7cd43dd2a28416b8959cba3e", + "tests/test_case_1_3_integration.py": "sha256:4862c5a004075eb18733d354bee7dda3f55e04a6ee8ecb44b76166347c121158", + "tests/test_case_admission_modes.py": "sha256:9110d6b6286cf0f85268d4970d1fd403250da3f04351d70b1fd2d9d7a93e2d89", + "tests/test_case_attended_boundary.py": "sha256:4615e653f06144305ea1a84e98bc6260e16ed41ef9e5db2d0f162ef9f723ef2d" + }, + "unchanged_pins_verified": { + "src/warden/case_inputs.py": "sha256:1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7", + "registry/routing/catalog.yaml": "sha256:ba9634a92e584dac8c42e8624ab52adce06e79a285209cb64c0daa4dac9cd7d8", + "registry/generated/high-risk-data-paths.yaml": "sha256:5cd9c03b86751f7197cb5661a53a86d59765137958b215ec14bb343156f216d0" + } + }, + "findings": [ + { + "id": "KR1-F1", + "status": "resolved in source", + "evidence": "The cached tuple now retains attended mode. All run/rollback/reconcile paths enter _admission before journal lock/load/save or owner callbacks. Caller-session evidence is reverified, then attended human actor, presence of any WARDEN_AGENT_ID key (including empty), and parent-session scope are checked. Independent changed actor/principal/descriptor-scope/session probes reject with no journal change/callback; both marker variants reject on all three cached operations." + }, + { + "id": "KR1-F2", + "status": "resolved in source", + "evidence": "Admission authenticates the complete catalog digest and parses its exact bytes. The same bytes are retained and reparsed at attended dispatch. Shared attended_lane_issue checks active/reviewed login or attended-change whose session_lane resolves to active/reviewed login in that same parsed dictionary. The 9-case author matrix checks planner/executor parity and the parser case checks missing session_lane. Independent dispatch interception verifies byte-identical catalog input on run/rollback/reconcile despite a later invalid ambient catalog file. The two unresolved Apps steps refuse in actual frozen composition integration." + }, + { + "id": "KR1-F3", + "status": "resolved in source", + "evidence": "Docstring explicitly declares acknowledged-type substitution, materialized example plus replaced acceptance, in-memory promotion and parameter pin, stub adapters, unconditional evidence verifier and removed agent marker. Test expects five conditional admissions and two Apps refusals, with no adapter dispatch. These are fixture admissions, not authenticated events." + }, + { + "id": "KR1-F4", + "status": "resolved in source", + "evidence": "New 19-test attended matrix has no sibling source dependency; 13 tests fail against unchanged frozen production source and 6 controls pass. Reviewer-owned 22 tests add independent changed authority and catalog snapshot checks. Combined frozen baseline: 22 failed,19 passed; candidate tests:41 passed within focused set. No control is weakened." + } + ], + "additional_blocking_findings": [], + "candidate_correction_required": false, + "test_isolation_proposal": { + "disposition": "accept narrow test-only patch", + "production_changes": false, + "baseline_without_proposal": "25 original access/helper tests pass in empty home; they do not alone prove absence of ambient lookup. Appended sentinel regression fails (1 failed in0.51s), with forbidden mocked subprocess callback raising before any process starts.", + "with_proposal": "26 access/helper tests pass in2.16s. Autouse fixture stubs the lazy advisory identity function; existing explicit display test overrides it. Real helper coverage remains separately subprocess-mocked.", + "limits": "Module-local fixture is not an execution sandbox, does not fix production behavior and does not protect unrelated tests. Parent must retain broader isolation handoff." + }, + "validation": [ + { + "command": "python3 /tmp/warden-kernel-review-r2/run.py focused-corrected.log -m pytest -q tests/test_review_r2_independent.py tests/test_case_attended_boundary.py tests/test_case_admission_modes.py", + "result": "64 passed in2.55s" + }, + { + "command": "python3 /tmp/warden-kernel-review-r2/run-baseline.py baseline-corrected.log -m pytest -q tests/test_case_attended_boundary.py tests/test_review_r2_independent.py", + "result": "22 failed,19 passed in2.11s; expected differential:13 original defect probes,6 independent identity marker probes,3 reparse assertions fail on predecessor" + }, + { + "command": "python3 /tmp/warden-kernel-review-r2/run.py integration-safe.log /tmp/warden-kernel-review-r2/integration_runner.py", + "result": "6 passed in10.11s; actual pinned metadata validator executes as isolated Python child with guard preamble; native owner callbacks remain forbidden" + }, + { + "command": "python3 /tmp/warden-kernel-review-r2/run.py final-focused.log -m pytest -q tests/test_review_r2_independent.py tests/test_case_attended_boundary.py tests/test_case_admission_modes.py tests/test_access.py tests/test_openbao_caller.py", + "result": "90 passed in4.22s, including separate isolation proposal" + } + ], + "invalid_initial_attempts": { + "independent.log": "23 failed,47 passed.21 reviewer tests used lane_entry(lane=login), which changes lane id rather than kind; corrected to dict(lane_entry(),lane=login).2 integration tests were refused by strict subprocess guard at pinned validator launch. No owner/native callback occurred. These are setup/isolation-control refusals, not candidate defects or passing evidence.", + "baseline.log": "Same initial reviewer fixture mistake; superseded by preserved corrected baseline. Earlier author tests still reproduced13 defects." + }, + "isolation": { + "home": "/tmp/warden-kernel-review-r2/home (empty, independently asserted)", + "environment": "Allowlisted HOME,PATH=/usr/bin:/bin,PYTHONPATH pointing only to scratch guard and source,PYTHONDONTWRITEBYTECODE,WARDEN_TEST_NETKINGDOM_ROOT,LANG. No ambient home/environment/token copied, inspected or inventoried. Synthetic BAO_TOKEN sentinel exists only under monkeypatch.", + "interpreter": "Read-only existing scratch venv /tmp/warden-kernel-review-r1/final/.venv/bin/python; no dependency install/network", + "controls": "Socket audit events denied. Process creation denied except the exact reviewed offline BrowserInputs validator command (empty child environment, pinned source modules copied by production code). Reviewer wrapper prepends child audit denial for sockets, further subprocess, and ambient /home or /root file reads. Final focused run also denies ambient home reads in parent. Guard probes independently demonstrate socket/native lookup refusal.", + "native_actions": false, + "credential_access": false, + "canonical_edits": false, + "new_commits": false + }, + "limitations": [ + "Focused independent review, not a new full-suite certification; author full-suite claims are not independently adopted.", + "Existing KERNEL-1 review remains retained, including its prior ambient-lookup deviation and unknown outcome; this successor does not erase or reclassify it.", + "WARDEN_AGENT_ID is a source marker boundary, not proof of isolated human authentication; the injected evidence verifier and owner adapter remain required trust roots.", + "No real native execution, owner evidence provenance, login choice, original-created-object readback or revocation event was exercised.", + "No external owner assent, founder exact-type acknowledgment, 1.3 pin, native T03 or downstream acceptance is supplied.", + "Exact-type known fixed pins and all draft lanes remain unchanged; no catalog was promoted." + ], + "knowledge_orientation": { + "package_version": "0.2.6", + "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "validated_start_status": "withheld/orientation-only (exit2; changed source repositories)", + "source_review": "Read start/local-work cards as orientation only, actual ops-warden AGENTS.md/INTENT.md, frozen NetKingdom SECURITY-COMPANION.md and scoped production/test source directly. No inferred authority from withheld card." + }, + "follow_up": [ + "Root may integrate exact reviewed source and narrow separate test patch within its existing authorization.", + "Keep original adverse review intact and hand off broader test isolation as live owner work.", + "Owner login/catalog promotion, founder exact type/PIN-1, authenticated native T03 and downstream acceptance remain separate gates." + ], + "grants": "none", + "packet_hashes": { + "final-focused.log": "sha256:53adf310d91cbaaa401731f7680654bf8a0bf0f5aa955b22bbc9b6adafacb793", + "focused-corrected.log": "sha256:e942df3c21caf8eef7e94cfc70ba98b8005da909079fb9adb8502b4c7ede60ec", + "reviewer-probes.py": "sha256:e52025893ca0755b3a62e2585cf2f16002a5b705f7cfb0582b05435d3b9e336a", + "integration_runner.py": "sha256:d77da107d67ec827f19e0c46045a2802081a62283c3bdb5bfec35feabb454234", + "isolation-proposal.log": "sha256:a2a8b7c722b4b682291e588ccc374559218555adbf75b3102784d7dba2223202", + "run.py": "sha256:c6f931ba4ff9b0ef42627030a016dd7ebe3f353b3024f1c40b80df890aefe59f", + "baseline-corrected.log": "sha256:1becfd551cea8b80bc0821f266396c69ea435d644d734a605379da342902ad37", + "isolation-sentinel-baseline.log": "sha256:69907c6283e73d78a50c871ff3f021da07c00b8902330a4f7b6fce72fc5b0071", + "isolation-baseline.log": "sha256:caa59ec08f580e3130ece8f55340e0ffbf170b205cd6e8c7263d5cb0a8948cfa", + "integration-safe.log": "sha256:e4bfb3379648c76417228ff5049df1fdbfc09f81a979500e549e1eed41a61c4e", + "run-baseline.py": "sha256:02e2a409cd23fb52f518c202c923877fdcc381f708b4e8d77bad04e034b46e73", + "independent.log": "sha256:ffb5623ba301aebcf597a5009d7cfe3e04356fd59f1e4bc858084f3e8d0e4c5a", + "setup.py": "sha256:fcdeb50a196ce5afda6243b9fb51584af58ba5d55e75a4caed017f742d560500", + "sitecustomize.py": "sha256:6b7cec873fa1fca6767b77f514bec499adb5e1e7745dbc8eddf36771917ad4f4" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/reviewer-probes.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/reviewer-probes.py new file mode 100644 index 00000000..db57d015 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/reviewer-probes.py @@ -0,0 +1,94 @@ +"""Reviewer-owned synthetic probes; no native owner acceptance.""" +from dataclasses import asdict, replace +import json +import os +import socket +import subprocess + +import pytest +import yaml +from tests.test_case_run import make_rig, fixture_catalog, lane_entry +from warden.case_run import digest +from warden.case_receipt_state import Refused, Outcome + + +def setup(tmp_path, monkeypatch, *, entries=None): + monkeypatch.delenv('WARDEN_AGENT_ID', raising=False) + rig = make_rig(tmp_path) + ex, original, calls, sources, kwargs = rig + case, typ, _, _ = sources + d = replace(original[0], effect='change', revocation_scopes=('parent-session',)) + ds = json.dumps(asdict(d)).encode() + t = yaml.safe_load(typ) + t['spec']['execution_descriptors'] = {d.step: digest(ds)} + t['spec']['follow_through'] = [dict(id=d.step,owner=d.owner,lane=d.lane,warden_executes=False,attended=True)] + catalog = fixture_catalog(*(entries or [dict(lane_entry(),lane='login')])) + ex.evidence_verifier = lambda *_: True + admitted = ex.admit(case,yaml.safe_dump(t).encode(),ds,catalog,**kwargs) + return ex, admitted, calls, catalog + + +def count(ex): + return tuple(ex.journal.db.execute('SELECT COUNT(*) FROM '+name).fetchone()[0] for name in ['intents','receipts']) + + +def prepare(ex, admitted, operation): + if operation == 'rollback': + assert ex.run(*admitted).last.outcome is Outcome.SUCCESS + if operation == 'reconcile': + old = ex.adapters[admitted[0].adapter] + def lost(_): + raise RuntimeError('lost synthetic response') + ex.adapters[old.reference] = replace(old,invoke=lost) + assert ex.run(*admitted).last.outcome is Outcome.UNCERTAIN + + +@pytest.mark.parametrize('operation',['run','rollback','reconcile']) +@pytest.mark.parametrize('tamper',['marker','empty-marker','actor','principal','scopes','expired-session']) +def test_each_cached_operation_rejects_changed_authority(tmp_path,monkeypatch,operation,tamper): + ex,args,calls,_ = setup(tmp_path,monkeypatch) + prepare(ex,args,operation) + callbacks=[] + old=ex.adapters[args[0].adapter] + def forbidden(*_): + callbacks.append('called') + raise AssertionError('must not dispatch') + ex.adapters[old.reference]=replace(old,invoke=forbidden,rollback=forbidden,reconcile=forbidden) + before=count(ex) + if tamper in ('marker','empty-marker'): + monkeypatch.setenv('WARDEN_AGENT_ID', '' if tamper=='empty-marker' else 'review-agent') + elif tamper=='actor':args=(args[0],replace(args[1],actor='agent'),args[2]) + elif tamper=='principal':args=(args[0],replace(args[1],principal='other'),args[2]) + elif tamper=='scopes':args=(replace(args[0],revocation_scopes=()),args[1],args[2]) + else:ex.evidence_verifier=lambda *_: False + with pytest.raises(Refused):getattr(ex,operation)(*args) + assert count(ex)==before + assert callbacks==[] + + +@pytest.mark.parametrize('operation',['run','rollback','reconcile']) +def test_dispatch_reparses_exact_admitted_catalog(tmp_path,monkeypatch,operation): + ex,args,_,catalog = setup(tmp_path,monkeypatch) + prepare(ex,args,operation) + import warden.case_run as source + parse=source._catalog_lane + seen=[] + def checked(blob,descriptor): + seen.append(blob) + return parse(blob,descriptor) + monkeypatch.setattr(source,'_catalog_lane',checked) + # A later ambient file must not replace the authenticated admission snapshot. + monkeypatch.setenv('WARDEN_ROUTING_CATALOG',str(tmp_path/'untrusted.yaml')) + (tmp_path/'untrusted.yaml').write_bytes(fixture_catalog(dict(lane_entry(),lane='secret'))) + result=getattr(ex,operation)(*args) + assert result.last.outcome in (Outcome.SUCCESS,Outcome.ROLLED_BACK,Outcome.NO_CHANGE) + assert seen==[catalog] + assert isinstance(catalog,bytes) + + +def test_isolation_denies_network_and_native_process(): + assert os.environ.get('BAO_TOKEN') is None + assert os.environ.get('VAULT_TOKEN') is None + assert list(__import__('pathlib').Path.home().glob('*'))==[] + with pytest.raises(RuntimeError,match='sockets disabled'):socket.socket() + with pytest.raises(RuntimeError,match='subprocess disabled'):subprocess.run(['bao','token','lookup']) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run-baseline.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run-baseline.py new file mode 100644 index 00000000..3875d7e8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run-baseline.py @@ -0,0 +1,9 @@ +from pathlib import Path +import subprocess,sys +r=Path('/tmp/warden-kernel-review-r2') +env={'HOME':str(r/'home'),'PATH':'/usr/bin:/bin','PYTHONPATH':str(r/'guard')+':'+str(r/'baseline/src'),'PYTHONDONTWRITEBYTECODE':'1','WARDEN_TEST_NETKINGDOM_ROOT':str(r/'net-kingdom'),'LANG':'C.UTF-8'} +p='/tmp/warden-kernel-review-r1/final/.venv/bin/python' +with (r/sys.argv[1]).open('w') as log: + result=subprocess.run([p,*sys.argv[2:]],cwd=r/'baseline',env=env,stdout=log,stderr=subprocess.STDOUT) +print(result.returncode) +sys.exit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run.py new file mode 100644 index 00000000..7bfae1a2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/run.py @@ -0,0 +1,9 @@ +from pathlib import Path +import subprocess,sys +r=Path('/tmp/warden-kernel-review-r2') +env={'HOME':str(r/'home'),'PATH':'/usr/bin:/bin','PYTHONPATH':str(r/'guard')+':'+str(r/'candidate/src'),'PYTHONDONTWRITEBYTECODE':'1','WARDEN_TEST_NETKINGDOM_ROOT':str(r/'net-kingdom'),'LANG':'C.UTF-8'} +p='/tmp/warden-kernel-review-r1/final/.venv/bin/python' +with (r/sys.argv[1]).open('w') as log: + result=subprocess.run([p,*sys.argv[2:]],cwd=r/'candidate',env=env,stdout=log,stderr=subprocess.STDOUT) +print(result.returncode) +sys.exit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/setup.py b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/setup.py new file mode 100644 index 00000000..e06203b6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-review-r2/setup.py @@ -0,0 +1,13 @@ +from pathlib import Path +import subprocess,tarfile,io,hashlib,json +root=Path('/tmp/warden-kernel-review-r2') +packet=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/warden-1.3-kernel-fixes-r1') +for name,repo,rev in [('candidate','ops-warden','5abde817775d6304e3cf5a46948717a1606981bf'),('net-kingdom','net-kingdom','001a38ca2aa85155ce4840c4da22aca6e2346171')]: + tree=root/name; tree.mkdir() + archive=subprocess.check_output(['git','-C','/home/worsch/'+repo,'archive',rev]) + with tarfile.open(fileobj=io.BytesIO(archive)) as t:t.extractall(tree,filter='data') +subprocess.run(['git','apply',str(packet/'candidate.patch')],cwd=root/'candidate',check=True) +(root/'home').mkdir(); (root/'guard').mkdir() +(root/'guard/sitecustomize.py').write_text('''import sys\ndef guard(event,args):\n if event.startswith('socket.'):\n raise RuntimeError('review isolation: sockets disabled')\n if event in {'subprocess.Popen','os.system','os.exec','os.posix_spawn'}:\n raise RuntimeError('review isolation: subprocess disabled')\nsys.addaudithook(guard)\n''') +(root/'run.py').write_text('''from pathlib import Path\nimport subprocess,sys\nr=Path('/tmp/warden-kernel-review-r2')\nenv={'HOME':str(r/'home'),'PATH':'/usr/bin:/bin','PYTHONPATH':str(r/'guard')+':'+str(r/'candidate/src'),'PYTHONDONTWRITEBYTECODE':'1','WARDEN_TEST_NETKINGDOM_ROOT':str(r/'net-kingdom'),'LANG':'C.UTF-8'}\np='/tmp/warden-kernel-review-r1/final/.venv/bin/python'\nwith (r/sys.argv[1]).open('w') as log:\n result=subprocess.run([p,*sys.argv[2:]],cwd=r/'candidate',env=env,stdout=log,stderr=subprocess.STDOUT)\nprint(result.returncode)\nsys.exit(result.returncode)\n''') +print('Archived public source and applied exact patch; isolated runner created.')