diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py new file mode 100644 index 00000000..98ed1cee --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py @@ -0,0 +1,289 @@ +"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" +from dataclasses import asdict, replace +import copy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from warden.case_run import Invocation, digest +from warden.case_receipt_state import Outcome, Pins, Refused +import yaml + +ROOT = Path(__file__).parent +spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') +a = importlib.util.module_from_spec(spec) +spec.loader.exec_module(a) +spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class RegistrationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.binary = Path(self.temp.name) / 'keycape' + self.binary.write_bytes(b'synthetic checker binary') + self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', + 'parameters': f.FORGEJO_PARAMS}}).encode() + desc = a.descriptor(digest(self.binary.read_bytes())) + self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), + digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) + self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', pins) + self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') + self.secret, self.raw = f.fixture() + self.effects = [] + self.evidence = {} + self.revocations = [] + self.kw = dict(admitted=self.admitted, case_source=self.case, + expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', + check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), + publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, + revoke_session=self.revoke) + self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) + self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) + self.check_patch.start() + self.kube_patch.start() + self.addCleanup(self.check_patch.stop) + self.addCleanup(self.kube_patch.stop) + self.addCleanup(self.temp.cleanup) + + def publish(self, payload): + ref = 'owner-evidence:attempt-' + str(len(self.evidence)) + self.evidence[ref] = payload + return ref + + def revoke(self, inv): + self.revocations.append(inv) + return 'session-revoked:' + inv.operation + ':' + inv.attempt_id + + def kube(self, args, payload=None): + if args[:3] == ['get', 'namespace', 'kube-system']: + return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} + if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + return copy.deepcopy(self.secret) + if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: + return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', + 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} + if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: + self.assertEqual(payload[:2], [ + {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, + {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) + self.effects.append(copy.deepcopy(payload)) + self.secret['data']['config.yaml'] = payload[2]['value'] + self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) + return copy.deepcopy(self.secret) + raise AssertionError('unexpected tooling operation') + + def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): + adapter = a.build(**self.kw) + original = copy.deepcopy(self.secret['data']) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.SUCCESS) + self.assertEqual(result.created, ('keycape-client',)) + self.assertTrue(adapter.verify(self.inv, result)) + undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) + self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) + self.assertEqual(self.secret['data'], original) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + for payload in self.evidence.values(): + self.assertNotIn(b'synthetic-marker', payload) + self.assertNotIn(b'config.yaml', payload) + self.assertNotIn(b'signing-key', payload) + + def test_identical_client_is_no_change_and_cannot_be_removed(self): + self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.NO_CHANGE) + self.assertEqual(result.created, ()) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) + self.assertFalse(self.effects) + + def test_existing_drift_refused_and_session_revoked(self): + self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) + with self.assertRaises(Refused): + a.build(**self.kw).invoke(self.inv) + self.assertFalse(self.effects) + self.assertEqual(self.revocations, [self.inv]) + + def test_invocation_rebinding_refused_before_tooling(self): + for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, + {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, + {'pins': replace(self.inv.pins, case=digest(b'other'))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**self.kw).invoke(replace(self.inv, **changes)) + self.assertFalse(self.effects) + + def test_initial_case_descriptor_and_target_pins_are_required(self): + for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, + {'admitted': replace(self.admitted, actor='agent')}, + {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**dict(self.kw, **changes)) + self.assertFalse(self.effects) + + def test_binary_or_source_drift_refused_before_tooling(self): + adapter = a.build(**self.kw) + self.binary.write_bytes(b'changed') + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.binary.write_bytes(b'synthetic checker binary') + with patch.object(a, 'source_digest', return_value=digest(b'changed')): + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self): + substitute = Path(self.temp.name) / 'other-checker' + substitute.write_bytes(b'caller-selected executable') + with self.assertRaisesRegex(Refused, 'descriptor pin differs'): + a.build(**dict(self.kw, check_config=substitute, + check_config_digest=digest(substitute.read_bytes()))) + self.assertFalse(self.effects) + + def test_stale_revision_refuses_mutation(self): + adapter = a.build(**self.kw) + self.secret['metadata']['resourceVersion'] = '8' + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_rollback_requires_exact_provenance_and_postcreation_revision(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') + for created in ((), ('other',), ('keycape-client', 'other')): + with self.subTest(created=created), self.assertRaises(Refused): + adapter.rollback(rollback, created) + self.secret['metadata']['resourceVersion'] = '9' + with self.assertRaises(Refused): + adapter.rollback(rollback, result.created) + self.assertEqual(len(self.effects), 1) + + def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) + self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) + self.evidence[result.evidence] = b'forged' + self.assertFalse(adapter.verify(self.inv, result)) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertEqual(len(self.effects), 1) + + def test_process_loss_does_not_reconstruct_created_authority(self): + result = a.build(**self.kw).invoke(self.inv) + fresh = a.build(**self.kw) + with self.assertRaises(Refused): + fresh.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertIsNone(fresh.reconcile) + self.assertEqual(len(self.effects), 1) + + def test_failed_readback_is_uncertain_and_never_provides_creation(self): + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + adapter = a.build(**self.kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertEqual(len(self.effects), 1) + self.assertFalse(self.evidence) + self.assertEqual(self.revocations, [self.inv]) + + def test_revocation_or_publication_failure_cannot_produce_verified_success(self): + for capability in ('publish', 'revoke_session'): + self.secret, _ = f.fixture() + with self.subTest(capability=capability): + kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) + adapter = a.build(**kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertFalse(self.evidence) + + def kernel_fixture(self): + # Fixture-only proposed type/case; canonical files are never edited. + # Warden's published producer pins apply unchanged. Owner trust and + # proposed descriptor/type/case are fixture-only, never native admission. + import warden.case_inputs as inputs + from warden.case_run import Executor, Journal + root = ROOT.parents[2] + case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) + case['spec']['status'] = 'materialized' + case_source = yaml.safe_dump(case).encode() + typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) + typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} + type_source = yaml.safe_dump(typ).encode() + catalog = b'fixture-only-catalog' + sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() + pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), + digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) + invocation = replace(self.admitted, pins=pins) + adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, + case_acceptance='fixture-acceptance')) + def fixture_verifier(purpose, source, evidence): + if purpose.startswith('session-revocation:'): + _, lane, operation, attempt = purpose.split(':') + return source == digest(case_source) and lane == invocation.lane and evidence == ( + 'session-revoked:' + operation + ':' + attempt) + return evidence in {'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'} + executor = Executor(Journal(Path(self.temp.name) / 'private'), + {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) + admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, + principal='founder', actor='human', lane=invocation.lane, + acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', + descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') + self.assertEqual(admitted[1], invocation) + return executor, admitted + + def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): + executor, admitted = self.kernel_fixture() + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.SUCCESS) + self.assertEqual(result.last.created, ('keycape-client',)) + undone = executor.rollback(*admitted) + self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + + def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): + executor, admitted = self.kernel_fixture() + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) + self.assertFalse(result.last.created) + with self.assertRaises(Refused): + executor.run(*admitted, retry=True) + with self.assertRaises(Refused): + executor.rollback(*admitted) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 1) + + def test_kernel_external_revision_change_retains_uncertain_rollback(self): + executor, admitted = self.kernel_fixture() + self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) + self.secret['metadata']['resourceVersion'] = '9' + self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/workplans/NK-WP-0048-security-case-types.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/workplans/NK-WP-0048-security-case-types.md new file mode 100644 index 00000000..ab541c64 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/postimages/workplans/NK-WP-0048-security-case-types.md @@ -0,0 +1,736 @@ +--- +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-16" +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, 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 +``` + +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. Exact root review/publication receipts are required on integration. + +## T14 — Preserve created-only provenance for group ensure and revoke + +```task +id: NK-WP-0048-T14 +status: todo +priority: high +``` + +Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner +ensure/revoke contract with independently retained prestate and exact newly +created groups/memberships before exposing reversible owner callbacks. Current +ensure returns no membership-creation inventory; current revoke may remove a +membership that predated this case. Completion requires actual helper-backed +positive/refusal fixtures, all validation before mutation, per-attempt evidence +and revocation, readback, ambiguity handling, and rollback that preserves every +pre-existing/shared group and membership. No existing endpoint or service token +is treated as new authorization. + +## T15 — Define authenticated recovery for lost registration process state + +```task +id: NK-WP-0048-T15 +status: todo +priority: high +``` + +Owner: NetKingdom for registration state, with Warden owning kernel evidence +admission. Next action: select the exact existing owner evidence holder/readback +contract and define authenticated lookup for the original creation attempt. +Completion requires restart/lost-receipt fixtures that bind case/source/principal, +original created-only provenance and current native revision before any undo; +uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses +post-restart rollback and installs no reconciliation callback. A new broker or +unverified JSON receipt is not a recovery contract. Actual evidence-holder +acceptance and attended recovery remain separately authorized native work. + +## T16 — Select and review the actual machine descriptor composition + +```task +id: NK-WP-0048-T16 +status: todo +priority: high +``` + +Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden +independently owns global provenance/admission. Next action: identify the exact +reviewed checker executable artifact and native owner evidence/session callback +contracts, then assemble the complete step descriptor set and a proposed explicit +type version selection. T13's retained descriptor pins a synthetic checker only. +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. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/preimages/workplans/NK-WP-0048-security-case-types.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/preimages/workplans/NK-WP-0048-security-case-types.md new file mode 100644 index 00000000..4901a63c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/preimages/workplans/NK-WP-0048-security-case-types.md @@ -0,0 +1,647 @@ +--- +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-16" +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, 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. Next action: wrap the existing contained `keycape-client.py` +registration/config/env and identity-provisioner ensure/revoke entry points in +bounded owner adapters or receipt-only attended steps for Warden's closed kernel. +Reuse the existing caller/session boundary, CAS/readback and status-only outputs; +never acquire user-engine's runtime identity or expose registry/client secrets. +Completion requires exact adapter/source descriptors, positive and refusal source +checks, uncertainty/created-only rollback behavior and a separate owner review of +the proposed machine seam. Source preparation can begin without credentials. +Warden independently owns global provenance/admission integration; Platform owns +mint/broker and native structural receipts; Apps owns consumer delivery/group +acceptance. Their assent is not supplied by this task. Pinning descriptors into +an acknowledged type remains a separately reviewed type-source change. Actual +attended execution still requires its existing owner authorization and native +receipts. T06 retains all native/reference lifecycle acceptance scope. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-dependencies.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-dependencies.json new file mode 100644 index 00000000..8407e5c4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-dependencies.json @@ -0,0 +1,145 @@ +[ + { + "id": "5be011af-9971-41de-aee2-0df91c79af9c", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on WARDEN-WP-0044-T03", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:23.339345Z", + "updated_at": "2026-10-11T21:25:23.339345Z" + }, + { + "id": "5237ec7c-866c-4f27-a8cb-702783a8a905", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "56cea6be-c613-586c-80cd-125340b374da", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on MASON-WP-0010-T03", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:23.096030Z", + "updated_at": "2026-10-11T21:25:23.096030Z" + }, + { + "id": "a90f4024-8184-476d-b739-eaac2351edae", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "8e519082-dc8b-5e46-b660-03d03e55dcf6", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T08", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:22.707856Z", + "updated_at": "2026-10-11T21:25:22.707856Z" + }, + { + "id": "5d4af363-6360-43e0-bbb5-8a98328d32ec", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7", + "to_workplan_id": null, + "to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T07 depends_on NK-WP-0048-T06", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:23.604357Z", + "updated_at": "2026-10-11T21:25:23.604357Z" + }, + { + "id": "43cc0e48-0cf4-4b76-b970-1fa614d30120", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "3d987d3d-0797-538b-86e4-51185a327ae2", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T05", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:22.002531Z", + "updated_at": "2026-10-11T21:25:22.002531Z" + }, + { + "id": "9c4debbd-3083-4f99-ae87-ea9242e44194", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "0ee12dfc-8260-5038-bd39-cc7fb4113be0", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T09", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-11T21:25:22.899227Z", + "updated_at": "2026-10-11T21:25:22.899227Z" + }, + { + "id": "60e1c744-af55-428a-b15d-d58186917a0d", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "b264c10b-565c-5d29-b788-03cf5fbd2883", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T13", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-16T19:50:38.153890Z", + "updated_at": "2026-10-16T19:50:38.153890Z" + }, + { + "id": "b456021f-947c-4bbc-a17b-683bea36476f", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "1b825705-4848-5edc-832f-b1622cd9bdb7", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T15", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-16T19:50:38.468028Z", + "updated_at": "2026-10-16T19:50:38.468028Z" + }, + { + "id": "953f22a9-db9d-45b7-b9cd-f7a994637ccb", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T12", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-16T19:47:17.264847Z", + "updated_at": "2026-10-16T19:47:17.264847Z" + }, + { + "id": "38ffb7ca-f4b7-414e-8199-0046ca66ee71", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "fce95440-a25b-5f27-8ef3-a3d182099113", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T14", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-16T19:50:38.342407Z", + "updated_at": "2026-10-16T19:50:38.342407Z" + }, + { + "id": "ee7151bc-9ce5-49e0-a6f5-4e2d728506ae", + "from_workplan_id": "e31f3f9d-0748-5137-b0a3-7b3ca46a66eb", + "from_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "to_workplan_id": null, + "to_task_id": "200bf500-5084-5ae7-be26-3033fe1f296e", + "relationship_type": "starts_after", + "description": "NK-WP-0048-T06 depends_on NK-WP-0048-T16", + "source_owner": null, + "source_commit": null, + "created_at": "2026-10-16T19:50:38.587474Z", + "updated_at": "2026-10-16T19:50:38.587474Z" + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-tasks.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-tasks.json new file mode 100644 index 00000000..54c61482 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/projection-tasks.json @@ -0,0 +1,120 @@ +[ + { + "id": "NK-WP-0048-T01", + "uuid": "930ac3e9-20ec-5473-98cd-3fe8fddb730f", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T02", + "uuid": "19c25d96-a55c-58f0-9168-3fdbc4955fc2", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T03", + "uuid": "e487975b-73b1-516c-971d-da4cbbb93063", + "status": "done", + "depends_on": [ + "NK-WP-0048-T01" + ] + }, + { + "id": "NK-WP-0048-T04", + "uuid": "2fffe112-7d55-5680-9952-78d74f44f87f", + "status": "done", + "depends_on": [ + "NK-WP-0048-T02", + "NK-WP-0048-T03" + ] + }, + { + "id": "NK-WP-0048-T05", + "uuid": "3d987d3d-0797-538b-86e4-51185a327ae2", + "status": "done", + "depends_on": [ + "NK-WP-0048-T04", + "MASON-WP-0010-T01", + "WARDEN-WP-0044-T01" + ] + }, + { + "id": "NK-WP-0048-T06", + "uuid": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c", + "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", + "MASON-WP-0010-T03", + "WARDEN-WP-0044-T03" + ] + }, + { + "id": "NK-WP-0048-T07", + "uuid": "c3c9c3ae-4f23-5ba7-9e6f-e63bcf29b9f7", + "status": "wait", + "depends_on": [ + "NK-WP-0048-T06" + ] + }, + { + "id": "NK-WP-0048-T08", + "uuid": "8e519082-dc8b-5e46-b660-03d03e55dcf6", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T09", + "uuid": "0ee12dfc-8260-5038-bd39-cc7fb4113be0", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T10", + "uuid": "4b06ca97-c1e4-5680-bf22-ab75ab1646a4", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T11", + "uuid": "ef2a0119-f59e-5e45-9519-115da9df7b85", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T12", + "uuid": "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd", + "status": "todo", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T13", + "uuid": "b264c10b-565c-5d29-b788-03cf5fbd2883", + "status": "done", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T14", + "uuid": "fce95440-a25b-5f27-8ef3-a3d182099113", + "status": "todo", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T15", + "uuid": "1b825705-4848-5edc-832f-b1622cd9bdb7", + "status": "todo", + "depends_on": [] + }, + { + "id": "NK-WP-0048-T16", + "uuid": "200bf500-5084-5ae7-be26-3033fe1f296e", + "status": "todo", + "depends_on": [] + } +] diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/proposed-descriptor.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/proposed-descriptor.json new file mode 100644 index 00000000..4d6ad4a7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/proposed-descriptor.json @@ -0,0 +1 @@ +{"actor":"human","adapter":"net-kingdom/keycape-registration@v1","adapter_source":"sha256:284e1d70b56461c6190ea634dea63d1b55890d657df8379f2459dfc882260142","allowed_created":["keycape-client"],"effect":"change","lane":"key-cape-oidc-login","owner":"net-kingdom","retry_safe":false,"revocation_scopes":["parent-session"],"step":"register-client","target_parameter":"client_id","version":"warden.case.execution.v1"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.index b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.index new file mode 100644 index 00000000..ea9ccd0b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.index @@ -0,0 +1,100 @@ + +# Custodian Brief — net-kingdom + +**Domain:** infotech +**Last synced:** 2026-10-15 17:24 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Implement deterministic posture and evidence feedback +Progress: 4/5 done | workplan_id: `9d7b04f9-3803-5613-b7a5-8bd606c77f5a` + +**Open tasks:** +- ! Obtain audit-core freshness adoption `7d2029d2` + *(wait: audit-core tenancy.yaml must publish structured authoritative owner, observation/expiry and replacement-action metadata for WH-ENG-20260822-AUDIT-E2-03. Re-evaluate with explicit current as-of; later recovery prose does not renew E2.)* + +### Publish the NetKingdom emission-cadence security profile +Progress: 4/5 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc` + +**Open tasks:** +- ! Bind and hand off the published contract `e3fbc8b8` + *(wait: Approval Engine and Qonto owner declarations now pass the published schema and supplied-inventory profile; local-identity incompatibility is retained. Requires actual sender cadence mappings, lifecycle delivery/readback/reconciliation and Kings Guard operational feed evidence.)* + +### Reconcile reef placement and security-zone canon dependencies +Progress: 3/6 done | workplan_id: `965ad365-6b81-50a1-a2a3-2d0c1fcce0b4` + +**Open tasks:** +- ! T02 — Extend provider declarations to reefs `5b35b646` + *(wait: Requires repo-manager acceptance of the reef provider carrier/schema, then railiance-infra authored ceilings, conditions and evidence in the authoritative reef declaration.)* +- ! T03 — Reconcile reef ceilings mechanically `e3f0bb0f` + *(wait: Mechanical join of reef ceilings to consumer V declarations waits on T02.)* +- ! T06 — Resolve the `DataClassification` mismatch `bf8b3e1d` + *(wait: Requires InfoTechCanon ruling on public disclosure classification versus synthetic provenance, then ops-warden authoritative dataclass_floor mapping and tests; no public-to-synthetic alias.)* + +### Remove bootstrap secret output and verify separated recovery custody +Progress: 1/3 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804` + +**Open tasks:** +- ! Verify owner-approved recovery and current native callers `d68cd4d3` + *(wait: Source caller reconciliation and exact owner-scoped isolated recovery ceremony are required; no live bootstrap or root retrieval is authorized by this source receipt.)* +- ► Reconcile other legacy custody callers and root-output boundaries `479d6970` + +### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker +Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07` + +**Open tasks:** +- ! Admit the database consumer `2fe6f100` + *(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)* +- ! Package the broker deployment `32d1411b` + *(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)* +- ! Integrate an upstream identity provider `6697a994` + *(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)* +- ! Define federated assurance `d845380d` + *(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)* +- ! Verify IAM conformance and coexistence `9cb7fd93` + *(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)* +- ! Enforce tenant and platform boundaries `38998c68` + *(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)* +- ! Prove recovery and audit delivery `e5f44ddc` + *(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)* +- … and 1 more open tasks + +### Establish SecurityCaseTypes so that known security cases are light +Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb` + +**Open tasks:** +- ! T05 — Acknowledge the type through informed-decision `d9b26bcf` + *(wait: 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.)* +- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae` + *(wait: The next types follow once the first one is proven end to end.)* + +### Register Playwright human and agent consumers through governed identity delivery +Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377` + +**Open tasks:** +- ! Verify issuer and real consumer acceptance `5a3aaf57` + *(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)* +- ► Deliver custody integration and guarded registration activation `a9dadd07` + +### Define an execution-attribution receipt for Railiance runs +Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487` + +**Open tasks:** +- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1` + *(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)* + +### Cut over NetKingdom identity to railiance01 and retire CoulombCore +Progress: 7/8 done | workplan_id: `d76ddccc-00c8-548a-b141-2cd660fa38da` + +**Open tasks:** +- ! T08 - Final deletion and closure `42a3b4c0` + *(wait: Founder 2026-10-11: coulombcore is retired and the machine reused (D4 batch approved). Order: host-side inventory (containers, on-disk manifests; no kube API, certs expired 2026-08-31), then stop k3s for good and wipe for reuse. Executes after Sol's passing railiance01 restore/restart drill receipt. Never rotate certs or restart k3s there first.)* + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.worktree b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.worktree new file mode 100644 index 00000000..18bd5dff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/brief.worktree @@ -0,0 +1,85 @@ + +# Custodian Brief — net-kingdom + +**Domain:** infotech +**Last synced:** 2026-10-16 19:19 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Publish the NetKingdom emission-cadence security profile +Progress: 5/6 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc` + +**Open tasks:** +- ► Bind and hand off the published contract `e3fbc8b8` + +### Register Playwright human and agent consumers through governed identity delivery +Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377` + +**Open tasks:** +- ! Verify issuer and real consumer acceptance `5a3aaf57` + *(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)* +- ► Deliver custody integration and guarded registration activation `a9dadd07` + +### Remove bootstrap secret output and verify separated recovery custody +Progress: 22/31 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804` + +**Open tasks:** +- ! Accept remaining recovery and caller evidence and close the G08 handoff `d68cd4d3` + *(wait: Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete.)* +- ! Resolve historical credential and configuration-preserving rotation dispositions `3ea6bf00` + *(wait: Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged.)* +- ! Verify current caller and provider admission at their actual boundaries `77aa42be` + *(wait: Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers.)* +- ! Prove retained privacyIDEA token decryption and existing-token MFA `6f8af1a7` + *(wait: LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs.)* +- ! Prove restored KeyCape issuer, policy and consumer continuity `141dd773` + *(wait: LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.)* +- ! Prove restored Authelia storage and OIDC continuity `b47f9975` + *(wait: LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15.)* +- ► Map current callers, owners and native acceptance gaps `479d6970` +- … and 2 more open tasks + +### Establish SecurityCaseTypes so that known security cases are light +Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb` + +**Open tasks:** +- ! T05 — Acknowledge the type through informed-decision `d9b26bcf` + *(wait: 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.)* +- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae` + *(wait: The next types follow once the first one is proven end to end.)* + +### Define an execution-attribution receipt for Railiance runs +Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487` + +**Open tasks:** +- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1` + *(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)* + +### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker +Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07` + +**Open tasks:** +- ! Admit the database consumer `2fe6f100` + *(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)* +- ! Package the broker deployment `32d1411b` + *(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)* +- ! Integrate an upstream identity provider `6697a994` + *(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)* +- ! Define federated assurance `d845380d` + *(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)* +- ! Verify IAM conformance and coexistence `9cb7fd93` + *(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)* +- ! Enforce tenant and platform boundaries `38998c68` + *(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)* +- ! Prove recovery and audit delivery `e5f44ddc` + *(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)* +- … and 1 more open tasks + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/index-record.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/index-record.txt new file mode 100644 index 00000000..f072587f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/index-record.txt @@ -0,0 +1 @@ +100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0 .custodian-brief.md diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/status.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/status.txt new file mode 100644 index 00000000..30b297bf --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/status.txt @@ -0,0 +1,7 @@ +MM .custodian-brief.md + M WORK-RECORDS.md + M workplans/NK-WP-0048-security-case-types.md +?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/ +?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl +?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json +?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/work-records.preimage b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/work-records.preimage new file mode 100644 index 00000000..35eb3e92 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/publication-backup/work-records.preimage @@ -0,0 +1,263 @@ +# Work Records — net-kingdom + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | NK-WP-ADHOC-2026-07-02 | finished | — | workplans/ADHOC-2026-07-02.md | +| workplan | NK-WP-ADHOC-2026-08-14 | finished | — | workplans/ADHOC-2026-08-14.md | +| workplan | NK-WP-ADHOC-2026-08-23 | finished | — | workplans/ADHOC-2026-08-23.md | +| workplan | NK-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | NK-WP-ADHOC-2026-10-10 | finished | — | workplans/ADHOC-2026-10-10.md | +| workplan | NK-WP-ADHOC-2026-10-11 | finished | — | workplans/ADHOC-2026-10-11.md | +| workplan | NK-WP-ADHOC-2026-10-16 | finished | — | workplans/ADHOC-2026-10-16.md | +| workplan | NET-WP-0020 | finished | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| workplan | NK-WP-0009 | finished | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| workplan | NK-WP-0011 | active | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| workplan | NK-WP-0021 | finished | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| workplan | NK-WP-0022 | finished | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| workplan | NK-WP-0023 | finished | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| workplan | NK-WP-0024 | finished | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| workplan | NK-WP-0025 | finished | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| workplan | NK-WP-0026 | finished | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| workplan | NK-WP-0027 | backlog | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| workplan | NK-WP-0028 | finished | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| workplan | NK-WP-0029 | finished | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| workplan | NK-WP-0030 | finished | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| workplan | NK-WP-0031 | finished | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| workplan | NK-WP-0032 | finished | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| workplan | NK-WP-0033 | finished | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| workplan | NK-WP-0034 | finished | — | workplans/NK-WP-0034-verification-that-verifies.md | +| workplan | NK-WP-0035 | active | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| workplan | NK-WP-0036 | finished | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| workplan | NK-WP-0037 | finished | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| workplan | NK-WP-0038 | finished | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| workplan | NK-WP-0039 | finished | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| workplan | NK-WP-0040 | active | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| workplan | NK-WP-0041 | finished | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| workplan | NK-WP-0042 | finished | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| workplan | NK-WP-0043 | finished | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| workplan | NK-WP-0044 | finished | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| workplan | NK-WP-0045 | finished | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| workplan | NK-WP-0046 | finished | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| workplan | NK-WP-0047 | finished | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| workplan | NK-WP-0048 | active | — | workplans/NK-WP-0048-security-case-types.md | +| workplan | NK-WP-0049 | active | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| workplan | NK-WP-0050 | active | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| workplan | NK-WP-0051 | finished | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | +| task | NK-WP-ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md | +| task | NK-WP-ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-14-T02 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-14-T03 | done | — | workplans/ADHOC-2026-08-14.md | +| task | NK-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md | +| task | NK-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| task | NK-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | +| task | NK-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T03 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T04 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T05 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T06 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-11-T07 | done | — | workplans/ADHOC-2026-10-11.md | +| task | NK-WP-ADHOC-2026-10-16-T01 | done | — | workplans/ADHOC-2026-10-16.md | +| task | NET-WP-0020-T01 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T02 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T03 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T04 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NET-WP-0020-T05 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | +| task | NK-WP-0009-T01 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T02 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T03 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T04 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T05 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0009-T06 | done | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | +| task | NK-WP-0011-T01 | done | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T02 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T03 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T04 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T05 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T06 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T07 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T08 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0011-T09 | wait | — | workplans/NK-WP-0011-enterprise-federation-saml.md | +| task | NK-WP-0021-T01 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T02 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T03 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T04 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0021-T05 | done | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +| task | NK-WP-0022-T01 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T02 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T03 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T04 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T05 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T06 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T07 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0022-T08 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +| task | NK-WP-0023-T01 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T02 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T03 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T04 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T05 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T06 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T07 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0023-T08 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +| task | NK-WP-0024-T01 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T02 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T03 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0024-T04 | done | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | +| task | NK-WP-0025-T01 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T02 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T03 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T04 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0025-T05 | done | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | +| task | NK-WP-0026-T01 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| task | NK-WP-0026-T02 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +| task | NK-WP-0027-T01 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T02 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T03 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T04 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T05 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0027-T06 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +| task | NK-WP-0028-T01 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T02 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T03 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0028-T04 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +| task | NK-WP-0029-T01 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T02 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T03 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0029-T04 | done | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | +| task | NK-WP-0030-T01 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T02 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T03 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T04 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0030-T05 | done | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +| task | NK-WP-0031-T01 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T02 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T03 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T04 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0031-T05 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +| task | NK-WP-0032-T01 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T02 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T03 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0032-T04 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +| task | NK-WP-0033-T01 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T02 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T03 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T04 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0033-T05 | done | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | +| task | NK-WP-0034-T01 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T02 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T03 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0034-T04 | done | — | workplans/NK-WP-0034-verification-that-verifies.md | +| task | NK-WP-0035-T01 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T02 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T03 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T04 | progress | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0035-T06 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +| task | NK-WP-0036-T01 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T02 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T04 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T05 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0036-T06 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +| task | NK-WP-0037-T01 | done | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| task | NK-WP-0037-T02 | done | — | workplans/NK-WP-0037-vergabe-company-welcome.md | +| task | NK-WP-0038-T01 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| task | NK-WP-0038-T02 | done | — | workplans/NK-WP-0038-tenant-scoped-identity-lifecycle.md | +| task | NK-WP-0039-T01 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T02 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T03 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0039-T04 | done | — | workplans/NK-WP-0039-flex-auth-access-engine-coordinate-intake.md | +| task | NK-WP-0040-T01 | done | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| task | NK-WP-0040-T02 | wait | — | workplans/NK-WP-0040-execution-attribution-receipt.md | +| task | NK-WP-0041-T01 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0041-T02 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0041-T03 | done | — | workplans/NK-WP-0041-onboarding-journey-usability.md | +| task | NK-WP-0042-T01 | done | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| task | NK-WP-0042-T02 | cancel | — | workplans/NK-WP-0042-workload-mfa-step-up.md | +| task | NK-WP-0043-T01 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0043-T02 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0043-T03 | done | — | workplans/NK-WP-0043-cadence-assessment-and-operating-guidance.md | +| task | NK-WP-0044-T01 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T02 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T03 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T04 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T05 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T06 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0044-T07 | done | — | workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md | +| task | NK-WP-0045-T01 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T02 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T03 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T04 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T05 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0045-T06 | done | — | workplans/NK-WP-0045-root-grant-custody-record.md | +| task | NK-WP-0046-T01 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0046-T02 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0046-T03 | done | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | +| task | NK-WP-0047-T01 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0047-T02 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0047-T03 | done | — | workplans/NK-WP-0047-platform-password-reset-mail.md | +| task | NK-WP-0048-T01 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T02 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T03 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T04 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T05 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T06 | wait | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T07 | wait | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T08 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T09 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T10 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T11 | done | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0048-T12 | todo | — | workplans/NK-WP-0048-security-case-types.md | +| task | NK-WP-0049-T01 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T02 | progress | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T03 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T04 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T05 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T06 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T07 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T08 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T09 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T10 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T11 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T12 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T13 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T14 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T15 | progress | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T16 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T17 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T18 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T19 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T20 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T21 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T22 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T23 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T24 | todo | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T25 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T26 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T27 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T28 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T29 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T30 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0049-T31 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | +| task | NK-WP-0050-T01 | done | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0050-T02 | progress | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0050-T03 | wait | — | workplans/NK-WP-0050-playwright-consumer-registration.md | +| task | NK-WP-0051-T01 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T02 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T03 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T04 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T05 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| task | NK-WP-0051-T06 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | +| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NET-IN-0001 | open | — | intakes/intakes.md | +| intake | NET-IN-0002 | open | — | intakes/intakes.md | +| intake | NET-IN-0003 | open | — | intakes/intakes.md | +| intake | NET-IN-0004 | open | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/record_publication_r3.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/record_publication_r3.py new file mode 100644 index 00000000..5d05bfec --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/record_publication_r3.py @@ -0,0 +1,21 @@ +from pathlib import Path +import re,subprocess +owner=Path('/home/worsch/net-kingdom');rel='workplans/NK-WP-0048-security-case-types.md';packet=Path(__file__).resolve().parent +source=(owner/rel).read_text();committed=subprocess.check_output(['git','-C',str(owner),'show','HEAD:'+rel]).decode() +# The ordinary sync may inject only these exact new UUID lines here. +without=re.sub(r'^state_hub_task_id: "[^"\n]+"\n','',source,flags=re.M) +base_without=re.sub(r'^state_hub_task_id: "[^"\n]+"\n','',committed,flags=re.M) +if without!=base_without:raise SystemExit('unexpected source writeback beyond UUIDs') +needle='## T14 — Preserve created-only provenance for group ensure and revoke' +paragraph='''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. + +''' +if source.count(needle)!=1:raise SystemExit('task anchor differs') +(owner/rel).write_text(source.replace(needle,paragraph+needle)) +print('Recorded actual publication and normal projection outcome alongside four normal UUID injections.') diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/implementation.patch new file mode 100644 index 00000000..d29e95a9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/implementation.patch @@ -0,0 +1,529 @@ +--- /dev/null ++++ b/sso-mfa/k8s/keycape/keycape-registration-adapter.py +@@ -0,0 +1,178 @@ ++"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. ++ ++No CLI, credentials, evidence trust root or live registration is installed here. ++Owner publication/authentication and parent-session revocation are mandatory ++injected capabilities. The kernel must independently admit both evidence kinds. ++""" ++from __future__ import annotations ++ ++import argparse ++from dataclasses import asdict, replace ++import json ++import importlib.util ++from pathlib import Path ++ ++import yaml ++ ++from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER ++from warden.case_receipt_state import Outcome, Refused ++ ++ROOT = Path(__file__).resolve().parent ++SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', ++ 'portal-client-rollout.py', 'openbao-client-config.py', ++ 'register-user-engine-portal.py') ++REFERENCE = 'net-kingdom/keycape-registration@v1' ++ ++# Importing the maintained helper performs no tooling operation. ++_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') ++helper = importlib.util.module_from_spec(_spec) ++_spec.loader.exec_module(helper) ++ ++ ++def source_digest(): ++ """Pin adapter plus the complete local helper import closure, in filename order.""" ++ return digest(json.dumps({name: digest((ROOT / name).read_bytes()) ++ for name in SOURCE_FILES}, sort_keys=True, ++ separators=(',', ':')).encode()) ++ ++ ++def descriptor(): ++ return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', ++ REFERENCE, source_digest(), 'human', 'key-cape-oidc-login', ++ 'client_id', 'change', ('keycape-client',), False, ++ ('parent-session',)) ++ ++ ++def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, ++ expected_resource_version: str, case_acceptance: str, ++ check_config: Path, check_config_digest: str, publish, authenticate, ++ revoke_session): ++ """Bind one admitted case and observed revision; return a closed kernel Adapter. ++ ++ publish(bytes)->identifier durably retains only the closed metadata payload. ++ authenticate(identifier, bytes)->True must verify independent owner custody. ++ revoke_session(invocation)->identifier must revoke this attempt's parent ++ session; Warden independently authenticates it using its own evidence verifier. ++ These callbacks are capabilities, never command strings or case parameters. ++ ++ Only a verified creation in this process may be rolled back. Process loss, ++ write/readback ambiguity, publication/revocation failure and source drift ++ require owner reconciliation; none are converted to retry-safe failures. ++ """ ++ reviewed_source = source_digest() ++ desc = descriptor() ++ if type(admitted) is not Invocation or not all( ++ isinstance(value, str) and IDENTIFIER.fullmatch(value) ++ for value in (admitted.step, admitted.owner, admitted.target, ++ admitted.actor, admitted.principal, admitted.lane)): ++ raise Refused('bounded invocation required') ++ if desc.adapter_source != reviewed_source: ++ raise Refused('owner source changed during binding') ++ if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( ++ desc.step, desc.owner, desc.actor, desc.lane): ++ raise Refused('registration invocation boundary differs') ++ if admitted.operation != 'admission' or admitted.attempt_id: ++ raise Refused('expected admission invocation') ++ if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: ++ raise Refused('case source pin differs') ++ # Pins.adapter is the descriptor digest, not the owner source digest. ++ descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ if admitted.pins.adapter != digest(descriptor_bytes): ++ raise Refused('descriptor pin differs') ++ if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in ++ (expected_uid, expected_resource_version, case_acceptance)): ++ raise Refused('bounded attended binding required') ++ if digest(case_acceptance.encode()) != admitted.pins.acceptance: ++ raise Refused('acceptance evidence binding differs') ++ if not all(callable(x) for x in (publish, authenticate, revoke_session)): ++ raise Refused('owner evidence and revocation capabilities required') ++ with contained_output(): ++ case = yaml.safe_load(case_source) ++ client = helper.client_from_case(case) ++ if admitted.target != client['clientId']: ++ raise Refused('case target differs') ++ check_config = Path(check_config) ++ records = {} ++ creation = None ++ ++ def binding(inv, operation): ++ if (replace(inv, attempt_id='', operation='admission') != admitted ++ or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): ++ raise Refused('attempt binding differs') ++ if source_digest() != reviewed_source: ++ raise Refused('owner source changed') ++ if digest(check_config.read_bytes()) != check_config_digest: ++ raise Refused('check-config binary changed') ++ ++ def execute(inv, operation, created=()): ++ nonlocal creation ++ try: ++ with contained_output(): ++ binding(inv, 'run' if operation == 'add' else 'rollback') ++ uid, revision = expected_uid, expected_resource_version ++ if operation == 'remove': ++ if (created != ('keycape-client',) or creation is None ++ or not verify(*creation)): ++ raise Refused('no authenticated case-created client') ++ original_payload = json.loads(records[creation[1].evidence][1]) ++ uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] ++ args = argparse.Namespace(mode='apply', operation=operation, ++ expected_uid=uid, expected_resource_version=revision, ++ case_acceptance=case_acceptance, check_config=str(check_config)) ++ receipt = helper.run(args, client) ++ # The helper has completed CAS, whole-data readback and preservation. ++ # Never retain check-config diagnostics or arbitrary helper fields. ++ if not (receipt['schema'] == 'netkingdom.keycape-client.v1' ++ and receipt['target'] == 'sso/keycape-config' ++ and receipt['client_id'] == admitted.target ++ and receipt['mode'] == 'apply' and receipt['operation'] == operation ++ and receipt['values_emitted'] is False ++ and type(receipt['changed']) is bool ++ and receipt['unrelated_config_bytes_preserved'] is True ++ and receipt['other_secret_data_unchanged'] is True ++ and receipt['before'] == {'uid': uid, 'resource_version': revision} ++ and receipt['after']['uid'] == uid ++ and receipt['check_config']['proposed']['valid'] is True): ++ raise Refused('owner readback contract differs') ++ after = receipt['after'] ++ if (set(after) != {'uid', 'resource_version'} or ++ not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): ++ raise Refused('owner revision contract differs') ++ changed = receipt['changed'] ++ outcome = (Outcome.ROLLED_BACK if operation == 'remove' else ++ Outcome.SUCCESS if changed else Outcome.NO_CHANGE) ++ objects = ('keycape-client',) if operation == 'add' and changed else () ++ finally: ++ # Even helper failure must end the parent session. Failures propagate ++ # contained to the kernel and become uncertain, never safe to replay. ++ with contained_output(): ++ revocation = revoke_session(inv) ++ if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): ++ raise Refused('session revocation evidence missing') ++ payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', ++ 'invocation': asdict(inv), 'source': reviewed_source, ++ 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], ++ 'after': after, 'session_revocation': revocation}, ++ sort_keys=True, separators=(',', ':')).encode() ++ with contained_output(): ++ evidence = publish(payload) ++ if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: ++ raise Refused('owner evidence identifier invalid or reused') ++ result = OwnerResult(outcome, evidence, objects, True, revocation, ++ attempt_id=inv.attempt_id, operation=inv.operation) ++ records[evidence] = (inv, payload, result) ++ if operation == 'add' and changed: ++ creation = (inv, result) ++ elif operation == 'remove': ++ creation = None ++ return result ++ ++ def verify(inv, result): ++ record = records.get(result.evidence) if type(result) is OwnerResult else None ++ if record is None or record[0] != inv or record[2] != result: ++ return False ++ with contained_output(): ++ return authenticate(result.evidence, record[1]) is True ++ ++ return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), ++ verify, lambda inv, created: execute(inv, 'remove', created)) +--- /dev/null ++++ b/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py +@@ -0,0 +1,285 @@ ++"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" ++from dataclasses import asdict, replace ++import copy ++import importlib.util ++import json ++from pathlib import Path ++import tempfile ++import unittest ++from unittest.mock import patch ++ ++from warden.case_run import Invocation, digest ++from warden.case_receipt_state import Outcome, Pins, Refused ++import yaml ++ ++ROOT = Path(__file__).parent ++spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') ++a = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(a) ++spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') ++f = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(f) ++ ++ ++class RegistrationTests(unittest.TestCase): ++ def setUp(self): ++ self.temp = tempfile.TemporaryDirectory() ++ self.binary = Path(self.temp.name) / 'keycape' ++ self.binary.write_bytes(b'synthetic checker binary') ++ self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', ++ 'parameters': f.FORGEJO_PARAMS}}).encode() ++ desc = a.descriptor() ++ self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), ++ digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) ++ self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', pins) ++ self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') ++ self.secret, self.raw = f.fixture() ++ self.effects = [] ++ self.evidence = {} ++ self.revocations = [] ++ self.kw = dict(admitted=self.admitted, case_source=self.case, ++ expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', ++ check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), ++ publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, ++ revoke_session=self.revoke) ++ self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) ++ self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) ++ self.check_patch.start() ++ self.kube_patch.start() ++ self.addCleanup(self.check_patch.stop) ++ self.addCleanup(self.kube_patch.stop) ++ self.addCleanup(self.temp.cleanup) ++ ++ def publish(self, payload): ++ ref = 'owner-evidence:attempt-' + str(len(self.evidence)) ++ self.evidence[ref] = payload ++ return ref ++ ++ def revoke(self, inv): ++ self.revocations.append(inv) ++ return 'session-revoked:' + inv.operation + ':' + inv.attempt_id ++ ++ def kube(self, args, payload=None): ++ if args[:3] == ['get', 'namespace', 'kube-system']: ++ return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} ++ if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ return copy.deepcopy(self.secret) ++ if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: ++ return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', ++ 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} ++ if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: ++ self.assertEqual(payload[:2], [ ++ {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, ++ {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) ++ self.effects.append(copy.deepcopy(payload)) ++ self.secret['data']['config.yaml'] = payload[2]['value'] ++ self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) ++ return copy.deepcopy(self.secret) ++ raise AssertionError('unexpected tooling operation') ++ ++ def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): ++ adapter = a.build(**self.kw) ++ original = copy.deepcopy(self.secret['data']) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.created, ('keycape-client',)) ++ self.assertTrue(adapter.verify(self.inv, result)) ++ undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) ++ self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(self.secret['data'], original) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ for payload in self.evidence.values(): ++ self.assertNotIn(b'synthetic-marker', payload) ++ self.assertNotIn(b'config.yaml', payload) ++ self.assertNotIn(b'signing-key', payload) ++ ++ def test_identical_client_is_no_change_and_cannot_be_removed(self): ++ self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.NO_CHANGE) ++ self.assertEqual(result.created, ()) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) ++ self.assertFalse(self.effects) ++ ++ def test_existing_drift_refused_and_session_revoked(self): ++ self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) ++ with self.assertRaises(Refused): ++ a.build(**self.kw).invoke(self.inv) ++ self.assertFalse(self.effects) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_invocation_rebinding_refused_before_tooling(self): ++ for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, ++ {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, ++ {'pins': replace(self.inv.pins, case=digest(b'other'))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**self.kw).invoke(replace(self.inv, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_initial_case_descriptor_and_target_pins_are_required(self): ++ for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, ++ {'admitted': replace(self.admitted, actor='agent')}, ++ {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**dict(self.kw, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_binary_or_source_drift_refused_before_tooling(self): ++ adapter = a.build(**self.kw) ++ self.binary.write_bytes(b'changed') ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.binary.write_bytes(b'synthetic checker binary') ++ with patch.object(a, 'source_digest', return_value=digest(b'changed')): ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_stale_revision_refuses_mutation(self): ++ adapter = a.build(**self.kw) ++ self.secret['metadata']['resourceVersion'] = '8' ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_rollback_requires_exact_provenance_and_postcreation_revision(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') ++ for created in ((), ('other',), ('keycape-client', 'other')): ++ with self.subTest(created=created), self.assertRaises(Refused): ++ adapter.rollback(rollback, created) ++ self.secret['metadata']['resourceVersion'] = '9' ++ with self.assertRaises(Refused): ++ adapter.rollback(rollback, result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) ++ self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) ++ self.evidence[result.evidence] = b'forged' ++ self.assertFalse(adapter.verify(self.inv, result)) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_process_loss_does_not_reconstruct_created_authority(self): ++ result = a.build(**self.kw).invoke(self.inv) ++ fresh = a.build(**self.kw) ++ with self.assertRaises(Refused): ++ fresh.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertIsNone(fresh.reconcile) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_failed_readback_is_uncertain_and_never_provides_creation(self): ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ adapter = a.build(**self.kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertEqual(len(self.effects), 1) ++ self.assertFalse(self.evidence) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_revocation_or_publication_failure_cannot_produce_verified_success(self): ++ for capability in ('publish', 'revoke_session'): ++ self.secret, _ = f.fixture() ++ with self.subTest(capability=capability): ++ kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) ++ adapter = a.build(**kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.evidence) ++ ++ def kernel_fixture(self): ++ # Fixture-only proposed type/case; canonical files are never edited. ++ # Warden's current producer pins predate 015694ee. The explicit override ++ # below proves compatibility, not an owner pin update or native admission. ++ import hashlib ++ import warden.case_inputs as inputs ++ from warden.case_run import Executor, Journal ++ root = ROOT.parents[2] ++ case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) ++ case['spec']['status'] = 'materialized' ++ case_source = yaml.safe_dump(case).encode() ++ typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) ++ typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} ++ type_source = yaml.safe_dump(typ).encode() ++ catalog = b'fixture-only-catalog' ++ sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() ++ pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), ++ digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) ++ invocation = replace(self.admitted, pins=pins) ++ adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, ++ case_acceptance='fixture-acceptance')) ++ def fixture_verifier(purpose, source, evidence): ++ if purpose.startswith('session-revocation:'): ++ _, lane, operation, attempt = purpose.split(':') ++ return source == digest(case_source) and lane == invocation.lane and evidence == ( ++ 'session-revoked:' + operation + ':' + attempt) ++ return evidence in {'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'} ++ executor = Executor(Journal(Path(self.temp.name) / 'private'), ++ {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) ++ revised_pins = {rel: hashlib.sha256((root / rel).read_bytes()).hexdigest() ++ for rel in inputs.SOURCE_PINS} ++ with patch.object(inputs, 'SOURCE_PINS', revised_pins): ++ admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, ++ principal='founder', actor='human', lane=invocation.lane, ++ acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', ++ descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') ++ self.assertEqual(admitted[1], invocation) ++ return executor, admitted ++ ++ def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): ++ executor, admitted = self.kernel_fixture() ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.last.created, ('keycape-client',)) ++ undone = executor.rollback(*admitted) ++ self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ ++ def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): ++ executor, admitted = self.kernel_fixture() ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) ++ self.assertFalse(result.last.created) ++ with self.assertRaises(Refused): ++ executor.run(*admitted, retry=True) ++ with self.assertRaises(Refused): ++ executor.rollback(*admitted) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 1) ++ ++ def test_kernel_external_revision_change_retains_uncertain_rollback(self): ++ executor, admitted = self.kernel_fixture() ++ self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) ++ self.secret['metadata']['resourceVersion'] = '9' ++ self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 2) ++ ++ ++if __name__ == '__main__': ++ unittest.main() +--- /dev/null ++++ b/sso-mfa/k8s/keycape/registration-adapter.md +@@ -0,0 +1,57 @@ ++# Attended registration adapter candidate ++ ++Exercise status: unexercised ++ ++This is a source-only, injection-only adapter for the existing Warden closed ++kernel. It installs no command, runtime dispatcher, credential provider, evidence ++trust root or new service. NetKingdom type/case status and bytes remain unchanged. ++The generated descriptor is a proposal; it is not owner acknowledgment. ++ ++`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply ++mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ ++catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter ++and its complete local helper import closure. The separately selected ++`check-config` binary has its own checked digest. The caller constructs the ++proposed invocation from exact source inputs, installs the fixed adapter once, ++and calls normal `Executor.admit`; independently derived admission must match. ++No callable replacement after admission is required or supported by this recipe. ++ ++The operation uses existing cluster/issuer checks, byte-preserving merge, ++Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS ++and whole-data readback. Only closed metadata is passed to the injected durable ++owner publisher. The owner authenticator must independently authenticate those ++exact bytes. The session-revocation capability must end the same attempt's ++parent session; Warden separately verifies its exact revocation evidence. ++These capabilities are owner-selected callables, never case-supplied commands. ++No implementation of these native capabilities is supplied or implied here. ++ ++An identical existing client returns no change and creates no rollback authority. ++Removal requires this process's independently authenticated successful creation ++receipt, exact created-object label, unchanged postcreation UID/resourceVersion, ++and the original exact client record. External drift, a lost readback, receipt ++publication/revocation failure or process loss requires owner reconciliation. ++No uncertainty is labeled retry-safe. No automatic reconciliation is installed. ++Rollback after restart intentionally remains unavailable until an owner supplies ++independently authenticated durable creation provenance and reviews recovery. ++ ++The source tests execute actual maintained helper CAS/readback against synthetic ++Kubernetes objects. Three checks exercise actual Warden admission/run/rollback ++and uncertainty transitions with synthetic owner trust. Those checks explicitly ++override Warden's stale producer source pins inside the test only, selecting the ++current local source bytes. This is a compatibility fixture, not acknowledgment, ++provenance, normal native admission or deployment acceptance. ++ ++Remaining work belongs in live work records before T12 closure: ++ ++- Warden owns review and refresh of BrowserInputs producer source pins, global ++ evidence admission and exact native evidence/session capabilities. Its current ++ pins predate NetKingdom 015694ee and refuse current producer bytes. ++- NetKingdom owns separately reviewed descriptor/type pin selection, the env ++ binding/config/restart steps, and group ensure/revoke provenance. Current group ++ ensure does not report newly added memberships; current revoke can remove ++ memberships that predate the case. Wrapping these as reversible creation would ++ be unsound. Preserve them until a reviewed created-only contract exists. ++- Platform owns structural/mint/broker and secret delivery evidence; Apps owns ++ actual consumer login and group enforcement. No assent is supplied here. ++- Attended use still requires existing owner authorization, current caller ++ identity, native source/config/image pins, native receipts and cleanup proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py new file mode 100644 index 00000000..e3526aa1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py @@ -0,0 +1,178 @@ +"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. + +No CLI, credentials, evidence trust root or live registration is installed here. +Owner publication/authentication and parent-session revocation are mandatory +injected capabilities. The kernel must independently admit both evidence kinds. +""" +from __future__ import annotations + +import argparse +from dataclasses import asdict, replace +import json +import importlib.util +from pathlib import Path + +import yaml + +from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER +from warden.case_receipt_state import Outcome, Refused + +ROOT = Path(__file__).resolve().parent +SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', + 'portal-client-rollout.py', 'openbao-client-config.py', + 'register-user-engine-portal.py') +REFERENCE = 'net-kingdom/keycape-registration@v1' + +# Importing the maintained helper performs no tooling operation. +_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') +helper = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(helper) + + +def source_digest(): + """Pin adapter plus the complete local helper import closure, in filename order.""" + return digest(json.dumps({name: digest((ROOT / name).read_bytes()) + for name in SOURCE_FILES}, sort_keys=True, + separators=(',', ':')).encode()) + + +def descriptor(): + return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', + REFERENCE, source_digest(), 'human', 'key-cape-oidc-login', + 'client_id', 'change', ('keycape-client',), False, + ('parent-session',)) + + +def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, + expected_resource_version: str, case_acceptance: str, + check_config: Path, check_config_digest: str, publish, authenticate, + revoke_session): + """Bind one admitted case and observed revision; return a closed kernel Adapter. + + publish(bytes)->identifier durably retains only the closed metadata payload. + authenticate(identifier, bytes)->True must verify independent owner custody. + revoke_session(invocation)->identifier must revoke this attempt's parent + session; Warden independently authenticates it using its own evidence verifier. + These callbacks are capabilities, never command strings or case parameters. + + Only a verified creation in this process may be rolled back. Process loss, + write/readback ambiguity, publication/revocation failure and source drift + require owner reconciliation; none are converted to retry-safe failures. + """ + reviewed_source = source_digest() + desc = descriptor() + if type(admitted) is not Invocation or not all( + isinstance(value, str) and IDENTIFIER.fullmatch(value) + for value in (admitted.step, admitted.owner, admitted.target, + admitted.actor, admitted.principal, admitted.lane)): + raise Refused('bounded invocation required') + if desc.adapter_source != reviewed_source: + raise Refused('owner source changed during binding') + if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( + desc.step, desc.owner, desc.actor, desc.lane): + raise Refused('registration invocation boundary differs') + if admitted.operation != 'admission' or admitted.attempt_id: + raise Refused('expected admission invocation') + if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: + raise Refused('case source pin differs') + # Pins.adapter is the descriptor digest, not the owner source digest. + descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + if admitted.pins.adapter != digest(descriptor_bytes): + raise Refused('descriptor pin differs') + if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in + (expected_uid, expected_resource_version, case_acceptance)): + raise Refused('bounded attended binding required') + if digest(case_acceptance.encode()) != admitted.pins.acceptance: + raise Refused('acceptance evidence binding differs') + if not all(callable(x) for x in (publish, authenticate, revoke_session)): + raise Refused('owner evidence and revocation capabilities required') + with contained_output(): + case = yaml.safe_load(case_source) + client = helper.client_from_case(case) + if admitted.target != client['clientId']: + raise Refused('case target differs') + check_config = Path(check_config) + records = {} + creation = None + + def binding(inv, operation): + if (replace(inv, attempt_id='', operation='admission') != admitted + or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): + raise Refused('attempt binding differs') + if source_digest() != reviewed_source: + raise Refused('owner source changed') + if digest(check_config.read_bytes()) != check_config_digest: + raise Refused('check-config binary changed') + + def execute(inv, operation, created=()): + nonlocal creation + try: + with contained_output(): + binding(inv, 'run' if operation == 'add' else 'rollback') + uid, revision = expected_uid, expected_resource_version + if operation == 'remove': + if (created != ('keycape-client',) or creation is None + or not verify(*creation)): + raise Refused('no authenticated case-created client') + original_payload = json.loads(records[creation[1].evidence][1]) + uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] + args = argparse.Namespace(mode='apply', operation=operation, + expected_uid=uid, expected_resource_version=revision, + case_acceptance=case_acceptance, check_config=str(check_config)) + receipt = helper.run(args, client) + # The helper has completed CAS, whole-data readback and preservation. + # Never retain check-config diagnostics or arbitrary helper fields. + if not (receipt['schema'] == 'netkingdom.keycape-client.v1' + and receipt['target'] == 'sso/keycape-config' + and receipt['client_id'] == admitted.target + and receipt['mode'] == 'apply' and receipt['operation'] == operation + and receipt['values_emitted'] is False + and type(receipt['changed']) is bool + and receipt['unrelated_config_bytes_preserved'] is True + and receipt['other_secret_data_unchanged'] is True + and receipt['before'] == {'uid': uid, 'resource_version': revision} + and receipt['after']['uid'] == uid + and receipt['check_config']['proposed']['valid'] is True): + raise Refused('owner readback contract differs') + after = receipt['after'] + if (set(after) != {'uid', 'resource_version'} or + not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): + raise Refused('owner revision contract differs') + changed = receipt['changed'] + outcome = (Outcome.ROLLED_BACK if operation == 'remove' else + Outcome.SUCCESS if changed else Outcome.NO_CHANGE) + objects = ('keycape-client',) if operation == 'add' and changed else () + finally: + # Even helper failure must end the parent session. Failures propagate + # contained to the kernel and become uncertain, never safe to replay. + with contained_output(): + revocation = revoke_session(inv) + if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): + raise Refused('session revocation evidence missing') + payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', + 'invocation': asdict(inv), 'source': reviewed_source, + 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], + 'after': after, 'session_revocation': revocation}, + sort_keys=True, separators=(',', ':')).encode() + with contained_output(): + evidence = publish(payload) + if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: + raise Refused('owner evidence identifier invalid or reused') + result = OwnerResult(outcome, evidence, objects, True, revocation, + attempt_id=inv.attempt_id, operation=inv.operation) + records[evidence] = (inv, payload, result) + if operation == 'add' and changed: + creation = (inv, result) + elif operation == 'remove': + creation = None + return result + + def verify(inv, result): + record = records.get(result.evidence) if type(result) is OwnerResult else None + if record is None or record[0] != inv or record[2] != result: + return False + with contained_output(): + return authenticate(result.evidence, record[1]) is True + + return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), + verify, lambda inv, created: execute(inv, 'remove', created)) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/registration-adapter.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/registration-adapter.md new file mode 100644 index 00000000..c1925b2f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/registration-adapter.md @@ -0,0 +1,57 @@ +# Attended registration adapter candidate + +Exercise status: unexercised + +This is a source-only, injection-only adapter for the existing Warden closed +kernel. It installs no command, runtime dispatcher, credential provider, evidence +trust root or new service. NetKingdom type/case status and bytes remain unchanged. +The generated descriptor is a proposal; it is not owner acknowledgment. + +`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply +mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ +catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter +and its complete local helper import closure. The separately selected +`check-config` binary has its own checked digest. The caller constructs the +proposed invocation from exact source inputs, installs the fixed adapter once, +and calls normal `Executor.admit`; independently derived admission must match. +No callable replacement after admission is required or supported by this recipe. + +The operation uses existing cluster/issuer checks, byte-preserving merge, +Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS +and whole-data readback. Only closed metadata is passed to the injected durable +owner publisher. The owner authenticator must independently authenticate those +exact bytes. The session-revocation capability must end the same attempt's +parent session; Warden separately verifies its exact revocation evidence. +These capabilities are owner-selected callables, never case-supplied commands. +No implementation of these native capabilities is supplied or implied here. + +An identical existing client returns no change and creates no rollback authority. +Removal requires this process's independently authenticated successful creation +receipt, exact created-object label, unchanged postcreation UID/resourceVersion, +and the original exact client record. External drift, a lost readback, receipt +publication/revocation failure or process loss requires owner reconciliation. +No uncertainty is labeled retry-safe. No automatic reconciliation is installed. +Rollback after restart intentionally remains unavailable until an owner supplies +independently authenticated durable creation provenance and reviews recovery. + +The source tests execute actual maintained helper CAS/readback against synthetic +Kubernetes objects. Three checks exercise actual Warden admission/run/rollback +and uncertainty transitions with synthetic owner trust. Those checks explicitly +override Warden's stale producer source pins inside the test only, selecting the +current local source bytes. This is a compatibility fixture, not acknowledgment, +provenance, normal native admission or deployment acceptance. + +Remaining work belongs in live work records before T12 closure: + +- Warden owns review and refresh of BrowserInputs producer source pins, global + evidence admission and exact native evidence/session capabilities. Its current + pins predate NetKingdom 015694ee and refuse current producer bytes. +- NetKingdom owns separately reviewed descriptor/type pin selection, the env + binding/config/restart steps, and group ensure/revoke provenance. Current group + ensure does not report newly added memberships; current revoke can remove + memberships that predate the case. Wrapping these as reversible creation would + be unsound. Preserve them until a reviewed created-only contract exists. +- Platform owns structural/mint/broker and secret delivery evidence; Apps owns + actual consumer login and group enforcement. No assent is supplied here. +- Attended use still requires existing owner authorization, current caller + identity, native source/config/image pins, native receipts and cleanup proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py new file mode 100644 index 00000000..bfbf21e3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py @@ -0,0 +1,285 @@ +"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" +from dataclasses import asdict, replace +import copy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from warden.case_run import Invocation, digest +from warden.case_receipt_state import Outcome, Pins, Refused +import yaml + +ROOT = Path(__file__).parent +spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') +a = importlib.util.module_from_spec(spec) +spec.loader.exec_module(a) +spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class RegistrationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.binary = Path(self.temp.name) / 'keycape' + self.binary.write_bytes(b'synthetic checker binary') + self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', + 'parameters': f.FORGEJO_PARAMS}}).encode() + desc = a.descriptor() + self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), + digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) + self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', pins) + self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') + self.secret, self.raw = f.fixture() + self.effects = [] + self.evidence = {} + self.revocations = [] + self.kw = dict(admitted=self.admitted, case_source=self.case, + expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', + check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), + publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, + revoke_session=self.revoke) + self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) + self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) + self.check_patch.start() + self.kube_patch.start() + self.addCleanup(self.check_patch.stop) + self.addCleanup(self.kube_patch.stop) + self.addCleanup(self.temp.cleanup) + + def publish(self, payload): + ref = 'owner-evidence:attempt-' + str(len(self.evidence)) + self.evidence[ref] = payload + return ref + + def revoke(self, inv): + self.revocations.append(inv) + return 'session-revoked:' + inv.operation + ':' + inv.attempt_id + + def kube(self, args, payload=None): + if args[:3] == ['get', 'namespace', 'kube-system']: + return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} + if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + return copy.deepcopy(self.secret) + if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: + return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', + 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} + if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: + self.assertEqual(payload[:2], [ + {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, + {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) + self.effects.append(copy.deepcopy(payload)) + self.secret['data']['config.yaml'] = payload[2]['value'] + self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) + return copy.deepcopy(self.secret) + raise AssertionError('unexpected tooling operation') + + def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): + adapter = a.build(**self.kw) + original = copy.deepcopy(self.secret['data']) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.SUCCESS) + self.assertEqual(result.created, ('keycape-client',)) + self.assertTrue(adapter.verify(self.inv, result)) + undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) + self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) + self.assertEqual(self.secret['data'], original) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + for payload in self.evidence.values(): + self.assertNotIn(b'synthetic-marker', payload) + self.assertNotIn(b'config.yaml', payload) + self.assertNotIn(b'signing-key', payload) + + def test_identical_client_is_no_change_and_cannot_be_removed(self): + self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.NO_CHANGE) + self.assertEqual(result.created, ()) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) + self.assertFalse(self.effects) + + def test_existing_drift_refused_and_session_revoked(self): + self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) + with self.assertRaises(Refused): + a.build(**self.kw).invoke(self.inv) + self.assertFalse(self.effects) + self.assertEqual(self.revocations, [self.inv]) + + def test_invocation_rebinding_refused_before_tooling(self): + for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, + {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, + {'pins': replace(self.inv.pins, case=digest(b'other'))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**self.kw).invoke(replace(self.inv, **changes)) + self.assertFalse(self.effects) + + def test_initial_case_descriptor_and_target_pins_are_required(self): + for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, + {'admitted': replace(self.admitted, actor='agent')}, + {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**dict(self.kw, **changes)) + self.assertFalse(self.effects) + + def test_binary_or_source_drift_refused_before_tooling(self): + adapter = a.build(**self.kw) + self.binary.write_bytes(b'changed') + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.binary.write_bytes(b'synthetic checker binary') + with patch.object(a, 'source_digest', return_value=digest(b'changed')): + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_stale_revision_refuses_mutation(self): + adapter = a.build(**self.kw) + self.secret['metadata']['resourceVersion'] = '8' + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_rollback_requires_exact_provenance_and_postcreation_revision(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') + for created in ((), ('other',), ('keycape-client', 'other')): + with self.subTest(created=created), self.assertRaises(Refused): + adapter.rollback(rollback, created) + self.secret['metadata']['resourceVersion'] = '9' + with self.assertRaises(Refused): + adapter.rollback(rollback, result.created) + self.assertEqual(len(self.effects), 1) + + def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) + self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) + self.evidence[result.evidence] = b'forged' + self.assertFalse(adapter.verify(self.inv, result)) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertEqual(len(self.effects), 1) + + def test_process_loss_does_not_reconstruct_created_authority(self): + result = a.build(**self.kw).invoke(self.inv) + fresh = a.build(**self.kw) + with self.assertRaises(Refused): + fresh.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertIsNone(fresh.reconcile) + self.assertEqual(len(self.effects), 1) + + def test_failed_readback_is_uncertain_and_never_provides_creation(self): + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + adapter = a.build(**self.kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertEqual(len(self.effects), 1) + self.assertFalse(self.evidence) + self.assertEqual(self.revocations, [self.inv]) + + def test_revocation_or_publication_failure_cannot_produce_verified_success(self): + for capability in ('publish', 'revoke_session'): + self.secret, _ = f.fixture() + with self.subTest(capability=capability): + kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) + adapter = a.build(**kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertFalse(self.evidence) + + def kernel_fixture(self): + # Fixture-only proposed type/case; canonical files are never edited. + # Warden's current producer pins predate 015694ee. The explicit override + # below proves compatibility, not an owner pin update or native admission. + import hashlib + import warden.case_inputs as inputs + from warden.case_run import Executor, Journal + root = ROOT.parents[2] + case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) + case['spec']['status'] = 'materialized' + case_source = yaml.safe_dump(case).encode() + typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) + typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} + type_source = yaml.safe_dump(typ).encode() + catalog = b'fixture-only-catalog' + sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() + pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), + digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) + invocation = replace(self.admitted, pins=pins) + adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, + case_acceptance='fixture-acceptance')) + def fixture_verifier(purpose, source, evidence): + if purpose.startswith('session-revocation:'): + _, lane, operation, attempt = purpose.split(':') + return source == digest(case_source) and lane == invocation.lane and evidence == ( + 'session-revoked:' + operation + ':' + attempt) + return evidence in {'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'} + executor = Executor(Journal(Path(self.temp.name) / 'private'), + {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) + revised_pins = {rel: hashlib.sha256((root / rel).read_bytes()).hexdigest() + for rel in inputs.SOURCE_PINS} + with patch.object(inputs, 'SOURCE_PINS', revised_pins): + admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, + principal='founder', actor='human', lane=invocation.lane, + acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', + descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') + self.assertEqual(admitted[1], invocation) + return executor, admitted + + def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): + executor, admitted = self.kernel_fixture() + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.SUCCESS) + self.assertEqual(result.last.created, ('keycape-client',)) + undone = executor.rollback(*admitted) + self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + + def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): + executor, admitted = self.kernel_fixture() + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) + self.assertFalse(result.last.created) + with self.assertRaises(Refused): + executor.run(*admitted, retry=True) + with self.assertRaises(Refused): + executor.rollback(*admitted) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 1) + + def test_kernel_external_revision_change_retains_uncertain_rollback(self): + executor, admitted = self.kernel_fixture() + self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) + self.secret['metadata']['resourceVersion'] = '9' + self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/proposed-descriptor.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/proposed-descriptor.json new file mode 100644 index 00000000..c4dbe764 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/proposed-descriptor.json @@ -0,0 +1 @@ +{"actor":"human","adapter":"net-kingdom/keycape-registration@v1","adapter_source":"sha256:5a920f62400a09a0e597c7895e275dae2876cff6737aae92963eff3ae1fb8cec","allowed_created":["keycape-client"],"effect":"change","lane":"key-cape-oidc-login","owner":"net-kingdom","retry_safe":false,"revocation_scopes":["parent-session"],"step":"register-client","target_parameter":"client_id","version":"warden.case.execution.v1"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/tests.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/tests.txt new file mode 100644 index 00000000..2c52aa5c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/rejected-r1/tests.txt @@ -0,0 +1,40 @@ +test_access_groups_require_groups_scope (test_keycape_client.ClientRecordTests.test_access_groups_require_groups_scope) ... ok +test_client_from_case_requires_the_type (test_keycape_client.ClientRecordTests.test_client_from_case_requires_the_type) ... ok +test_confidential_record_binds_env_secret_and_pins_mfa (test_keycape_client.ClientRecordTests.test_confidential_record_binds_env_secret_and_pins_mfa) ... ok +test_invalid_params_refused (test_keycape_client.ClientRecordTests.test_invalid_params_refused) ... ok +test_public_record_has_no_secret (test_keycape_client.ClientRecordTests.test_public_record_has_no_secret) ... ok +test_redirects_keycape_would_refuse_at_startup_are_refused (test_keycape_client.ClientRecordTests.test_redirects_keycape_would_refuse_at_startup_are_refused) ... ok +test_vergabe_case_reproduces_the_hand_built_registration (test_keycape_client.ClientRecordTests.test_vergabe_case_reproduces_the_hand_built_registration) ... ok +test_add_appends_only_this_client_and_preserves_bytes (test_keycape_client.MergeTests.test_add_appends_only_this_client_and_preserves_bytes) ... ok +test_identical_is_noop_and_drift_refused (test_keycape_client.MergeTests.test_identical_is_noop_and_drift_refused) ... ok +test_removal_removes_exactly_this_client (test_keycape_client.MergeTests.test_removal_removes_exactly_this_client) ... ok +test_shared_secret_ref_and_duplicates_refused (test_keycape_client.MergeTests.test_shared_secret_ref_and_duplicates_refused) ... ok +test_apply_needs_case_acceptance (test_keycape_client.RunTests.test_apply_needs_case_acceptance) ... ok +test_apply_needs_env_binding_for_confidential_clients (test_keycape_client.RunTests.test_apply_needs_env_binding_for_confidential_clients) ... ok +test_apply_needs_keycape_check_config (test_keycape_client.RunTests.test_apply_needs_keycape_check_config) ... ok +test_apply_patches_reads_back_and_records_acceptance (test_keycape_client.RunTests.test_apply_patches_reads_back_and_records_acceptance) ... ok +test_apply_refuses_a_changed_revision (test_keycape_client.RunTests.test_apply_refuses_a_changed_revision) ... ok +test_apply_refuses_a_registry_keycape_would_refuse (test_keycape_client.RunTests.test_apply_refuses_a_registry_keycape_would_refuse) ... ok +test_inspect_changes_nothing_and_emits_no_values (test_keycape_client.RunTests.test_inspect_changes_nothing_and_emits_no_values) ... ok +test_inspect_reports_live_and_proposed_checks_with_deployment_env (test_keycape_client.RunTests.test_inspect_reports_live_and_proposed_checks_with_deployment_env) ... ok +test_main_contains_failures (test_keycape_client.RunTests.test_main_contains_failures) ... ok +test_actual_add_and_created_only_rollback_preserve_original_bytes (test_keycape_registration_adapter.RegistrationTests.test_actual_add_and_created_only_rollback_preserve_original_bytes) ... ok +test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust (test_keycape_registration_adapter.RegistrationTests.test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust) ... ok +test_binary_or_source_drift_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_binary_or_source_drift_refused_before_tooling) ... ok +test_existing_drift_refused_and_session_revoked (test_keycape_registration_adapter.RegistrationTests.test_existing_drift_refused_and_session_revoked) ... ok +test_failed_readback_is_uncertain_and_never_provides_creation (test_keycape_registration_adapter.RegistrationTests.test_failed_readback_is_uncertain_and_never_provides_creation) ... ok +test_identical_client_is_no_change_and_cannot_be_removed (test_keycape_registration_adapter.RegistrationTests.test_identical_client_is_no_change_and_cannot_be_removed) ... ok +test_initial_case_descriptor_and_target_pins_are_required (test_keycape_registration_adapter.RegistrationTests.test_initial_case_descriptor_and_target_pins_are_required) ... ok +test_invocation_rebinding_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_invocation_rebinding_refused_before_tooling) ... ok +test_kernel_external_revision_change_retains_uncertain_rollback (test_keycape_registration_adapter.RegistrationTests.test_kernel_external_revision_change_retains_uncertain_rollback) ... ok +test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo (test_keycape_registration_adapter.RegistrationTests.test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo) ... ok +test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo (test_keycape_registration_adapter.RegistrationTests.test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo) ... ok +test_process_loss_does_not_reconstruct_created_authority (test_keycape_registration_adapter.RegistrationTests.test_process_loss_does_not_reconstruct_created_authority) ... ok +test_revocation_or_publication_failure_cannot_produce_verified_success (test_keycape_registration_adapter.RegistrationTests.test_revocation_or_publication_failure_cannot_produce_verified_success) ... ok +test_rollback_requires_exact_provenance_and_postcreation_revision (test_keycape_registration_adapter.RegistrationTests.test_rollback_requires_exact_provenance_and_postcreation_revision) ... ok +test_stale_revision_refuses_mutation (test_keycape_registration_adapter.RegistrationTests.test_stale_revision_refuses_mutation) ... ok + +---------------------------------------------------------------------- +Ran 35 tests in 1.267s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/README.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/README.md new file mode 100644 index 00000000..832adda0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/README.md @@ -0,0 +1,47 @@ +# NetKingdom registration adapter source candidate R2 + +Source-only candidate awaiting independent root review. No canonical owner file, +type/case bytes or status changed; no credential or live action was used. +Exercise status: unexercised. + +The patch adds a fixed attended registration adapter around actual maintained +`keycape-client.run`, its contract notes and 16 new fixture checks. The 20 +existing helper checks also pass. Three new checks exercise actual Warden +admission/dispatch/rollback and uncertain transitions with synthetic owner trust +and explicit test-only producer-pin overrides. No replacement of adapter +callables after admission is used: the proposed Invocation is built first, and +normal admission independently derives the same full binding. + +R1's root review found an unbound config-checker executable: the caller could +supply another binary and its matching hash under the same descriptor. That +rejected revision remains in `rejected-r1/`. R2 includes the exact selected +checker hash in the adapter source-composition pin and descriptor. Substitution +under the original descriptor is now refused before tooling. The descriptor +retained here uses a synthetic checker digest only; native executable selection +and owner acknowledgment remain outstanding. + +Reproduce from current pinned owner dependencies: + +```bash +python3 docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/verify_candidate.py +``` + +`implementation.patch` adds three files. `postimages/` holds the exact proposed +bytes. `manifest.json` binds all source/fixture hashes and the observed owner +state at packaging; this is not a reconstructed session-start dirty-state +snapshot. `tests.txt` retains the 36 passing checks. Warden/source owner heads +and current canonical changes remain outside this patch. + +The owner contract in `postimages/sso-mfa/k8s/keycape/registration-adapter.md` +records required native callbacks and remaining scope. In particular: Warden's +current producer pins refuse the new NetKingdom validator bytes; group ensure +has no created-membership provenance; group revoke can remove a pre-existing +membership; env/config/restart adapters remain; durable post-restart creation +reconciliation is absent. Platform/Apps native acceptance remains separately +owned. None of these gaps is supplied by the fixture proof. + +Suggested owner workplan refinement on reviewed integration: retain T12 for +remaining attended steps/recovery and extract the independently reviewed +registration source deliverable into a new completed task with this evidence +and publication hash. Do not close T12 or T06, do not pin this descriptor into +the acknowledged type, and do not claim native registration acceptance. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/implementation.patch new file mode 100644 index 00000000..dbbb7207 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/implementation.patch @@ -0,0 +1,541 @@ +--- /dev/null ++++ b/sso-mfa/k8s/keycape/keycape-registration-adapter.py +@@ -0,0 +1,180 @@ ++"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. ++ ++No CLI, credentials, evidence trust root or live registration is installed here. ++Owner publication/authentication and parent-session revocation are mandatory ++injected capabilities. The kernel must independently admit both evidence kinds. ++""" ++from __future__ import annotations ++ ++import argparse ++from dataclasses import asdict, replace ++import json ++import importlib.util ++from pathlib import Path ++ ++import yaml ++ ++from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER, DIGEST ++from warden.case_receipt_state import Outcome, Refused ++ ++ROOT = Path(__file__).resolve().parent ++SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', ++ 'portal-client-rollout.py', 'openbao-client-config.py', ++ 'register-user-engine-portal.py') ++REFERENCE = 'net-kingdom/keycape-registration@v1' ++ ++# Importing the maintained helper performs no tooling operation. ++_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') ++helper = importlib.util.module_from_spec(_spec) ++_spec.loader.exec_module(helper) ++ ++ ++def source_digest(check_config_digest): ++ """Pin adapter plus the complete local helper import closure, in filename order.""" ++ if not isinstance(check_config_digest, str) or not DIGEST.fullmatch(check_config_digest): ++ raise Refused('exact reviewed checker digest required') ++ composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}, ++ 'check_config_binary': check_config_digest} ++ return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode()) ++ ++ ++def descriptor(check_config_digest): ++ return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', ++ REFERENCE, source_digest(check_config_digest), 'human', 'key-cape-oidc-login', ++ 'client_id', 'change', ('keycape-client',), False, ++ ('parent-session',)) ++ ++ ++def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, ++ expected_resource_version: str, case_acceptance: str, ++ check_config: Path, check_config_digest: str, publish, authenticate, ++ revoke_session): ++ """Bind one admitted case and observed revision; return a closed kernel Adapter. ++ ++ publish(bytes)->identifier durably retains only the closed metadata payload. ++ authenticate(identifier, bytes)->True must verify independent owner custody. ++ revoke_session(invocation)->identifier must revoke this attempt's parent ++ session; Warden independently authenticates it using its own evidence verifier. ++ These callbacks are capabilities, never command strings or case parameters. ++ ++ Only a verified creation in this process may be rolled back. Process loss, ++ write/readback ambiguity, publication/revocation failure and source drift ++ require owner reconciliation; none are converted to retry-safe failures. ++ """ ++ reviewed_source = source_digest(check_config_digest) ++ desc = descriptor(check_config_digest) ++ if type(admitted) is not Invocation or not all( ++ isinstance(value, str) and IDENTIFIER.fullmatch(value) ++ for value in (admitted.step, admitted.owner, admitted.target, ++ admitted.actor, admitted.principal, admitted.lane)): ++ raise Refused('bounded invocation required') ++ if desc.adapter_source != reviewed_source: ++ raise Refused('owner source changed during binding') ++ if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( ++ desc.step, desc.owner, desc.actor, desc.lane): ++ raise Refused('registration invocation boundary differs') ++ if admitted.operation != 'admission' or admitted.attempt_id: ++ raise Refused('expected admission invocation') ++ if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: ++ raise Refused('case source pin differs') ++ # Pins.adapter is the descriptor digest, not the owner source digest. ++ descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ if admitted.pins.adapter != digest(descriptor_bytes): ++ raise Refused('descriptor pin differs') ++ if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in ++ (expected_uid, expected_resource_version, case_acceptance)): ++ raise Refused('bounded attended binding required') ++ if digest(case_acceptance.encode()) != admitted.pins.acceptance: ++ raise Refused('acceptance evidence binding differs') ++ if not all(callable(x) for x in (publish, authenticate, revoke_session)): ++ raise Refused('owner evidence and revocation capabilities required') ++ with contained_output(): ++ case = yaml.safe_load(case_source) ++ client = helper.client_from_case(case) ++ if admitted.target != client['clientId']: ++ raise Refused('case target differs') ++ check_config = Path(check_config) ++ records = {} ++ creation = None ++ ++ def binding(inv, operation): ++ if (replace(inv, attempt_id='', operation='admission') != admitted ++ or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): ++ raise Refused('attempt binding differs') ++ if source_digest(check_config_digest) != reviewed_source: ++ raise Refused('owner source changed') ++ if digest(check_config.read_bytes()) != check_config_digest: ++ raise Refused('check-config binary changed') ++ ++ def execute(inv, operation, created=()): ++ nonlocal creation ++ try: ++ with contained_output(): ++ binding(inv, 'run' if operation == 'add' else 'rollback') ++ uid, revision = expected_uid, expected_resource_version ++ if operation == 'remove': ++ if (created != ('keycape-client',) or creation is None ++ or not verify(*creation)): ++ raise Refused('no authenticated case-created client') ++ original_payload = json.loads(records[creation[1].evidence][1]) ++ uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] ++ args = argparse.Namespace(mode='apply', operation=operation, ++ expected_uid=uid, expected_resource_version=revision, ++ case_acceptance=case_acceptance, check_config=str(check_config)) ++ receipt = helper.run(args, client) ++ # The helper has completed CAS, whole-data readback and preservation. ++ # Never retain check-config diagnostics or arbitrary helper fields. ++ if not (receipt['schema'] == 'netkingdom.keycape-client.v1' ++ and receipt['target'] == 'sso/keycape-config' ++ and receipt['client_id'] == admitted.target ++ and receipt['mode'] == 'apply' and receipt['operation'] == operation ++ and receipt['values_emitted'] is False ++ and type(receipt['changed']) is bool ++ and receipt['unrelated_config_bytes_preserved'] is True ++ and receipt['other_secret_data_unchanged'] is True ++ and receipt['before'] == {'uid': uid, 'resource_version': revision} ++ and receipt['after']['uid'] == uid ++ and receipt['check_config']['proposed']['valid'] is True): ++ raise Refused('owner readback contract differs') ++ after = receipt['after'] ++ if (set(after) != {'uid', 'resource_version'} or ++ not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): ++ raise Refused('owner revision contract differs') ++ changed = receipt['changed'] ++ outcome = (Outcome.ROLLED_BACK if operation == 'remove' else ++ Outcome.SUCCESS if changed else Outcome.NO_CHANGE) ++ objects = ('keycape-client',) if operation == 'add' and changed else () ++ finally: ++ # Even helper failure must end the parent session. Failures propagate ++ # contained to the kernel and become uncertain, never safe to replay. ++ with contained_output(): ++ revocation = revoke_session(inv) ++ if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): ++ raise Refused('session revocation evidence missing') ++ payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', ++ 'invocation': asdict(inv), 'source': reviewed_source, ++ 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], ++ 'after': after, 'session_revocation': revocation}, ++ sort_keys=True, separators=(',', ':')).encode() ++ with contained_output(): ++ evidence = publish(payload) ++ if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: ++ raise Refused('owner evidence identifier invalid or reused') ++ result = OwnerResult(outcome, evidence, objects, True, revocation, ++ attempt_id=inv.attempt_id, operation=inv.operation) ++ records[evidence] = (inv, payload, result) ++ if operation == 'add' and changed: ++ creation = (inv, result) ++ elif operation == 'remove': ++ creation = None ++ return result ++ ++ def verify(inv, result): ++ record = records.get(result.evidence) if type(result) is OwnerResult else None ++ if record is None or record[0] != inv or record[2] != result: ++ return False ++ with contained_output(): ++ return authenticate(result.evidence, record[1]) is True ++ ++ return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), ++ verify, lambda inv, created: execute(inv, 'remove', created)) +--- /dev/null ++++ b/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py +@@ -0,0 +1,293 @@ ++"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" ++from dataclasses import asdict, replace ++import copy ++import importlib.util ++import json ++from pathlib import Path ++import tempfile ++import unittest ++from unittest.mock import patch ++ ++from warden.case_run import Invocation, digest ++from warden.case_receipt_state import Outcome, Pins, Refused ++import yaml ++ ++ROOT = Path(__file__).parent ++spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') ++a = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(a) ++spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') ++f = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(f) ++ ++ ++class RegistrationTests(unittest.TestCase): ++ def setUp(self): ++ self.temp = tempfile.TemporaryDirectory() ++ self.binary = Path(self.temp.name) / 'keycape' ++ self.binary.write_bytes(b'synthetic checker binary') ++ self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', ++ 'parameters': f.FORGEJO_PARAMS}}).encode() ++ desc = a.descriptor(digest(self.binary.read_bytes())) ++ self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), ++ digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) ++ self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', pins) ++ self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') ++ self.secret, self.raw = f.fixture() ++ self.effects = [] ++ self.evidence = {} ++ self.revocations = [] ++ self.kw = dict(admitted=self.admitted, case_source=self.case, ++ expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', ++ check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), ++ publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, ++ revoke_session=self.revoke) ++ self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) ++ self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) ++ self.check_patch.start() ++ self.kube_patch.start() ++ self.addCleanup(self.check_patch.stop) ++ self.addCleanup(self.kube_patch.stop) ++ self.addCleanup(self.temp.cleanup) ++ ++ def publish(self, payload): ++ ref = 'owner-evidence:attempt-' + str(len(self.evidence)) ++ self.evidence[ref] = payload ++ return ref ++ ++ def revoke(self, inv): ++ self.revocations.append(inv) ++ return 'session-revoked:' + inv.operation + ':' + inv.attempt_id ++ ++ def kube(self, args, payload=None): ++ if args[:3] == ['get', 'namespace', 'kube-system']: ++ return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} ++ if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ return copy.deepcopy(self.secret) ++ if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: ++ return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', ++ 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} ++ if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: ++ self.assertEqual(payload[:2], [ ++ {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, ++ {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) ++ self.effects.append(copy.deepcopy(payload)) ++ self.secret['data']['config.yaml'] = payload[2]['value'] ++ self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) ++ return copy.deepcopy(self.secret) ++ raise AssertionError('unexpected tooling operation') ++ ++ def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): ++ adapter = a.build(**self.kw) ++ original = copy.deepcopy(self.secret['data']) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.created, ('keycape-client',)) ++ self.assertTrue(adapter.verify(self.inv, result)) ++ undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) ++ self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(self.secret['data'], original) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ for payload in self.evidence.values(): ++ self.assertNotIn(b'synthetic-marker', payload) ++ self.assertNotIn(b'config.yaml', payload) ++ self.assertNotIn(b'signing-key', payload) ++ ++ def test_identical_client_is_no_change_and_cannot_be_removed(self): ++ self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.NO_CHANGE) ++ self.assertEqual(result.created, ()) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) ++ self.assertFalse(self.effects) ++ ++ def test_existing_drift_refused_and_session_revoked(self): ++ self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) ++ with self.assertRaises(Refused): ++ a.build(**self.kw).invoke(self.inv) ++ self.assertFalse(self.effects) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_invocation_rebinding_refused_before_tooling(self): ++ for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, ++ {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, ++ {'pins': replace(self.inv.pins, case=digest(b'other'))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**self.kw).invoke(replace(self.inv, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_initial_case_descriptor_and_target_pins_are_required(self): ++ for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, ++ {'admitted': replace(self.admitted, actor='agent')}, ++ {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**dict(self.kw, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_binary_or_source_drift_refused_before_tooling(self): ++ adapter = a.build(**self.kw) ++ self.binary.write_bytes(b'changed') ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.binary.write_bytes(b'synthetic checker binary') ++ with patch.object(a, 'source_digest', return_value=digest(b'changed')): ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self): ++ substitute = Path(self.temp.name) / 'other-checker' ++ substitute.write_bytes(b'caller-selected executable') ++ with self.assertRaisesRegex(Refused, 'descriptor pin differs'): ++ a.build(**dict(self.kw, check_config=substitute, ++ check_config_digest=digest(substitute.read_bytes()))) ++ self.assertFalse(self.effects) ++ ++ def test_stale_revision_refuses_mutation(self): ++ adapter = a.build(**self.kw) ++ self.secret['metadata']['resourceVersion'] = '8' ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_rollback_requires_exact_provenance_and_postcreation_revision(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') ++ for created in ((), ('other',), ('keycape-client', 'other')): ++ with self.subTest(created=created), self.assertRaises(Refused): ++ adapter.rollback(rollback, created) ++ self.secret['metadata']['resourceVersion'] = '9' ++ with self.assertRaises(Refused): ++ adapter.rollback(rollback, result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) ++ self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) ++ self.evidence[result.evidence] = b'forged' ++ self.assertFalse(adapter.verify(self.inv, result)) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_process_loss_does_not_reconstruct_created_authority(self): ++ result = a.build(**self.kw).invoke(self.inv) ++ fresh = a.build(**self.kw) ++ with self.assertRaises(Refused): ++ fresh.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertIsNone(fresh.reconcile) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_failed_readback_is_uncertain_and_never_provides_creation(self): ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ adapter = a.build(**self.kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertEqual(len(self.effects), 1) ++ self.assertFalse(self.evidence) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_revocation_or_publication_failure_cannot_produce_verified_success(self): ++ for capability in ('publish', 'revoke_session'): ++ self.secret, _ = f.fixture() ++ with self.subTest(capability=capability): ++ kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) ++ adapter = a.build(**kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.evidence) ++ ++ def kernel_fixture(self): ++ # Fixture-only proposed type/case; canonical files are never edited. ++ # Warden's current producer pins predate 015694ee. The explicit override ++ # below proves compatibility, not an owner pin update or native admission. ++ import hashlib ++ import warden.case_inputs as inputs ++ from warden.case_run import Executor, Journal ++ root = ROOT.parents[2] ++ case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) ++ case['spec']['status'] = 'materialized' ++ case_source = yaml.safe_dump(case).encode() ++ typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) ++ typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} ++ type_source = yaml.safe_dump(typ).encode() ++ catalog = b'fixture-only-catalog' ++ sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() ++ pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), ++ digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) ++ invocation = replace(self.admitted, pins=pins) ++ adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, ++ case_acceptance='fixture-acceptance')) ++ def fixture_verifier(purpose, source, evidence): ++ if purpose.startswith('session-revocation:'): ++ _, lane, operation, attempt = purpose.split(':') ++ return source == digest(case_source) and lane == invocation.lane and evidence == ( ++ 'session-revoked:' + operation + ':' + attempt) ++ return evidence in {'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'} ++ executor = Executor(Journal(Path(self.temp.name) / 'private'), ++ {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) ++ revised_pins = {rel: hashlib.sha256((root / rel).read_bytes()).hexdigest() ++ for rel in inputs.SOURCE_PINS} ++ with patch.object(inputs, 'SOURCE_PINS', revised_pins): ++ admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, ++ principal='founder', actor='human', lane=invocation.lane, ++ acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', ++ descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') ++ self.assertEqual(admitted[1], invocation) ++ return executor, admitted ++ ++ def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): ++ executor, admitted = self.kernel_fixture() ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.last.created, ('keycape-client',)) ++ undone = executor.rollback(*admitted) ++ self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ ++ def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): ++ executor, admitted = self.kernel_fixture() ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) ++ self.assertFalse(result.last.created) ++ with self.assertRaises(Refused): ++ executor.run(*admitted, retry=True) ++ with self.assertRaises(Refused): ++ executor.rollback(*admitted) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 1) ++ ++ def test_kernel_external_revision_change_retains_uncertain_rollback(self): ++ executor, admitted = self.kernel_fixture() ++ self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) ++ self.secret['metadata']['resourceVersion'] = '9' ++ self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 2) ++ ++ ++if __name__ == '__main__': ++ unittest.main() +--- /dev/null ++++ b/sso-mfa/k8s/keycape/registration-adapter.md +@@ -0,0 +1,59 @@ ++# Attended registration adapter candidate ++ ++Exercise status: unexercised ++ ++This is a source-only, injection-only adapter for the existing Warden closed ++kernel. It installs no command, runtime dispatcher, credential provider, evidence ++trust root or new service. NetKingdom type/case status and bytes remain unchanged. ++The generated descriptor is a proposal; it is not owner acknowledgment. ++ ++`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply ++mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ ++catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter ++and its complete local helper import closure together with the exact reviewed ++`check-config` executable digest. Changing that digest changes the descriptor ++source pin and descriptor bytes, requiring independently reviewed admission. ++The fixed executable is rehashed before each attempt. The caller constructs the ++proposed invocation from exact source inputs, installs the fixed adapter once, ++and calls normal `Executor.admit`; independently derived admission must match. ++No callable replacement after admission is required or supported by this recipe. ++ ++The operation uses existing cluster/issuer checks, byte-preserving merge, ++Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS ++and whole-data readback. Only closed metadata is passed to the injected durable ++owner publisher. The owner authenticator must independently authenticate those ++exact bytes. The session-revocation capability must end the same attempt's ++parent session; Warden separately verifies its exact revocation evidence. ++These capabilities are owner-selected callables, never case-supplied commands. ++No implementation of these native capabilities is supplied or implied here. ++ ++An identical existing client returns no change and creates no rollback authority. ++Removal requires this process's independently authenticated successful creation ++receipt, exact created-object label, unchanged postcreation UID/resourceVersion, ++and the original exact client record. External drift, a lost readback, receipt ++publication/revocation failure or process loss requires owner reconciliation. ++No uncertainty is labeled retry-safe. No automatic reconciliation is installed. ++Rollback after restart intentionally remains unavailable until an owner supplies ++independently authenticated durable creation provenance and reviews recovery. ++ ++The source tests execute actual maintained helper CAS/readback against synthetic ++Kubernetes objects. Three checks exercise actual Warden admission/run/rollback ++and uncertainty transitions with synthetic owner trust. Those checks explicitly ++override Warden's stale producer source pins inside the test only, selecting the ++current local source bytes. This is a compatibility fixture, not acknowledgment, ++provenance, normal native admission or deployment acceptance. ++ ++Remaining work belongs in live work records before T12 closure: ++ ++- Warden owns review and refresh of BrowserInputs producer source pins, global ++ evidence admission and exact native evidence/session capabilities. Its current ++ pins predate NetKingdom 015694ee and refuse current producer bytes. ++- NetKingdom owns separately reviewed descriptor/type pin selection, the env ++ binding/config/restart steps, and group ensure/revoke provenance. Current group ++ ensure does not report newly added memberships; current revoke can remove ++ memberships that predate the case. Wrapping these as reversible creation would ++ be unsound. Preserve them until a reviewed created-only contract exists. ++- Platform owns structural/mint/broker and secret delivery evidence; Apps owns ++ actual consumer login and group enforcement. No assent is supplied here. ++- Attended use still requires existing owner authorization, current caller ++ identity, native source/config/image pins, native receipts and cleanup proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/manifest.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/manifest.json new file mode 100644 index 00000000..13677637 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/manifest.json @@ -0,0 +1,58 @@ +{ + "schema": "netkingdom.registration-adapter-source-candidate.v1", + "date": "2026-10-16", + "owner": "net-kingdom", + "candidate": "/tmp/nk-browser-adapter-next", + "owner_revision_at_packaging": "015694ee414c802dc2ef1fa0120dfed610086e69", + "owner_status_at_packaging": "MM .custodian-brief.md\n M WORK-RECORDS.md\n M workplans/NK-WP-0048-security-case-types.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md\n", + "owner_diff_digest_at_packaging": "sha256:7ed1cfaf24a0b154180c93c7491ae7274e508d8f2258c6e741bf69f3768d74eb", + "warden_revision": "d79f06595545f2b66631ffbcee14a382e96bc293", + "adapter_source": "sha256:284e1d70b56461c6190ea634dea63d1b55890d657df8379f2459dfc882260142", + "postimages": { + "sso-mfa/k8s/keycape/keycape-registration-adapter.py": "sha256:8a6bd0ea111d67a0df89c7e3cd968e7bf81d29c2f2a70adf08eb747e1d1a5bdc", + "sso-mfa/k8s/keycape/test_keycape_registration_adapter.py": "sha256:a71517c753c314301c1d51bfe8c4ff65597a0bd663dd8db38a4d4ce1026c3c5c", + "sso-mfa/k8s/keycape/registration-adapter.md": "sha256:eb154660abd8ea766d81108ae2ad948fe83da1a235071d8094ad60daea4853cd" + }, + "helper_dependency_hashes": { + "keycape-client.py": "sha256:e31ee2883fad4f93635700608ad98e05984c9e0453515dd7710c897ca856b8c8", + "portal-client-rollout.py": "sha256:158e1d36eb881cdd0a2eda91b472adea5e8fde5061e00d26dda368580aeb871f", + "openbao-client-config.py": "sha256:1b908b4c36134212d78f9fbe7a2a201eb3ced06d31bb1dcea4a59d80689d84b1", + "register-user-engine-portal.py": "sha256:67173edc44f5457e7b6a3e0fb2321b398159cfce5dea8ead01eabf835fb38d76" + }, + "tests": { + "new": 16, + "existing": 20, + "total": 36, + "passed": true, + "native": false, + "producer_pin_override": "explicit synthetic integration fixture only" + }, + "canonical_owner_writes": false, + "published": false, + "review_status": "r1 rejected: unbound checker; r2 awaiting independent root review", + "native_acceptance": false, + "revision": "r2", + "fixture_checker_digest": "sha256:bb8a8e3d1ff5176c3f7da3144b55fea2effc9e4e29790146e2ae58d7f8363fb0", + "descriptor_scope": "synthetic fixture only; no native checker artifact selected or admitted", + "fixture_dependencies": { + "sso-mfa/k8s/keycape/keycape-client.py": "sha256:e31ee2883fad4f93635700608ad98e05984c9e0453515dd7710c897ca856b8c8", + "sso-mfa/k8s/keycape/portal-client-rollout.py": "sha256:158e1d36eb881cdd0a2eda91b472adea5e8fde5061e00d26dda368580aeb871f", + "sso-mfa/k8s/keycape/openbao-client-config.py": "sha256:1b908b4c36134212d78f9fbe7a2a201eb3ced06d31bb1dcea4a59d80689d84b1", + "sso-mfa/k8s/keycape/register-user-engine-portal.py": "sha256:67173edc44f5457e7b6a3e0fb2321b398159cfce5dea8ead01eabf835fb38d76", + "sso-mfa/k8s/keycape/test_keycape_client.py": "sha256:9572044679feaedfe732e15a2b6f8ef6b41a8fe2e4430257f309a3e83e97736c", + "sso-mfa/k8s/keycape/vergabe-client-rollout.py": "sha256:8cee6cc5ff3e3e8617304055263d04b393d0e005c34d9af12cb7e8bc685bac1a", + "tools/security-case-types/security_case_evidence.py": "sha256:da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75", + "tools/security-case-types/security_case_validator.py": "sha256:1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b", + "tools/security-scenario-composer/security_scenario_composer.py": "sha256:9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", + "tools/playbook-capability-contract/playbook_contract_validator.py": "sha256:2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", + "security-case-types/oidc-browser-relying-party/type.yaml": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "security-cases/forgejo-netkingdom-oidc.yaml": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d" + }, + "packet_hashes": { + "implementation.patch": "sha256:8591db0f74a4db63d468cc3c6e552bbb0c9e6f562aaa50dd60449fcf66831b9c", + "proposed-descriptor.json": "sha256:53bc48959ae9fcb7f2927f34558e55275dcdb512efba116f75d54eca36a375c4", + "README.md": "sha256:90c79aa3bfc33be8e8ce13d028b4246d7712f853bb93a80664a8bdf7ce7d1b5c", + "verify_candidate.py": "sha256:00c7b20d049c579b393539d343dbcb0f0dd560de16d5a43593888f32c8fc18ac", + "tests.txt": "sha256:ca3b58cf03b87c2d7b27d3a94a9c22829f2dea1ffa8be0c2ec3959acd14a3d94" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py new file mode 100644 index 00000000..995dd263 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py @@ -0,0 +1,180 @@ +"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. + +No CLI, credentials, evidence trust root or live registration is installed here. +Owner publication/authentication and parent-session revocation are mandatory +injected capabilities. The kernel must independently admit both evidence kinds. +""" +from __future__ import annotations + +import argparse +from dataclasses import asdict, replace +import json +import importlib.util +from pathlib import Path + +import yaml + +from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER, DIGEST +from warden.case_receipt_state import Outcome, Refused + +ROOT = Path(__file__).resolve().parent +SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', + 'portal-client-rollout.py', 'openbao-client-config.py', + 'register-user-engine-portal.py') +REFERENCE = 'net-kingdom/keycape-registration@v1' + +# Importing the maintained helper performs no tooling operation. +_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') +helper = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(helper) + + +def source_digest(check_config_digest): + """Pin adapter plus the complete local helper import closure, in filename order.""" + if not isinstance(check_config_digest, str) or not DIGEST.fullmatch(check_config_digest): + raise Refused('exact reviewed checker digest required') + composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}, + 'check_config_binary': check_config_digest} + return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode()) + + +def descriptor(check_config_digest): + return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', + REFERENCE, source_digest(check_config_digest), 'human', 'key-cape-oidc-login', + 'client_id', 'change', ('keycape-client',), False, + ('parent-session',)) + + +def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, + expected_resource_version: str, case_acceptance: str, + check_config: Path, check_config_digest: str, publish, authenticate, + revoke_session): + """Bind one admitted case and observed revision; return a closed kernel Adapter. + + publish(bytes)->identifier durably retains only the closed metadata payload. + authenticate(identifier, bytes)->True must verify independent owner custody. + revoke_session(invocation)->identifier must revoke this attempt's parent + session; Warden independently authenticates it using its own evidence verifier. + These callbacks are capabilities, never command strings or case parameters. + + Only a verified creation in this process may be rolled back. Process loss, + write/readback ambiguity, publication/revocation failure and source drift + require owner reconciliation; none are converted to retry-safe failures. + """ + reviewed_source = source_digest(check_config_digest) + desc = descriptor(check_config_digest) + if type(admitted) is not Invocation or not all( + isinstance(value, str) and IDENTIFIER.fullmatch(value) + for value in (admitted.step, admitted.owner, admitted.target, + admitted.actor, admitted.principal, admitted.lane)): + raise Refused('bounded invocation required') + if desc.adapter_source != reviewed_source: + raise Refused('owner source changed during binding') + if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( + desc.step, desc.owner, desc.actor, desc.lane): + raise Refused('registration invocation boundary differs') + if admitted.operation != 'admission' or admitted.attempt_id: + raise Refused('expected admission invocation') + if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: + raise Refused('case source pin differs') + # Pins.adapter is the descriptor digest, not the owner source digest. + descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + if admitted.pins.adapter != digest(descriptor_bytes): + raise Refused('descriptor pin differs') + if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in + (expected_uid, expected_resource_version, case_acceptance)): + raise Refused('bounded attended binding required') + if digest(case_acceptance.encode()) != admitted.pins.acceptance: + raise Refused('acceptance evidence binding differs') + if not all(callable(x) for x in (publish, authenticate, revoke_session)): + raise Refused('owner evidence and revocation capabilities required') + with contained_output(): + case = yaml.safe_load(case_source) + client = helper.client_from_case(case) + if admitted.target != client['clientId']: + raise Refused('case target differs') + check_config = Path(check_config) + records = {} + creation = None + + def binding(inv, operation): + if (replace(inv, attempt_id='', operation='admission') != admitted + or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): + raise Refused('attempt binding differs') + if source_digest(check_config_digest) != reviewed_source: + raise Refused('owner source changed') + if digest(check_config.read_bytes()) != check_config_digest: + raise Refused('check-config binary changed') + + def execute(inv, operation, created=()): + nonlocal creation + try: + with contained_output(): + binding(inv, 'run' if operation == 'add' else 'rollback') + uid, revision = expected_uid, expected_resource_version + if operation == 'remove': + if (created != ('keycape-client',) or creation is None + or not verify(*creation)): + raise Refused('no authenticated case-created client') + original_payload = json.loads(records[creation[1].evidence][1]) + uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] + args = argparse.Namespace(mode='apply', operation=operation, + expected_uid=uid, expected_resource_version=revision, + case_acceptance=case_acceptance, check_config=str(check_config)) + receipt = helper.run(args, client) + # The helper has completed CAS, whole-data readback and preservation. + # Never retain check-config diagnostics or arbitrary helper fields. + if not (receipt['schema'] == 'netkingdom.keycape-client.v1' + and receipt['target'] == 'sso/keycape-config' + and receipt['client_id'] == admitted.target + and receipt['mode'] == 'apply' and receipt['operation'] == operation + and receipt['values_emitted'] is False + and type(receipt['changed']) is bool + and receipt['unrelated_config_bytes_preserved'] is True + and receipt['other_secret_data_unchanged'] is True + and receipt['before'] == {'uid': uid, 'resource_version': revision} + and receipt['after']['uid'] == uid + and receipt['check_config']['proposed']['valid'] is True): + raise Refused('owner readback contract differs') + after = receipt['after'] + if (set(after) != {'uid', 'resource_version'} or + not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): + raise Refused('owner revision contract differs') + changed = receipt['changed'] + outcome = (Outcome.ROLLED_BACK if operation == 'remove' else + Outcome.SUCCESS if changed else Outcome.NO_CHANGE) + objects = ('keycape-client',) if operation == 'add' and changed else () + finally: + # Even helper failure must end the parent session. Failures propagate + # contained to the kernel and become uncertain, never safe to replay. + with contained_output(): + revocation = revoke_session(inv) + if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): + raise Refused('session revocation evidence missing') + payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', + 'invocation': asdict(inv), 'source': reviewed_source, + 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], + 'after': after, 'session_revocation': revocation}, + sort_keys=True, separators=(',', ':')).encode() + with contained_output(): + evidence = publish(payload) + if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: + raise Refused('owner evidence identifier invalid or reused') + result = OwnerResult(outcome, evidence, objects, True, revocation, + attempt_id=inv.attempt_id, operation=inv.operation) + records[evidence] = (inv, payload, result) + if operation == 'add' and changed: + creation = (inv, result) + elif operation == 'remove': + creation = None + return result + + def verify(inv, result): + record = records.get(result.evidence) if type(result) is OwnerResult else None + if record is None or record[0] != inv or record[2] != result: + return False + with contained_output(): + return authenticate(result.evidence, record[1]) is True + + return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), + verify, lambda inv, created: execute(inv, 'remove', created)) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/registration-adapter.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/registration-adapter.md new file mode 100644 index 00000000..c2de4c9c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/registration-adapter.md @@ -0,0 +1,59 @@ +# Attended registration adapter candidate + +Exercise status: unexercised + +This is a source-only, injection-only adapter for the existing Warden closed +kernel. It installs no command, runtime dispatcher, credential provider, evidence +trust root or new service. NetKingdom type/case status and bytes remain unchanged. +The generated descriptor is a proposal; it is not owner acknowledgment. + +`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply +mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ +catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter +and its complete local helper import closure together with the exact reviewed +`check-config` executable digest. Changing that digest changes the descriptor +source pin and descriptor bytes, requiring independently reviewed admission. +The fixed executable is rehashed before each attempt. The caller constructs the +proposed invocation from exact source inputs, installs the fixed adapter once, +and calls normal `Executor.admit`; independently derived admission must match. +No callable replacement after admission is required or supported by this recipe. + +The operation uses existing cluster/issuer checks, byte-preserving merge, +Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS +and whole-data readback. Only closed metadata is passed to the injected durable +owner publisher. The owner authenticator must independently authenticate those +exact bytes. The session-revocation capability must end the same attempt's +parent session; Warden separately verifies its exact revocation evidence. +These capabilities are owner-selected callables, never case-supplied commands. +No implementation of these native capabilities is supplied or implied here. + +An identical existing client returns no change and creates no rollback authority. +Removal requires this process's independently authenticated successful creation +receipt, exact created-object label, unchanged postcreation UID/resourceVersion, +and the original exact client record. External drift, a lost readback, receipt +publication/revocation failure or process loss requires owner reconciliation. +No uncertainty is labeled retry-safe. No automatic reconciliation is installed. +Rollback after restart intentionally remains unavailable until an owner supplies +independently authenticated durable creation provenance and reviews recovery. + +The source tests execute actual maintained helper CAS/readback against synthetic +Kubernetes objects. Three checks exercise actual Warden admission/run/rollback +and uncertainty transitions with synthetic owner trust. Those checks explicitly +override Warden's stale producer source pins inside the test only, selecting the +current local source bytes. This is a compatibility fixture, not acknowledgment, +provenance, normal native admission or deployment acceptance. + +Remaining work belongs in live work records before T12 closure: + +- Warden owns review and refresh of BrowserInputs producer source pins, global + evidence admission and exact native evidence/session capabilities. Its current + pins predate NetKingdom 015694ee and refuse current producer bytes. +- NetKingdom owns separately reviewed descriptor/type pin selection, the env + binding/config/restart steps, and group ensure/revoke provenance. Current group + ensure does not report newly added memberships; current revoke can remove + memberships that predate the case. Wrapping these as reversible creation would + be unsound. Preserve them until a reviewed created-only contract exists. +- Platform owns structural/mint/broker and secret delivery evidence; Apps owns + actual consumer login and group enforcement. No assent is supplied here. +- Attended use still requires existing owner authorization, current caller + identity, native source/config/image pins, native receipts and cleanup proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py new file mode 100644 index 00000000..20ce0f62 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py @@ -0,0 +1,293 @@ +"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" +from dataclasses import asdict, replace +import copy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from warden.case_run import Invocation, digest +from warden.case_receipt_state import Outcome, Pins, Refused +import yaml + +ROOT = Path(__file__).parent +spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') +a = importlib.util.module_from_spec(spec) +spec.loader.exec_module(a) +spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class RegistrationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.binary = Path(self.temp.name) / 'keycape' + self.binary.write_bytes(b'synthetic checker binary') + self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', + 'parameters': f.FORGEJO_PARAMS}}).encode() + desc = a.descriptor(digest(self.binary.read_bytes())) + self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), + digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) + self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', pins) + self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') + self.secret, self.raw = f.fixture() + self.effects = [] + self.evidence = {} + self.revocations = [] + self.kw = dict(admitted=self.admitted, case_source=self.case, + expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', + check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), + publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, + revoke_session=self.revoke) + self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) + self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) + self.check_patch.start() + self.kube_patch.start() + self.addCleanup(self.check_patch.stop) + self.addCleanup(self.kube_patch.stop) + self.addCleanup(self.temp.cleanup) + + def publish(self, payload): + ref = 'owner-evidence:attempt-' + str(len(self.evidence)) + self.evidence[ref] = payload + return ref + + def revoke(self, inv): + self.revocations.append(inv) + return 'session-revoked:' + inv.operation + ':' + inv.attempt_id + + def kube(self, args, payload=None): + if args[:3] == ['get', 'namespace', 'kube-system']: + return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} + if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + return copy.deepcopy(self.secret) + if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: + return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', + 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} + if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: + self.assertEqual(payload[:2], [ + {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, + {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) + self.effects.append(copy.deepcopy(payload)) + self.secret['data']['config.yaml'] = payload[2]['value'] + self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) + return copy.deepcopy(self.secret) + raise AssertionError('unexpected tooling operation') + + def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): + adapter = a.build(**self.kw) + original = copy.deepcopy(self.secret['data']) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.SUCCESS) + self.assertEqual(result.created, ('keycape-client',)) + self.assertTrue(adapter.verify(self.inv, result)) + undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) + self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) + self.assertEqual(self.secret['data'], original) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + for payload in self.evidence.values(): + self.assertNotIn(b'synthetic-marker', payload) + self.assertNotIn(b'config.yaml', payload) + self.assertNotIn(b'signing-key', payload) + + def test_identical_client_is_no_change_and_cannot_be_removed(self): + self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.NO_CHANGE) + self.assertEqual(result.created, ()) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) + self.assertFalse(self.effects) + + def test_existing_drift_refused_and_session_revoked(self): + self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) + with self.assertRaises(Refused): + a.build(**self.kw).invoke(self.inv) + self.assertFalse(self.effects) + self.assertEqual(self.revocations, [self.inv]) + + def test_invocation_rebinding_refused_before_tooling(self): + for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, + {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, + {'pins': replace(self.inv.pins, case=digest(b'other'))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**self.kw).invoke(replace(self.inv, **changes)) + self.assertFalse(self.effects) + + def test_initial_case_descriptor_and_target_pins_are_required(self): + for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, + {'admitted': replace(self.admitted, actor='agent')}, + {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**dict(self.kw, **changes)) + self.assertFalse(self.effects) + + def test_binary_or_source_drift_refused_before_tooling(self): + adapter = a.build(**self.kw) + self.binary.write_bytes(b'changed') + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.binary.write_bytes(b'synthetic checker binary') + with patch.object(a, 'source_digest', return_value=digest(b'changed')): + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self): + substitute = Path(self.temp.name) / 'other-checker' + substitute.write_bytes(b'caller-selected executable') + with self.assertRaisesRegex(Refused, 'descriptor pin differs'): + a.build(**dict(self.kw, check_config=substitute, + check_config_digest=digest(substitute.read_bytes()))) + self.assertFalse(self.effects) + + def test_stale_revision_refuses_mutation(self): + adapter = a.build(**self.kw) + self.secret['metadata']['resourceVersion'] = '8' + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_rollback_requires_exact_provenance_and_postcreation_revision(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') + for created in ((), ('other',), ('keycape-client', 'other')): + with self.subTest(created=created), self.assertRaises(Refused): + adapter.rollback(rollback, created) + self.secret['metadata']['resourceVersion'] = '9' + with self.assertRaises(Refused): + adapter.rollback(rollback, result.created) + self.assertEqual(len(self.effects), 1) + + def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) + self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) + self.evidence[result.evidence] = b'forged' + self.assertFalse(adapter.verify(self.inv, result)) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertEqual(len(self.effects), 1) + + def test_process_loss_does_not_reconstruct_created_authority(self): + result = a.build(**self.kw).invoke(self.inv) + fresh = a.build(**self.kw) + with self.assertRaises(Refused): + fresh.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertIsNone(fresh.reconcile) + self.assertEqual(len(self.effects), 1) + + def test_failed_readback_is_uncertain_and_never_provides_creation(self): + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + adapter = a.build(**self.kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertEqual(len(self.effects), 1) + self.assertFalse(self.evidence) + self.assertEqual(self.revocations, [self.inv]) + + def test_revocation_or_publication_failure_cannot_produce_verified_success(self): + for capability in ('publish', 'revoke_session'): + self.secret, _ = f.fixture() + with self.subTest(capability=capability): + kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) + adapter = a.build(**kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertFalse(self.evidence) + + def kernel_fixture(self): + # Fixture-only proposed type/case; canonical files are never edited. + # Warden's current producer pins predate 015694ee. The explicit override + # below proves compatibility, not an owner pin update or native admission. + import hashlib + import warden.case_inputs as inputs + from warden.case_run import Executor, Journal + root = ROOT.parents[2] + case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) + case['spec']['status'] = 'materialized' + case_source = yaml.safe_dump(case).encode() + typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) + typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} + type_source = yaml.safe_dump(typ).encode() + catalog = b'fixture-only-catalog' + sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() + pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), + digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) + invocation = replace(self.admitted, pins=pins) + adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, + case_acceptance='fixture-acceptance')) + def fixture_verifier(purpose, source, evidence): + if purpose.startswith('session-revocation:'): + _, lane, operation, attempt = purpose.split(':') + return source == digest(case_source) and lane == invocation.lane and evidence == ( + 'session-revoked:' + operation + ':' + attempt) + return evidence in {'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'} + executor = Executor(Journal(Path(self.temp.name) / 'private'), + {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) + revised_pins = {rel: hashlib.sha256((root / rel).read_bytes()).hexdigest() + for rel in inputs.SOURCE_PINS} + with patch.object(inputs, 'SOURCE_PINS', revised_pins): + admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, + principal='founder', actor='human', lane=invocation.lane, + acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', + descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') + self.assertEqual(admitted[1], invocation) + return executor, admitted + + def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): + executor, admitted = self.kernel_fixture() + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.SUCCESS) + self.assertEqual(result.last.created, ('keycape-client',)) + undone = executor.rollback(*admitted) + self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + + def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): + executor, admitted = self.kernel_fixture() + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) + self.assertFalse(result.last.created) + with self.assertRaises(Refused): + executor.run(*admitted, retry=True) + with self.assertRaises(Refused): + executor.rollback(*admitted) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 1) + + def test_kernel_external_revision_change_retains_uncertain_rollback(self): + executor, admitted = self.kernel_fixture() + self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) + self.secret['metadata']['resourceVersion'] = '9' + self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/proposed-descriptor.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/proposed-descriptor.json new file mode 100644 index 00000000..4d6ad4a7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/proposed-descriptor.json @@ -0,0 +1 @@ +{"actor":"human","adapter":"net-kingdom/keycape-registration@v1","adapter_source":"sha256:284e1d70b56461c6190ea634dea63d1b55890d657df8379f2459dfc882260142","allowed_created":["keycape-client"],"effect":"change","lane":"key-cape-oidc-login","owner":"net-kingdom","retry_safe":false,"revocation_scopes":["parent-session"],"step":"register-client","target_parameter":"client_id","version":"warden.case.execution.v1"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/tests.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/tests.txt new file mode 100644 index 00000000..26384da6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/tests.txt @@ -0,0 +1,41 @@ +test_access_groups_require_groups_scope (test_keycape_client.ClientRecordTests.test_access_groups_require_groups_scope) ... ok +test_client_from_case_requires_the_type (test_keycape_client.ClientRecordTests.test_client_from_case_requires_the_type) ... ok +test_confidential_record_binds_env_secret_and_pins_mfa (test_keycape_client.ClientRecordTests.test_confidential_record_binds_env_secret_and_pins_mfa) ... ok +test_invalid_params_refused (test_keycape_client.ClientRecordTests.test_invalid_params_refused) ... ok +test_public_record_has_no_secret (test_keycape_client.ClientRecordTests.test_public_record_has_no_secret) ... ok +test_redirects_keycape_would_refuse_at_startup_are_refused (test_keycape_client.ClientRecordTests.test_redirects_keycape_would_refuse_at_startup_are_refused) ... ok +test_vergabe_case_reproduces_the_hand_built_registration (test_keycape_client.ClientRecordTests.test_vergabe_case_reproduces_the_hand_built_registration) ... ok +test_add_appends_only_this_client_and_preserves_bytes (test_keycape_client.MergeTests.test_add_appends_only_this_client_and_preserves_bytes) ... ok +test_identical_is_noop_and_drift_refused (test_keycape_client.MergeTests.test_identical_is_noop_and_drift_refused) ... ok +test_removal_removes_exactly_this_client (test_keycape_client.MergeTests.test_removal_removes_exactly_this_client) ... ok +test_shared_secret_ref_and_duplicates_refused (test_keycape_client.MergeTests.test_shared_secret_ref_and_duplicates_refused) ... ok +test_apply_needs_case_acceptance (test_keycape_client.RunTests.test_apply_needs_case_acceptance) ... ok +test_apply_needs_env_binding_for_confidential_clients (test_keycape_client.RunTests.test_apply_needs_env_binding_for_confidential_clients) ... ok +test_apply_needs_keycape_check_config (test_keycape_client.RunTests.test_apply_needs_keycape_check_config) ... ok +test_apply_patches_reads_back_and_records_acceptance (test_keycape_client.RunTests.test_apply_patches_reads_back_and_records_acceptance) ... ok +test_apply_refuses_a_changed_revision (test_keycape_client.RunTests.test_apply_refuses_a_changed_revision) ... ok +test_apply_refuses_a_registry_keycape_would_refuse (test_keycape_client.RunTests.test_apply_refuses_a_registry_keycape_would_refuse) ... ok +test_inspect_changes_nothing_and_emits_no_values (test_keycape_client.RunTests.test_inspect_changes_nothing_and_emits_no_values) ... ok +test_inspect_reports_live_and_proposed_checks_with_deployment_env (test_keycape_client.RunTests.test_inspect_reports_live_and_proposed_checks_with_deployment_env) ... ok +test_main_contains_failures (test_keycape_client.RunTests.test_main_contains_failures) ... ok +test_actual_add_and_created_only_rollback_preserve_original_bytes (test_keycape_registration_adapter.RegistrationTests.test_actual_add_and_created_only_rollback_preserve_original_bytes) ... ok +test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust (test_keycape_registration_adapter.RegistrationTests.test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust) ... ok +test_binary_or_source_drift_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_binary_or_source_drift_refused_before_tooling) ... ok +test_existing_drift_refused_and_session_revoked (test_keycape_registration_adapter.RegistrationTests.test_existing_drift_refused_and_session_revoked) ... ok +test_failed_readback_is_uncertain_and_never_provides_creation (test_keycape_registration_adapter.RegistrationTests.test_failed_readback_is_uncertain_and_never_provides_creation) ... ok +test_identical_client_is_no_change_and_cannot_be_removed (test_keycape_registration_adapter.RegistrationTests.test_identical_client_is_no_change_and_cannot_be_removed) ... ok +test_initial_case_descriptor_and_target_pins_are_required (test_keycape_registration_adapter.RegistrationTests.test_initial_case_descriptor_and_target_pins_are_required) ... ok +test_invocation_rebinding_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_invocation_rebinding_refused_before_tooling) ... ok +test_kernel_external_revision_change_retains_uncertain_rollback (test_keycape_registration_adapter.RegistrationTests.test_kernel_external_revision_change_retains_uncertain_rollback) ... ok +test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo (test_keycape_registration_adapter.RegistrationTests.test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo) ... ok +test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo (test_keycape_registration_adapter.RegistrationTests.test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo) ... ok +test_process_loss_does_not_reconstruct_created_authority (test_keycape_registration_adapter.RegistrationTests.test_process_loss_does_not_reconstruct_created_authority) ... ok +test_revocation_or_publication_failure_cannot_produce_verified_success (test_keycape_registration_adapter.RegistrationTests.test_revocation_or_publication_failure_cannot_produce_verified_success) ... ok +test_rollback_requires_exact_provenance_and_postcreation_revision (test_keycape_registration_adapter.RegistrationTests.test_rollback_requires_exact_provenance_and_postcreation_revision) ... ok +test_stale_revision_refuses_mutation (test_keycape_registration_adapter.RegistrationTests.test_stale_revision_refuses_mutation) ... ok +test_substituting_caller_selected_checker_under_same_descriptor_is_refused (test_keycape_registration_adapter.RegistrationTests.test_substituting_caller_selected_checker_under_same_descriptor_is_refused) ... ok + +---------------------------------------------------------------------- +Ran 36 tests in 1.145s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/verify_candidate.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/verify_candidate.py new file mode 100644 index 00000000..de219faf --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r2/verify_candidate.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Reconstruct isolated source candidate and exercise synthetic fixtures only.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +p = argparse.ArgumentParser() +p.add_argument('--owner-root', type=Path, default=Path('/home/worsch/net-kingdom')) +p.add_argument('--warden-root', type=Path, default=Path('/home/worsch/ops-warden')) +args = p.parse_args() +packet = Path(__file__).resolve().parent +manifest = json.loads((packet / 'manifest.json').read_text()) +with tempfile.TemporaryDirectory(prefix='nk-registration-review-') as directory: + candidate = Path(directory) + for relative, expected in manifest['fixture_dependencies'].items(): + source = args.owner_root / relative + data = source.read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('fixture dependency drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + for relative, expected in manifest['postimages'].items(): + data = (packet / 'postimages' / relative).read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('candidate source drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + result = subprocess.run([sys.executable, '-m', 'unittest', 'discover', '-s', + str(candidate / 'sso-mfa/k8s/keycape'), '-p', 'test_keycape*.py', '-v'], + env=dict(os.environ, PYTHONPATH=str(args.warden_root / 'src'))) + raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/README.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/README.md new file mode 100644 index 00000000..c560678a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/README.md @@ -0,0 +1,52 @@ +# NetKingdom registration adapter source candidate R3 + +Source-only candidate awaiting independent root review. No canonical owner file, +type/case bytes or status changed; no credential or live action was used. +Exercise status: unexercised. + +The patch adds a fixed attended registration adapter around actual maintained +`keycape-client.run`, its contract notes and 16 new fixture checks. The 20 +existing helper checks also pass. Three new checks exercise actual Warden +admission/dispatch/rollback and uncertain transitions with synthetic owner trust +with unchanged published Warden d97652a producer pins. No replacement of adapter +callables after admission is used: the proposed Invocation is built first, and +normal admission independently derives the same full binding. + +R1's root review found an unbound config-checker executable: the caller could +supply another binary and its matching hash under the same descriptor. That +rejected revision remains in `rejected-r1/`; the superseded R2 source/fixture +packet remains in `reviewed-r2/`. R2 corrected the custody finding; R3 retains that correction and includes the exact selected +checker hash in the adapter source-composition pin and descriptor. Substitution +under the original descriptor is now refused before tooling. The descriptor +retained here uses a synthetic checker digest only; native executable selection +and owner acknowledgment remain outstanding. + +Reproduce from current pinned owner dependencies: + +```bash +python3 docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/verify_candidate.py +``` + +`implementation.patch` adds three files and proposes an exact owner workplan edit +against retained `preimages/`. No canonical workplan changed. `postimages/` holds the exact proposed +bytes. `manifest.json` binds all source/fixture hashes and the observed owner +state at packaging; this is not a reconstructed session-start dirty-state +snapshot. `tests.txt` retains the 36 passing checks. Warden/source owner heads +and current canonical changes remain outside this patch. + +The owner contract in `postimages/sso-mfa/k8s/keycape/registration-adapter.md` +records required native callbacks and remaining scope. In particular: published Warden d97652a now admits the exact producer source +bytes through its normal pins, without attesting this adapter; group ensure +has no created-membership provenance; group revoke can remove a pre-existing +membership; env/config/restart adapters remain; durable post-restart creation +reconciliation is absent. Platform/Apps native acceptance remains separately +owned. None of these gaps is supplied by the fixture proof. + +The exact workplan proposal preserves existing T10/T11/T12 Hub UUIDs and all +other prior bytes outside T06 dependencies/T12 prose. T13 records source +implementation and fixture proof as done, explicitly excluding independent +review/publication. T12 retains env/config/restart work, while T14 tracks group +creation provenance, T15 durable registration recovery and T16 actual descriptor/ +checker artifact selection and owner review. T06 retains all source dependencies +and native acceptance scope. No acknowledged type/case bytes or statuses change. +Root review and publication receipts must be added when integration is approved. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/implementation.patch new file mode 100644 index 00000000..c2c7b7e4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/implementation.patch @@ -0,0 +1,669 @@ +--- /dev/null ++++ b/sso-mfa/k8s/keycape/keycape-registration-adapter.py +@@ -0,0 +1,180 @@ ++"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. ++ ++No CLI, credentials, evidence trust root or live registration is installed here. ++Owner publication/authentication and parent-session revocation are mandatory ++injected capabilities. The kernel must independently admit both evidence kinds. ++""" ++from __future__ import annotations ++ ++import argparse ++from dataclasses import asdict, replace ++import json ++import importlib.util ++from pathlib import Path ++ ++import yaml ++ ++from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER, DIGEST ++from warden.case_receipt_state import Outcome, Refused ++ ++ROOT = Path(__file__).resolve().parent ++SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', ++ 'portal-client-rollout.py', 'openbao-client-config.py', ++ 'register-user-engine-portal.py') ++REFERENCE = 'net-kingdom/keycape-registration@v1' ++ ++# Importing the maintained helper performs no tooling operation. ++_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') ++helper = importlib.util.module_from_spec(_spec) ++_spec.loader.exec_module(helper) ++ ++ ++def source_digest(check_config_digest): ++ """Pin adapter plus the complete local helper import closure, in filename order.""" ++ if not isinstance(check_config_digest, str) or not DIGEST.fullmatch(check_config_digest): ++ raise Refused('exact reviewed checker digest required') ++ composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}, ++ 'check_config_binary': check_config_digest} ++ return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode()) ++ ++ ++def descriptor(check_config_digest): ++ return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', ++ REFERENCE, source_digest(check_config_digest), 'human', 'key-cape-oidc-login', ++ 'client_id', 'change', ('keycape-client',), False, ++ ('parent-session',)) ++ ++ ++def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, ++ expected_resource_version: str, case_acceptance: str, ++ check_config: Path, check_config_digest: str, publish, authenticate, ++ revoke_session): ++ """Bind one admitted case and observed revision; return a closed kernel Adapter. ++ ++ publish(bytes)->identifier durably retains only the closed metadata payload. ++ authenticate(identifier, bytes)->True must verify independent owner custody. ++ revoke_session(invocation)->identifier must revoke this attempt's parent ++ session; Warden independently authenticates it using its own evidence verifier. ++ These callbacks are capabilities, never command strings or case parameters. ++ ++ Only a verified creation in this process may be rolled back. Process loss, ++ write/readback ambiguity, publication/revocation failure and source drift ++ require owner reconciliation; none are converted to retry-safe failures. ++ """ ++ reviewed_source = source_digest(check_config_digest) ++ desc = descriptor(check_config_digest) ++ if type(admitted) is not Invocation or not all( ++ isinstance(value, str) and IDENTIFIER.fullmatch(value) ++ for value in (admitted.step, admitted.owner, admitted.target, ++ admitted.actor, admitted.principal, admitted.lane)): ++ raise Refused('bounded invocation required') ++ if desc.adapter_source != reviewed_source: ++ raise Refused('owner source changed during binding') ++ if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( ++ desc.step, desc.owner, desc.actor, desc.lane): ++ raise Refused('registration invocation boundary differs') ++ if admitted.operation != 'admission' or admitted.attempt_id: ++ raise Refused('expected admission invocation') ++ if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: ++ raise Refused('case source pin differs') ++ # Pins.adapter is the descriptor digest, not the owner source digest. ++ descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ if admitted.pins.adapter != digest(descriptor_bytes): ++ raise Refused('descriptor pin differs') ++ if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in ++ (expected_uid, expected_resource_version, case_acceptance)): ++ raise Refused('bounded attended binding required') ++ if digest(case_acceptance.encode()) != admitted.pins.acceptance: ++ raise Refused('acceptance evidence binding differs') ++ if not all(callable(x) for x in (publish, authenticate, revoke_session)): ++ raise Refused('owner evidence and revocation capabilities required') ++ with contained_output(): ++ case = yaml.safe_load(case_source) ++ client = helper.client_from_case(case) ++ if admitted.target != client['clientId']: ++ raise Refused('case target differs') ++ check_config = Path(check_config) ++ records = {} ++ creation = None ++ ++ def binding(inv, operation): ++ if (replace(inv, attempt_id='', operation='admission') != admitted ++ or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): ++ raise Refused('attempt binding differs') ++ if source_digest(check_config_digest) != reviewed_source: ++ raise Refused('owner source changed') ++ if digest(check_config.read_bytes()) != check_config_digest: ++ raise Refused('check-config binary changed') ++ ++ def execute(inv, operation, created=()): ++ nonlocal creation ++ try: ++ with contained_output(): ++ binding(inv, 'run' if operation == 'add' else 'rollback') ++ uid, revision = expected_uid, expected_resource_version ++ if operation == 'remove': ++ if (created != ('keycape-client',) or creation is None ++ or not verify(*creation)): ++ raise Refused('no authenticated case-created client') ++ original_payload = json.loads(records[creation[1].evidence][1]) ++ uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] ++ args = argparse.Namespace(mode='apply', operation=operation, ++ expected_uid=uid, expected_resource_version=revision, ++ case_acceptance=case_acceptance, check_config=str(check_config)) ++ receipt = helper.run(args, client) ++ # The helper has completed CAS, whole-data readback and preservation. ++ # Never retain check-config diagnostics or arbitrary helper fields. ++ if not (receipt['schema'] == 'netkingdom.keycape-client.v1' ++ and receipt['target'] == 'sso/keycape-config' ++ and receipt['client_id'] == admitted.target ++ and receipt['mode'] == 'apply' and receipt['operation'] == operation ++ and receipt['values_emitted'] is False ++ and type(receipt['changed']) is bool ++ and receipt['unrelated_config_bytes_preserved'] is True ++ and receipt['other_secret_data_unchanged'] is True ++ and receipt['before'] == {'uid': uid, 'resource_version': revision} ++ and receipt['after']['uid'] == uid ++ and receipt['check_config']['proposed']['valid'] is True): ++ raise Refused('owner readback contract differs') ++ after = receipt['after'] ++ if (set(after) != {'uid', 'resource_version'} or ++ not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): ++ raise Refused('owner revision contract differs') ++ changed = receipt['changed'] ++ outcome = (Outcome.ROLLED_BACK if operation == 'remove' else ++ Outcome.SUCCESS if changed else Outcome.NO_CHANGE) ++ objects = ('keycape-client',) if operation == 'add' and changed else () ++ finally: ++ # Even helper failure must end the parent session. Failures propagate ++ # contained to the kernel and become uncertain, never safe to replay. ++ with contained_output(): ++ revocation = revoke_session(inv) ++ if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): ++ raise Refused('session revocation evidence missing') ++ payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', ++ 'invocation': asdict(inv), 'source': reviewed_source, ++ 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], ++ 'after': after, 'session_revocation': revocation}, ++ sort_keys=True, separators=(',', ':')).encode() ++ with contained_output(): ++ evidence = publish(payload) ++ if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: ++ raise Refused('owner evidence identifier invalid or reused') ++ result = OwnerResult(outcome, evidence, objects, True, revocation, ++ attempt_id=inv.attempt_id, operation=inv.operation) ++ records[evidence] = (inv, payload, result) ++ if operation == 'add' and changed: ++ creation = (inv, result) ++ elif operation == 'remove': ++ creation = None ++ return result ++ ++ def verify(inv, result): ++ record = records.get(result.evidence) if type(result) is OwnerResult else None ++ if record is None or record[0] != inv or record[2] != result: ++ return False ++ with contained_output(): ++ return authenticate(result.evidence, record[1]) is True ++ ++ return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), ++ verify, lambda inv, created: execute(inv, 'remove', created)) +--- /dev/null ++++ b/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py +@@ -0,0 +1,289 @@ ++"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" ++from dataclasses import asdict, replace ++import copy ++import importlib.util ++import json ++from pathlib import Path ++import tempfile ++import unittest ++from unittest.mock import patch ++ ++from warden.case_run import Invocation, digest ++from warden.case_receipt_state import Outcome, Pins, Refused ++import yaml ++ ++ROOT = Path(__file__).parent ++spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') ++a = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(a) ++spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') ++f = importlib.util.module_from_spec(spec) ++spec.loader.exec_module(f) ++ ++ ++class RegistrationTests(unittest.TestCase): ++ def setUp(self): ++ self.temp = tempfile.TemporaryDirectory() ++ self.binary = Path(self.temp.name) / 'keycape' ++ self.binary.write_bytes(b'synthetic checker binary') ++ self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', ++ 'parameters': f.FORGEJO_PARAMS}}).encode() ++ desc = a.descriptor(digest(self.binary.read_bytes())) ++ self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() ++ pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), ++ digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) ++ self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', pins) ++ self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') ++ self.secret, self.raw = f.fixture() ++ self.effects = [] ++ self.evidence = {} ++ self.revocations = [] ++ self.kw = dict(admitted=self.admitted, case_source=self.case, ++ expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', ++ check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), ++ publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, ++ revoke_session=self.revoke) ++ self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) ++ self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) ++ self.check_patch.start() ++ self.kube_patch.start() ++ self.addCleanup(self.check_patch.stop) ++ self.addCleanup(self.kube_patch.stop) ++ self.addCleanup(self.temp.cleanup) ++ ++ def publish(self, payload): ++ ref = 'owner-evidence:attempt-' + str(len(self.evidence)) ++ self.evidence[ref] = payload ++ return ref ++ ++ def revoke(self, inv): ++ self.revocations.append(inv) ++ return 'session-revoked:' + inv.operation + ':' + inv.attempt_id ++ ++ def kube(self, args, payload=None): ++ if args[:3] == ['get', 'namespace', 'kube-system']: ++ return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} ++ if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ return copy.deepcopy(self.secret) ++ if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: ++ return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', ++ 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} ++ if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: ++ self.assertEqual(payload[:2], [ ++ {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, ++ {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) ++ self.effects.append(copy.deepcopy(payload)) ++ self.secret['data']['config.yaml'] = payload[2]['value'] ++ self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) ++ return copy.deepcopy(self.secret) ++ raise AssertionError('unexpected tooling operation') ++ ++ def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): ++ adapter = a.build(**self.kw) ++ original = copy.deepcopy(self.secret['data']) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.created, ('keycape-client',)) ++ self.assertTrue(adapter.verify(self.inv, result)) ++ undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) ++ self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(self.secret['data'], original) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ for payload in self.evidence.values(): ++ self.assertNotIn(b'synthetic-marker', payload) ++ self.assertNotIn(b'config.yaml', payload) ++ self.assertNotIn(b'signing-key', payload) ++ ++ def test_identical_client_is_no_change_and_cannot_be_removed(self): ++ self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertEqual(result.outcome, Outcome.NO_CHANGE) ++ self.assertEqual(result.created, ()) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) ++ self.assertFalse(self.effects) ++ ++ def test_existing_drift_refused_and_session_revoked(self): ++ self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) ++ with self.assertRaises(Refused): ++ a.build(**self.kw).invoke(self.inv) ++ self.assertFalse(self.effects) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_invocation_rebinding_refused_before_tooling(self): ++ for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, ++ {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, ++ {'pins': replace(self.inv.pins, case=digest(b'other'))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**self.kw).invoke(replace(self.inv, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_initial_case_descriptor_and_target_pins_are_required(self): ++ for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, ++ {'admitted': replace(self.admitted, actor='agent')}, ++ {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): ++ with self.subTest(changes=changes), self.assertRaises(Refused): ++ a.build(**dict(self.kw, **changes)) ++ self.assertFalse(self.effects) ++ ++ def test_binary_or_source_drift_refused_before_tooling(self): ++ adapter = a.build(**self.kw) ++ self.binary.write_bytes(b'changed') ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.binary.write_bytes(b'synthetic checker binary') ++ with patch.object(a, 'source_digest', return_value=digest(b'changed')): ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self): ++ substitute = Path(self.temp.name) / 'other-checker' ++ substitute.write_bytes(b'caller-selected executable') ++ with self.assertRaisesRegex(Refused, 'descriptor pin differs'): ++ a.build(**dict(self.kw, check_config=substitute, ++ check_config_digest=digest(substitute.read_bytes()))) ++ self.assertFalse(self.effects) ++ ++ def test_stale_revision_refuses_mutation(self): ++ adapter = a.build(**self.kw) ++ self.secret['metadata']['resourceVersion'] = '8' ++ with self.assertRaises(Refused): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.effects) ++ ++ def test_rollback_requires_exact_provenance_and_postcreation_revision(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') ++ for created in ((), ('other',), ('keycape-client', 'other')): ++ with self.subTest(created=created), self.assertRaises(Refused): ++ adapter.rollback(rollback, created) ++ self.secret['metadata']['resourceVersion'] = '9' ++ with self.assertRaises(Refused): ++ adapter.rollback(rollback, result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): ++ adapter = a.build(**self.kw) ++ result = adapter.invoke(self.inv) ++ self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) ++ self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) ++ self.evidence[result.evidence] = b'forged' ++ self.assertFalse(adapter.verify(self.inv, result)) ++ with self.assertRaises(Refused): ++ adapter.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_process_loss_does_not_reconstruct_created_authority(self): ++ result = a.build(**self.kw).invoke(self.inv) ++ fresh = a.build(**self.kw) ++ with self.assertRaises(Refused): ++ fresh.rollback(replace(self.inv, operation='rollback'), result.created) ++ self.assertIsNone(fresh.reconcile) ++ self.assertEqual(len(self.effects), 1) ++ ++ def test_failed_readback_is_uncertain_and_never_provides_creation(self): ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ adapter = a.build(**self.kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertEqual(len(self.effects), 1) ++ self.assertFalse(self.evidence) ++ self.assertEqual(self.revocations, [self.inv]) ++ ++ def test_revocation_or_publication_failure_cannot_produce_verified_success(self): ++ for capability in ('publish', 'revoke_session'): ++ self.secret, _ = f.fixture() ++ with self.subTest(capability=capability): ++ kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) ++ adapter = a.build(**kw) ++ with self.assertRaisesRegex(Refused, '^owner callback failed$'): ++ adapter.invoke(self.inv) ++ self.assertFalse(self.evidence) ++ ++ def kernel_fixture(self): ++ # Fixture-only proposed type/case; canonical files are never edited. ++ # Warden's published producer pins apply unchanged. Owner trust and ++ # proposed descriptor/type/case are fixture-only, never native admission. ++ import warden.case_inputs as inputs ++ from warden.case_run import Executor, Journal ++ root = ROOT.parents[2] ++ case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) ++ case['spec']['status'] = 'materialized' ++ case_source = yaml.safe_dump(case).encode() ++ typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) ++ typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} ++ type_source = yaml.safe_dump(typ).encode() ++ catalog = b'fixture-only-catalog' ++ sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', ++ 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() ++ pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), ++ digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) ++ invocation = replace(self.admitted, pins=pins) ++ adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, ++ case_acceptance='fixture-acceptance')) ++ def fixture_verifier(purpose, source, evidence): ++ if purpose.startswith('session-revocation:'): ++ _, lane, operation, attempt = purpose.split(':') ++ return source == digest(case_source) and lane == invocation.lane and evidence == ( ++ 'session-revoked:' + operation + ':' + attempt) ++ return evidence in {'fixture-acceptance', 'fixture-materialization', ++ 'fixture-descriptor', 'fixture-session'} ++ executor = Executor(Journal(Path(self.temp.name) / 'private'), ++ {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) ++ admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, ++ principal='founder', actor='human', lane=invocation.lane, ++ acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', ++ descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') ++ self.assertEqual(admitted[1], invocation) ++ return executor, admitted ++ ++ def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): ++ executor, admitted = self.kernel_fixture() ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.SUCCESS) ++ self.assertEqual(result.last.created, ('keycape-client',)) ++ undone = executor.rollback(*admitted) ++ self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) ++ self.assertEqual(len(self.effects), 2) ++ self.assertEqual(len(self.revocations), 2) ++ ++ def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): ++ executor, admitted = self.kernel_fixture() ++ original_kube = self.kube ++ def lost(args, payload=None): ++ if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: ++ raise RuntimeError('synthetic sensitive detail') ++ return original_kube(args, payload) ++ with patch.object(a.helper.lane, 'kubectl', side_effect=lost): ++ result = executor.run(*admitted) ++ self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) ++ self.assertFalse(result.last.created) ++ with self.assertRaises(Refused): ++ executor.run(*admitted, retry=True) ++ with self.assertRaises(Refused): ++ executor.rollback(*admitted) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 1) ++ ++ def test_kernel_external_revision_change_retains_uncertain_rollback(self): ++ executor, admitted = self.kernel_fixture() ++ self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) ++ self.secret['metadata']['resourceVersion'] = '9' ++ self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) ++ self.assertEqual(len(self.effects), 1) ++ self.assertEqual(len(self.revocations), 2) ++ ++ ++if __name__ == '__main__': ++ unittest.main() +--- /dev/null ++++ b/sso-mfa/k8s/keycape/registration-adapter.md +@@ -0,0 +1,59 @@ ++# Attended registration adapter candidate ++ ++Exercise status: unexercised ++ ++This is a source-only, injection-only adapter for the existing Warden closed ++kernel. It installs no command, runtime dispatcher, credential provider, evidence ++trust root or new service. NetKingdom type/case status and bytes remain unchanged. ++The generated descriptor is a proposal; it is not owner acknowledgment. ++ ++`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply ++mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ ++catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter ++and its complete local helper import closure together with the exact reviewed ++`check-config` executable digest. Changing that digest changes the descriptor ++source pin and descriptor bytes, requiring independently reviewed admission. ++The fixed executable is rehashed before each attempt. The caller constructs the ++proposed invocation from exact source inputs, installs the fixed adapter once, ++and calls normal `Executor.admit`; independently derived admission must match. ++No callable replacement after admission is required or supported by this recipe. ++ ++The operation uses existing cluster/issuer checks, byte-preserving merge, ++Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS ++and whole-data readback. Only closed metadata is passed to the injected durable ++owner publisher. The owner authenticator must independently authenticate those ++exact bytes. The session-revocation capability must end the same attempt's ++parent session; Warden separately verifies its exact revocation evidence. ++These capabilities are owner-selected callables, never case-supplied commands. ++No implementation of these native capabilities is supplied or implied here. ++ ++An identical existing client returns no change and creates no rollback authority. ++Removal requires this process's independently authenticated successful creation ++receipt, exact created-object label, unchanged postcreation UID/resourceVersion, ++and the original exact client record. External drift, a lost readback, receipt ++publication/revocation failure or process loss requires owner reconciliation. ++No uncertainty is labeled retry-safe. No automatic reconciliation is installed. ++Rollback after restart intentionally remains unavailable until an owner supplies ++independently authenticated durable creation provenance and reviews recovery. ++ ++The source tests execute actual maintained helper CAS/readback against synthetic ++Kubernetes objects. Three checks exercise actual Warden admission/run/rollback ++and uncertainty transitions with synthetic owner trust. Warden d97652a's exact ++published producer source pins apply unchanged through actual BrowserInputs. ++This is a source compatibility fixture, not acknowledgment, owner provenance, ++normal native admission or deployment acceptance. ++ ++Remaining work belongs in live work records before T12 closure: ++ ++- Warden owns global evidence admission and exact native evidence/session ++ capabilities. Published d97652a refreshes BrowserInputs producer pins for ++ NetKingdom 015694ee; that source assent does not attest this new adapter. ++- NetKingdom owns separately reviewed descriptor/type pin selection, the env ++ binding/config/restart steps, and group ensure/revoke provenance. Current group ++ ensure does not report newly added memberships; current revoke can remove ++ memberships that predate the case. Wrapping these as reversible creation would ++ be unsound. Preserve them until a reviewed created-only contract exists. ++- Platform owns structural/mint/broker and secret delivery evidence; Apps owns ++ actual consumer login and group enforcement. No assent is supplied here. ++- Attended use still requires existing owner authorization, current caller ++ identity, native source/config/image pins, native receipts and cleanup proof. +--- a/workplans/NK-WP-0048-security-case-types.md ++++ b/workplans/NK-WP-0048-security-case-types.md +@@ -323,7 +323,7 @@ + ```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, MASON-WP-0010-T03, WARDEN-WP-0044-T03] ++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, 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" +@@ -631,17 +631,106 @@ + state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd" + ``` + +-Owner: NetKingdom. Next action: wrap the existing contained `keycape-client.py` +-registration/config/env and identity-provisioner ensure/revoke entry points in +-bounded owner adapters or receipt-only attended steps for Warden's closed kernel. +-Reuse the existing caller/session boundary, CAS/readback and status-only outputs; +-never acquire user-engine's runtime identity or expose registry/client secrets. +-Completion requires exact adapter/source descriptors, positive and refusal source +-checks, uncertainty/created-only rollback behavior and a separate owner review of +-the proposed machine seam. Source preparation can begin without credentials. +-Warden independently owns global provenance/admission integration; Platform owns +-mint/broker and native structural receipts; Apps owns consumer delivery/group +-acceptance. Their assent is not supplied by this task. Pinning descriptors into +-an acknowledged type remains a separately reviewed type-source change. Actual +-attended execution still requires its existing owner authorization and native +-receipts. T06 retains all native/reference lifecycle acceptance scope. ++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 ++``` ++ ++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. Exact root review/publication receipts are required on integration. ++ ++## T14 — Preserve created-only provenance for group ensure and revoke ++ ++```task ++id: NK-WP-0048-T14 ++status: todo ++priority: high ++``` ++ ++Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner ++ensure/revoke contract with independently retained prestate and exact newly ++created groups/memberships before exposing reversible owner callbacks. Current ++ensure returns no membership-creation inventory; current revoke may remove a ++membership that predated this case. Completion requires actual helper-backed ++positive/refusal fixtures, all validation before mutation, per-attempt evidence ++and revocation, readback, ambiguity handling, and rollback that preserves every ++pre-existing/shared group and membership. No existing endpoint or service token ++is treated as new authorization. ++ ++## T15 — Define authenticated recovery for lost registration process state ++ ++```task ++id: NK-WP-0048-T15 ++status: todo ++priority: high ++``` ++ ++Owner: NetKingdom for registration state, with Warden owning kernel evidence ++admission. Next action: select the exact existing owner evidence holder/readback ++contract and define authenticated lookup for the original creation attempt. ++Completion requires restart/lost-receipt fixtures that bind case/source/principal, ++original created-only provenance and current native revision before any undo; ++uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses ++post-restart rollback and installs no reconciliation callback. A new broker or ++unverified JSON receipt is not a recovery contract. Actual evidence-holder ++acceptance and attended recovery remain separately authorized native work. ++ ++## T16 — Select and review the actual machine descriptor composition ++ ++```task ++id: NK-WP-0048-T16 ++status: todo ++priority: high ++``` ++ ++Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden ++independently owns global provenance/admission. Next action: identify the exact ++reviewed checker executable artifact and native owner evidence/session callback ++contracts, then assemble the complete step descriptor set and a proposed explicit ++type version selection. T13's retained descriptor pins a synthetic checker only. ++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. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/manifest.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/manifest.json new file mode 100644 index 00000000..4fa8706c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/manifest.json @@ -0,0 +1,63 @@ +{ + "schema": "netkingdom.registration-adapter-source-candidate.v1", + "date": "2026-10-16", + "owner": "net-kingdom", + "candidate": "/tmp/nk-browser-adapter-next", + "owner_revision_at_packaging": "015694ee414c802dc2ef1fa0120dfed610086e69", + "owner_status_at_packaging": "MM .custodian-brief.md\n M WORK-RECORDS.md\n M workplans/NK-WP-0048-security-case-types.md\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-capture/\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-inventory.jsonl\n?? docs/evidence/2026-10-15-cadence-adoption/hourly-observation-monitor.json\n?? intakes/NET-IN-0005-single-use-setup-link-mail-handoff.md\n", + "owner_diff_digest_at_packaging": "sha256:7ed1cfaf24a0b154180c93c7491ae7274e508d8f2258c6e741bf69f3768d74eb", + "warden_revision": "d97652a3bb5fa733b063d71a1b74068efcccf572", + "adapter_source": "sha256:284e1d70b56461c6190ea634dea63d1b55890d657df8379f2459dfc882260142", + "postimages": { + "sso-mfa/k8s/keycape/keycape-registration-adapter.py": "sha256:8a6bd0ea111d67a0df89c7e3cd968e7bf81d29c2f2a70adf08eb747e1d1a5bdc", + "sso-mfa/k8s/keycape/test_keycape_registration_adapter.py": "sha256:beabe9fe3d2c43d296fcac475d1d0c97ebb9829debdf68846c7f32b15958641c", + "sso-mfa/k8s/keycape/registration-adapter.md": "sha256:5dc28ecc87d029d42a7bd5a02501ee2665b956e1201b8fceddb5854214d26bf8", + "workplans/NK-WP-0048-security-case-types.md": "sha256:1a3eb6e7bf8753dfe40869284ee78fea2ebce2f0bcab30d47e0903d28fdada2d" + }, + "helper_dependency_hashes": { + "keycape-client.py": "sha256:e31ee2883fad4f93635700608ad98e05984c9e0453515dd7710c897ca856b8c8", + "portal-client-rollout.py": "sha256:158e1d36eb881cdd0a2eda91b472adea5e8fde5061e00d26dda368580aeb871f", + "openbao-client-config.py": "sha256:1b908b4c36134212d78f9fbe7a2a201eb3ced06d31bb1dcea4a59d80689d84b1", + "register-user-engine-portal.py": "sha256:67173edc44f5457e7b6a3e0fb2321b398159cfce5dea8ead01eabf835fb38d76" + }, + "tests": { + "new": 16, + "existing": 20, + "total": 36, + "passed": true, + "native": false, + "producer_pin_override": false, + "producer_admission": "normal published Warden d97652a exact source pins" + }, + "canonical_owner_writes": false, + "published": false, + "review_status": "r1 rejected: unbound checker; r2 checker corrected; r3 removes synthetic pin override, awaiting independent root review", + "native_acceptance": false, + "revision": "r3", + "fixture_checker_digest": "sha256:bb8a8e3d1ff5176c3f7da3144b55fea2effc9e4e29790146e2ae58d7f8363fb0", + "descriptor_scope": "synthetic fixture only; no native checker artifact selected or admitted", + "fixture_dependencies": { + "sso-mfa/k8s/keycape/keycape-client.py": "sha256:e31ee2883fad4f93635700608ad98e05984c9e0453515dd7710c897ca856b8c8", + "sso-mfa/k8s/keycape/portal-client-rollout.py": "sha256:158e1d36eb881cdd0a2eda91b472adea5e8fde5061e00d26dda368580aeb871f", + "sso-mfa/k8s/keycape/openbao-client-config.py": "sha256:1b908b4c36134212d78f9fbe7a2a201eb3ced06d31bb1dcea4a59d80689d84b1", + "sso-mfa/k8s/keycape/register-user-engine-portal.py": "sha256:67173edc44f5457e7b6a3e0fb2321b398159cfce5dea8ead01eabf835fb38d76", + "sso-mfa/k8s/keycape/test_keycape_client.py": "sha256:9572044679feaedfe732e15a2b6f8ef6b41a8fe2e4430257f309a3e83e97736c", + "sso-mfa/k8s/keycape/vergabe-client-rollout.py": "sha256:8cee6cc5ff3e3e8617304055263d04b393d0e005c34d9af12cb7e8bc685bac1a", + "tools/security-case-types/security_case_evidence.py": "sha256:da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75", + "tools/security-case-types/security_case_validator.py": "sha256:1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b", + "tools/security-scenario-composer/security_scenario_composer.py": "sha256:9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", + "tools/playbook-capability-contract/playbook_contract_validator.py": "sha256:2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", + "security-case-types/oidc-browser-relying-party/type.yaml": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "security-cases/forgejo-netkingdom-oidc.yaml": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d" + }, + "preimages": { + "workplans/NK-WP-0048-security-case-types.md": "sha256:40e15d545768d41e953904f7a63ef14fa5ab5cb5d78f9f69cec60547cb1f04b2" + }, + "packet_hashes": { + "implementation.patch": "sha256:f900f026441dd6fe8f83ceca2bb79212f5643e1f96ded21c190890d1f85dbab0", + "proposed-descriptor.json": "sha256:53bc48959ae9fcb7f2927f34558e55275dcdb512efba116f75d54eca36a375c4", + "README.md": "sha256:0ec6df7b586f95b7919a2daacde97e7b929dec24a370b55dab0982e3e0b938b8", + "verify_candidate.py": "sha256:00c7b20d049c579b393539d343dbcb0f0dd560de16d5a43593888f32c8fc18ac", + "tests.txt": "sha256:154850fd30c3fb32a8b54da1eed852c0337fffecc432dae4fe19a4ca4b17f8cd" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py new file mode 100644 index 00000000..995dd263 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/keycape-registration-adapter.py @@ -0,0 +1,180 @@ +"""Unexercised, injection-only attended registration seam for Warden's v1 kernel. + +No CLI, credentials, evidence trust root or live registration is installed here. +Owner publication/authentication and parent-session revocation are mandatory +injected capabilities. The kernel must independently admit both evidence kinds. +""" +from __future__ import annotations + +import argparse +from dataclasses import asdict, replace +import json +import importlib.util +from pathlib import Path + +import yaml + +from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER, DIGEST +from warden.case_receipt_state import Outcome, Refused + +ROOT = Path(__file__).resolve().parent +SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py', + 'portal-client-rollout.py', 'openbao-client-config.py', + 'register-user-engine-portal.py') +REFERENCE = 'net-kingdom/keycape-registration@v1' + +# Importing the maintained helper performs no tooling operation. +_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py') +helper = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(helper) + + +def source_digest(check_config_digest): + """Pin adapter plus the complete local helper import closure, in filename order.""" + if not isinstance(check_config_digest, str) or not DIGEST.fullmatch(check_config_digest): + raise Refused('exact reviewed checker digest required') + composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}, + 'check_config_binary': check_config_digest} + return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode()) + + +def descriptor(check_config_digest): + return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom', + REFERENCE, source_digest(check_config_digest), 'human', 'key-cape-oidc-login', + 'client_id', 'change', ('keycape-client',), False, + ('parent-session',)) + + +def build(*, admitted: Invocation, case_source: bytes, expected_uid: str, + expected_resource_version: str, case_acceptance: str, + check_config: Path, check_config_digest: str, publish, authenticate, + revoke_session): + """Bind one admitted case and observed revision; return a closed kernel Adapter. + + publish(bytes)->identifier durably retains only the closed metadata payload. + authenticate(identifier, bytes)->True must verify independent owner custody. + revoke_session(invocation)->identifier must revoke this attempt's parent + session; Warden independently authenticates it using its own evidence verifier. + These callbacks are capabilities, never command strings or case parameters. + + Only a verified creation in this process may be rolled back. Process loss, + write/readback ambiguity, publication/revocation failure and source drift + require owner reconciliation; none are converted to retry-safe failures. + """ + reviewed_source = source_digest(check_config_digest) + desc = descriptor(check_config_digest) + if type(admitted) is not Invocation or not all( + isinstance(value, str) and IDENTIFIER.fullmatch(value) + for value in (admitted.step, admitted.owner, admitted.target, + admitted.actor, admitted.principal, admitted.lane)): + raise Refused('bounded invocation required') + if desc.adapter_source != reviewed_source: + raise Refused('owner source changed during binding') + if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != ( + desc.step, desc.owner, desc.actor, desc.lane): + raise Refused('registration invocation boundary differs') + if admitted.operation != 'admission' or admitted.attempt_id: + raise Refused('expected admission invocation') + if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case: + raise Refused('case source pin differs') + # Pins.adapter is the descriptor digest, not the owner source digest. + descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + if admitted.pins.adapter != digest(descriptor_bytes): + raise Refused('descriptor pin differs') + if not all(isinstance(x, str) and 0 < len(x) <= 192 for x in + (expected_uid, expected_resource_version, case_acceptance)): + raise Refused('bounded attended binding required') + if digest(case_acceptance.encode()) != admitted.pins.acceptance: + raise Refused('acceptance evidence binding differs') + if not all(callable(x) for x in (publish, authenticate, revoke_session)): + raise Refused('owner evidence and revocation capabilities required') + with contained_output(): + case = yaml.safe_load(case_source) + client = helper.client_from_case(case) + if admitted.target != client['clientId']: + raise Refused('case target differs') + check_config = Path(check_config) + records = {} + creation = None + + def binding(inv, operation): + if (replace(inv, attempt_id='', operation='admission') != admitted + or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)): + raise Refused('attempt binding differs') + if source_digest(check_config_digest) != reviewed_source: + raise Refused('owner source changed') + if digest(check_config.read_bytes()) != check_config_digest: + raise Refused('check-config binary changed') + + def execute(inv, operation, created=()): + nonlocal creation + try: + with contained_output(): + binding(inv, 'run' if operation == 'add' else 'rollback') + uid, revision = expected_uid, expected_resource_version + if operation == 'remove': + if (created != ('keycape-client',) or creation is None + or not verify(*creation)): + raise Refused('no authenticated case-created client') + original_payload = json.loads(records[creation[1].evidence][1]) + uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version'] + args = argparse.Namespace(mode='apply', operation=operation, + expected_uid=uid, expected_resource_version=revision, + case_acceptance=case_acceptance, check_config=str(check_config)) + receipt = helper.run(args, client) + # The helper has completed CAS, whole-data readback and preservation. + # Never retain check-config diagnostics or arbitrary helper fields. + if not (receipt['schema'] == 'netkingdom.keycape-client.v1' + and receipt['target'] == 'sso/keycape-config' + and receipt['client_id'] == admitted.target + and receipt['mode'] == 'apply' and receipt['operation'] == operation + and receipt['values_emitted'] is False + and type(receipt['changed']) is bool + and receipt['unrelated_config_bytes_preserved'] is True + and receipt['other_secret_data_unchanged'] is True + and receipt['before'] == {'uid': uid, 'resource_version': revision} + and receipt['after']['uid'] == uid + and receipt['check_config']['proposed']['valid'] is True): + raise Refused('owner readback contract differs') + after = receipt['after'] + if (set(after) != {'uid', 'resource_version'} or + not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())): + raise Refused('owner revision contract differs') + changed = receipt['changed'] + outcome = (Outcome.ROLLED_BACK if operation == 'remove' else + Outcome.SUCCESS if changed else Outcome.NO_CHANGE) + objects = ('keycape-client',) if operation == 'add' and changed else () + finally: + # Even helper failure must end the parent session. Failures propagate + # contained to the kernel and become uncertain, never safe to replay. + with contained_output(): + revocation = revoke_session(inv) + if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation): + raise Refused('session revocation evidence missing') + payload = json.dumps({'schema': 'netkingdom.keycape-registration.attempt.v1', + 'invocation': asdict(inv), 'source': reviewed_source, + 'outcome': outcome.value, 'created': objects, 'before': receipt['before'], + 'after': after, 'session_revocation': revocation}, + sort_keys=True, separators=(',', ':')).encode() + with contained_output(): + evidence = publish(payload) + if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records: + raise Refused('owner evidence identifier invalid or reused') + result = OwnerResult(outcome, evidence, objects, True, revocation, + attempt_id=inv.attempt_id, operation=inv.operation) + records[evidence] = (inv, payload, result) + if operation == 'add' and changed: + creation = (inv, result) + elif operation == 'remove': + creation = None + return result + + def verify(inv, result): + record = records.get(result.evidence) if type(result) is OwnerResult else None + if record is None or record[0] != inv or record[2] != result: + return False + with contained_output(): + return authenticate(result.evidence, record[1]) is True + + return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'), + verify, lambda inv, created: execute(inv, 'remove', created)) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/registration-adapter.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/registration-adapter.md new file mode 100644 index 00000000..1ace7940 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/registration-adapter.md @@ -0,0 +1,59 @@ +# Attended registration adapter candidate + +Exercise status: unexercised + +This is a source-only, injection-only adapter for the existing Warden closed +kernel. It installs no command, runtime dispatcher, credential provider, evidence +trust root or new service. NetKingdom type/case status and bytes remain unchanged. +The generated descriptor is a proposal; it is not owner acknowledgment. + +`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply +mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/ +catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter +and its complete local helper import closure together with the exact reviewed +`check-config` executable digest. Changing that digest changes the descriptor +source pin and descriptor bytes, requiring independently reviewed admission. +The fixed executable is rehashed before each attempt. The caller constructs the +proposed invocation from exact source inputs, installs the fixed adapter once, +and calls normal `Executor.admit`; independently derived admission must match. +No callable replacement after admission is required or supported by this recipe. + +The operation uses existing cluster/issuer checks, byte-preserving merge, +Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS +and whole-data readback. Only closed metadata is passed to the injected durable +owner publisher. The owner authenticator must independently authenticate those +exact bytes. The session-revocation capability must end the same attempt's +parent session; Warden separately verifies its exact revocation evidence. +These capabilities are owner-selected callables, never case-supplied commands. +No implementation of these native capabilities is supplied or implied here. + +An identical existing client returns no change and creates no rollback authority. +Removal requires this process's independently authenticated successful creation +receipt, exact created-object label, unchanged postcreation UID/resourceVersion, +and the original exact client record. External drift, a lost readback, receipt +publication/revocation failure or process loss requires owner reconciliation. +No uncertainty is labeled retry-safe. No automatic reconciliation is installed. +Rollback after restart intentionally remains unavailable until an owner supplies +independently authenticated durable creation provenance and reviews recovery. + +The source tests execute actual maintained helper CAS/readback against synthetic +Kubernetes objects. Three checks exercise actual Warden admission/run/rollback +and uncertainty transitions with synthetic owner trust. Warden d97652a's exact +published producer source pins apply unchanged through actual BrowserInputs. +This is a source compatibility fixture, not acknowledgment, owner provenance, +normal native admission or deployment acceptance. + +Remaining work belongs in live work records before T12 closure: + +- Warden owns global evidence admission and exact native evidence/session + capabilities. Published d97652a refreshes BrowserInputs producer pins for + NetKingdom 015694ee; that source assent does not attest this new adapter. +- NetKingdom owns separately reviewed descriptor/type pin selection, the env + binding/config/restart steps, and group ensure/revoke provenance. Current group + ensure does not report newly added memberships; current revoke can remove + memberships that predate the case. Wrapping these as reversible creation would + be unsound. Preserve them until a reviewed created-only contract exists. +- Platform owns structural/mint/broker and secret delivery evidence; Apps owns + actual consumer login and group enforcement. No assent is supplied here. +- Attended use still requires existing owner authorization, current caller + identity, native source/config/image pins, native receipts and cleanup proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py new file mode 100644 index 00000000..98ed1cee --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/sso-mfa/k8s/keycape/test_keycape_registration_adapter.py @@ -0,0 +1,289 @@ +"""Actual helper/CAS execution against synthetic Kubernetes objects only.""" +from dataclasses import asdict, replace +import copy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from warden.case_run import Invocation, digest +from warden.case_receipt_state import Outcome, Pins, Refused +import yaml + +ROOT = Path(__file__).parent +spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py') +a = importlib.util.module_from_spec(spec) +spec.loader.exec_module(a) +spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py') +f = importlib.util.module_from_spec(spec) +spec.loader.exec_module(f) + + +class RegistrationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.binary = Path(self.temp.name) / 'keycape' + self.binary.write_bytes(b'synthetic checker binary') + self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1', + 'parameters': f.FORGEJO_PARAMS}}).encode() + desc = a.descriptor(digest(self.binary.read_bytes())) + self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode() + pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes), + digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet')) + self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', pins) + self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run') + self.secret, self.raw = f.fixture() + self.effects = [] + self.evidence = {} + self.revocations = [] + self.kw = dict(admitted=self.admitted, case_source=self.case, + expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test', + check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()), + publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload, + revoke_session=self.revoke) + self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True}) + self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube) + self.check_patch.start() + self.kube_patch.start() + self.addCleanup(self.check_patch.stop) + self.addCleanup(self.kube_patch.stop) + self.addCleanup(self.temp.cleanup) + + def publish(self, payload): + ref = 'owner-evidence:attempt-' + str(len(self.evidence)) + self.evidence[ref] = payload + return ref + + def revoke(self, inv): + self.revocations.append(inv) + return 'session-revoked:' + inv.operation + ':' + inv.attempt_id + + def kube(self, args, payload=None): + if args[:3] == ['get', 'namespace', 'kube-system']: + return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}} + if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + return copy.deepcopy(self.secret) + if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']: + return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', + 'env': [a.helper.env_binding(f.FORGEJO)]}]}}}} + if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']: + self.assertEqual(payload[:2], [ + {'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']}, + {'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}]) + self.effects.append(copy.deepcopy(payload)) + self.secret['data']['config.yaml'] = payload[2]['value'] + self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1) + return copy.deepcopy(self.secret) + raise AssertionError('unexpected tooling operation') + + def test_actual_add_and_created_only_rollback_preserve_original_bytes(self): + adapter = a.build(**self.kw) + original = copy.deepcopy(self.secret['data']) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.SUCCESS) + self.assertEqual(result.created, ('keycape-client',)) + self.assertTrue(adapter.verify(self.inv, result)) + undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created) + self.assertEqual(undone.outcome, Outcome.ROLLED_BACK) + self.assertEqual(self.secret['data'], original) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + for payload in self.evidence.values(): + self.assertNotIn(b'synthetic-marker', payload) + self.assertNotIn(b'config.yaml', payload) + self.assertNotIn(b'signing-key', payload) + + def test_identical_client_is_no_change_and_cannot_be_removed(self): + self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO]) + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertEqual(result.outcome, Outcome.NO_CHANGE) + self.assertEqual(result.created, ()) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',)) + self.assertFalse(self.effects) + + def test_existing_drift_refused_and_session_revoked(self): + self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')]) + with self.assertRaises(Refused): + a.build(**self.kw).invoke(self.inv) + self.assertFalse(self.effects) + self.assertEqual(self.revocations, [self.inv]) + + def test_invocation_rebinding_refused_before_tooling(self): + for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'}, + {'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''}, + {'pins': replace(self.inv.pins, case=digest(b'other'))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**self.kw).invoke(replace(self.inv, **changes)) + self.assertFalse(self.effects) + + def test_initial_case_descriptor_and_target_pins_are_required(self): + for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')}, + {'admitted': replace(self.admitted, actor='agent')}, + {'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}): + with self.subTest(changes=changes), self.assertRaises(Refused): + a.build(**dict(self.kw, **changes)) + self.assertFalse(self.effects) + + def test_binary_or_source_drift_refused_before_tooling(self): + adapter = a.build(**self.kw) + self.binary.write_bytes(b'changed') + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.binary.write_bytes(b'synthetic checker binary') + with patch.object(a, 'source_digest', return_value=digest(b'changed')): + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self): + substitute = Path(self.temp.name) / 'other-checker' + substitute.write_bytes(b'caller-selected executable') + with self.assertRaisesRegex(Refused, 'descriptor pin differs'): + a.build(**dict(self.kw, check_config=substitute, + check_config_digest=digest(substitute.read_bytes()))) + self.assertFalse(self.effects) + + def test_stale_revision_refuses_mutation(self): + adapter = a.build(**self.kw) + self.secret['metadata']['resourceVersion'] = '8' + with self.assertRaises(Refused): + adapter.invoke(self.inv) + self.assertFalse(self.effects) + + def test_rollback_requires_exact_provenance_and_postcreation_revision(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback') + for created in ((), ('other',), ('keycape-client', 'other')): + with self.subTest(created=created), self.assertRaises(Refused): + adapter.rollback(rollback, created) + self.secret['metadata']['resourceVersion'] = '9' + with self.assertRaises(Refused): + adapter.rollback(rollback, result.created) + self.assertEqual(len(self.effects), 1) + + def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self): + adapter = a.build(**self.kw) + result = adapter.invoke(self.inv) + self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result)) + self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False))) + self.evidence[result.evidence] = b'forged' + self.assertFalse(adapter.verify(self.inv, result)) + with self.assertRaises(Refused): + adapter.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertEqual(len(self.effects), 1) + + def test_process_loss_does_not_reconstruct_created_authority(self): + result = a.build(**self.kw).invoke(self.inv) + fresh = a.build(**self.kw) + with self.assertRaises(Refused): + fresh.rollback(replace(self.inv, operation='rollback'), result.created) + self.assertIsNone(fresh.reconcile) + self.assertEqual(len(self.effects), 1) + + def test_failed_readback_is_uncertain_and_never_provides_creation(self): + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + adapter = a.build(**self.kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertEqual(len(self.effects), 1) + self.assertFalse(self.evidence) + self.assertEqual(self.revocations, [self.inv]) + + def test_revocation_or_publication_failure_cannot_produce_verified_success(self): + for capability in ('publish', 'revoke_session'): + self.secret, _ = f.fixture() + with self.subTest(capability=capability): + kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))}) + adapter = a.build(**kw) + with self.assertRaisesRegex(Refused, '^owner callback failed$'): + adapter.invoke(self.inv) + self.assertFalse(self.evidence) + + def kernel_fixture(self): + # Fixture-only proposed type/case; canonical files are never edited. + # Warden's published producer pins apply unchanged. Owner trust and + # proposed descriptor/type/case are fixture-only, never native admission. + import warden.case_inputs as inputs + from warden.case_run import Executor, Journal + root = ROOT.parents[2] + case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes()) + case['spec']['status'] = 'materialized' + case_source = yaml.safe_dump(case).encode() + typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes()) + typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)} + type_source = yaml.safe_dump(typ).encode() + catalog = b'fixture-only-catalog' + sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human', + 'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode() + pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes), + digest(catalog), digest(b'fixture-acceptance'), digest(sheet)) + invocation = replace(self.admitted, pins=pins) + adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source, + case_acceptance='fixture-acceptance')) + def fixture_verifier(purpose, source, evidence): + if purpose.startswith('session-revocation:'): + _, lane, operation, attempt = purpose.split(':') + return source == digest(case_source) and lane == invocation.lane and evidence == ( + 'session-revoked:' + operation + ':' + attempt) + return evidence in {'fixture-acceptance', 'fixture-materialization', + 'fixture-descriptor', 'fixture-session'} + executor = Executor(Journal(Path(self.temp.name) / 'private'), + {adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root)) + admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog, + principal='founder', actor='human', lane=invocation.lane, + acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization', + descriptor_evidence='fixture-descriptor', session_evidence='fixture-session') + self.assertEqual(admitted[1], invocation) + return executor, admitted + + def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self): + executor, admitted = self.kernel_fixture() + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.SUCCESS) + self.assertEqual(result.last.created, ('keycape-client',)) + undone = executor.rollback(*admitted) + self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK) + self.assertEqual(len(self.effects), 2) + self.assertEqual(len(self.revocations), 2) + + def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self): + executor, admitted = self.kernel_fixture() + original_kube = self.kube + def lost(args, payload=None): + if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']: + raise RuntimeError('synthetic sensitive detail') + return original_kube(args, payload) + with patch.object(a.helper.lane, 'kubectl', side_effect=lost): + result = executor.run(*admitted) + self.assertEqual(result.last.outcome, Outcome.UNCERTAIN) + self.assertFalse(result.last.created) + with self.assertRaises(Refused): + executor.run(*admitted, retry=True) + with self.assertRaises(Refused): + executor.rollback(*admitted) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 1) + + def test_kernel_external_revision_change_retains_uncertain_rollback(self): + executor, admitted = self.kernel_fixture() + self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS) + self.secret['metadata']['resourceVersion'] = '9' + self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN) + self.assertEqual(len(self.effects), 1) + self.assertEqual(len(self.revocations), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/workplans/NK-WP-0048-security-case-types.md b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/workplans/NK-WP-0048-security-case-types.md new file mode 100644 index 00000000..ab541c64 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/postimages/workplans/NK-WP-0048-security-case-types.md @@ -0,0 +1,736 @@ +--- +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-16" +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, 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 +``` + +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. Exact root review/publication receipts are required on integration. + +## T14 — Preserve created-only provenance for group ensure and revoke + +```task +id: NK-WP-0048-T14 +status: todo +priority: high +``` + +Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner +ensure/revoke contract with independently retained prestate and exact newly +created groups/memberships before exposing reversible owner callbacks. Current +ensure returns no membership-creation inventory; current revoke may remove a +membership that predated this case. Completion requires actual helper-backed +positive/refusal fixtures, all validation before mutation, per-attempt evidence +and revocation, readback, ambiguity handling, and rollback that preserves every +pre-existing/shared group and membership. No existing endpoint or service token +is treated as new authorization. + +## T15 — Define authenticated recovery for lost registration process state + +```task +id: NK-WP-0048-T15 +status: todo +priority: high +``` + +Owner: NetKingdom for registration state, with Warden owning kernel evidence +admission. Next action: select the exact existing owner evidence holder/readback +contract and define authenticated lookup for the original creation attempt. +Completion requires restart/lost-receipt fixtures that bind case/source/principal, +original created-only provenance and current native revision before any undo; +uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses +post-restart rollback and installs no reconciliation callback. A new broker or +unverified JSON receipt is not a recovery contract. Actual evidence-holder +acceptance and attended recovery remain separately authorized native work. + +## T16 — Select and review the actual machine descriptor composition + +```task +id: NK-WP-0048-T16 +status: todo +priority: high +``` + +Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden +independently owns global provenance/admission. Next action: identify the exact +reviewed checker executable artifact and native owner evidence/session callback +contracts, then assemble the complete step descriptor set and a proposed explicit +type version selection. T13's retained descriptor pins a synthetic checker only. +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. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/proposed-descriptor.json b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/proposed-descriptor.json new file mode 100644 index 00000000..4d6ad4a7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/proposed-descriptor.json @@ -0,0 +1 @@ +{"actor":"human","adapter":"net-kingdom/keycape-registration@v1","adapter_source":"sha256:284e1d70b56461c6190ea634dea63d1b55890d657df8379f2459dfc882260142","allowed_created":["keycape-client"],"effect":"change","lane":"key-cape-oidc-login","owner":"net-kingdom","retry_safe":false,"revocation_scopes":["parent-session"],"step":"register-client","target_parameter":"client_id","version":"warden.case.execution.v1"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/tests.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/tests.txt new file mode 100644 index 00000000..18da8c87 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/tests.txt @@ -0,0 +1,41 @@ +test_access_groups_require_groups_scope (test_keycape_client.ClientRecordTests.test_access_groups_require_groups_scope) ... ok +test_client_from_case_requires_the_type (test_keycape_client.ClientRecordTests.test_client_from_case_requires_the_type) ... ok +test_confidential_record_binds_env_secret_and_pins_mfa (test_keycape_client.ClientRecordTests.test_confidential_record_binds_env_secret_and_pins_mfa) ... ok +test_invalid_params_refused (test_keycape_client.ClientRecordTests.test_invalid_params_refused) ... ok +test_public_record_has_no_secret (test_keycape_client.ClientRecordTests.test_public_record_has_no_secret) ... ok +test_redirects_keycape_would_refuse_at_startup_are_refused (test_keycape_client.ClientRecordTests.test_redirects_keycape_would_refuse_at_startup_are_refused) ... ok +test_vergabe_case_reproduces_the_hand_built_registration (test_keycape_client.ClientRecordTests.test_vergabe_case_reproduces_the_hand_built_registration) ... ok +test_add_appends_only_this_client_and_preserves_bytes (test_keycape_client.MergeTests.test_add_appends_only_this_client_and_preserves_bytes) ... ok +test_identical_is_noop_and_drift_refused (test_keycape_client.MergeTests.test_identical_is_noop_and_drift_refused) ... ok +test_removal_removes_exactly_this_client (test_keycape_client.MergeTests.test_removal_removes_exactly_this_client) ... ok +test_shared_secret_ref_and_duplicates_refused (test_keycape_client.MergeTests.test_shared_secret_ref_and_duplicates_refused) ... ok +test_apply_needs_case_acceptance (test_keycape_client.RunTests.test_apply_needs_case_acceptance) ... ok +test_apply_needs_env_binding_for_confidential_clients (test_keycape_client.RunTests.test_apply_needs_env_binding_for_confidential_clients) ... ok +test_apply_needs_keycape_check_config (test_keycape_client.RunTests.test_apply_needs_keycape_check_config) ... ok +test_apply_patches_reads_back_and_records_acceptance (test_keycape_client.RunTests.test_apply_patches_reads_back_and_records_acceptance) ... ok +test_apply_refuses_a_changed_revision (test_keycape_client.RunTests.test_apply_refuses_a_changed_revision) ... ok +test_apply_refuses_a_registry_keycape_would_refuse (test_keycape_client.RunTests.test_apply_refuses_a_registry_keycape_would_refuse) ... ok +test_inspect_changes_nothing_and_emits_no_values (test_keycape_client.RunTests.test_inspect_changes_nothing_and_emits_no_values) ... ok +test_inspect_reports_live_and_proposed_checks_with_deployment_env (test_keycape_client.RunTests.test_inspect_reports_live_and_proposed_checks_with_deployment_env) ... ok +test_main_contains_failures (test_keycape_client.RunTests.test_main_contains_failures) ... ok +test_actual_add_and_created_only_rollback_preserve_original_bytes (test_keycape_registration_adapter.RegistrationTests.test_actual_add_and_created_only_rollback_preserve_original_bytes) ... ok +test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust (test_keycape_registration_adapter.RegistrationTests.test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust) ... ok +test_binary_or_source_drift_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_binary_or_source_drift_refused_before_tooling) ... ok +test_existing_drift_refused_and_session_revoked (test_keycape_registration_adapter.RegistrationTests.test_existing_drift_refused_and_session_revoked) ... ok +test_failed_readback_is_uncertain_and_never_provides_creation (test_keycape_registration_adapter.RegistrationTests.test_failed_readback_is_uncertain_and_never_provides_creation) ... ok +test_identical_client_is_no_change_and_cannot_be_removed (test_keycape_registration_adapter.RegistrationTests.test_identical_client_is_no_change_and_cannot_be_removed) ... ok +test_initial_case_descriptor_and_target_pins_are_required (test_keycape_registration_adapter.RegistrationTests.test_initial_case_descriptor_and_target_pins_are_required) ... ok +test_invocation_rebinding_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_invocation_rebinding_refused_before_tooling) ... ok +test_kernel_external_revision_change_retains_uncertain_rollback (test_keycape_registration_adapter.RegistrationTests.test_kernel_external_revision_change_retains_uncertain_rollback) ... ok +test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo (test_keycape_registration_adapter.RegistrationTests.test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo) ... ok +test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo (test_keycape_registration_adapter.RegistrationTests.test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo) ... ok +test_process_loss_does_not_reconstruct_created_authority (test_keycape_registration_adapter.RegistrationTests.test_process_loss_does_not_reconstruct_created_authority) ... ok +test_revocation_or_publication_failure_cannot_produce_verified_success (test_keycape_registration_adapter.RegistrationTests.test_revocation_or_publication_failure_cannot_produce_verified_success) ... ok +test_rollback_requires_exact_provenance_and_postcreation_revision (test_keycape_registration_adapter.RegistrationTests.test_rollback_requires_exact_provenance_and_postcreation_revision) ... ok +test_stale_revision_refuses_mutation (test_keycape_registration_adapter.RegistrationTests.test_stale_revision_refuses_mutation) ... ok +test_substituting_caller_selected_checker_under_same_descriptor_is_refused (test_keycape_registration_adapter.RegistrationTests.test_substituting_caller_selected_checker_under_same_descriptor_is_refused) ... ok + +---------------------------------------------------------------------- +Ran 36 tests in 1.363s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/verify_candidate.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/verify_candidate.py new file mode 100644 index 00000000..de219faf --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/reviewed-r3/verify_candidate.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Reconstruct isolated source candidate and exercise synthetic fixtures only.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +p = argparse.ArgumentParser() +p.add_argument('--owner-root', type=Path, default=Path('/home/worsch/net-kingdom')) +p.add_argument('--warden-root', type=Path, default=Path('/home/worsch/ops-warden')) +args = p.parse_args() +packet = Path(__file__).resolve().parent +manifest = json.loads((packet / 'manifest.json').read_text()) +with tempfile.TemporaryDirectory(prefix='nk-registration-review-') as directory: + candidate = Path(directory) + for relative, expected in manifest['fixture_dependencies'].items(): + source = args.owner_root / relative + data = source.read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('fixture dependency drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + for relative, expected in manifest['postimages'].items(): + data = (packet / 'postimages' / relative).read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('candidate source drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + result = subprocess.run([sys.executable, '-m', 'unittest', 'discover', '-s', + str(candidate / 'sso-mfa/k8s/keycape'), '-p', 'test_keycape*.py', '-v'], + env=dict(os.environ, PYTHONPATH=str(args.warden_root / 'src'))) + raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_final_r3.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_final_r3.py new file mode 100644 index 00000000..97c3fde4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_final_r3.py @@ -0,0 +1,25 @@ +from pathlib import Path +import hashlib,json,subprocess +packet=Path(__file__).resolve().parent;owner=Path('/home/worsch/net-kingdom');backup=packet/'publication-backup' +git=lambda *args:subprocess.check_output(['git','-C',str(owner),*args]) +# Verify original staged/worktree brief bytes before changing its index entry. +if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief race') +if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('working brief race') +subprocess.run(['git','-C',str(owner),'restore','--staged','.custodian-brief.md'],check=True) +try: + result=subprocess.run(['statehub','fix-consistency','--repo','net-kingdom','--repo-path',str(owner),'--json'],capture_output=True,text=True,cwd=owner) + (packet/'normal-final.stdout.json').write_text(result.stdout) + (packet/'normal-final.stderr.txt').write_text(result.stderr) + (packet/'normal-final.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n') + print('statehub exit:',result.returncode) + print(result.stdout[-6000:]) + print(result.stderr[-1000:]) +finally: + # Restore exact pre-existing stage blob, independently of any auto-brief commit. + record=(backup/'index-record.txt').read_text().split() + original_blob=record[1];mode=record[0] + subprocess.run(['git','-C',str(owner),'update-index','--cacheinfo',f'{mode},{original_blob},.custodian-brief.md'],check=True) + (owner/'.custodian-brief.md').write_bytes((backup/'brief.worktree').read_bytes()) + if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief restoration failed') + if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('brief restoration failed') + print('Exact original staged and working brief bytes restored.') diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_r3.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_r3.py new file mode 100644 index 00000000..774b7a61 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_r3.py @@ -0,0 +1,25 @@ +from pathlib import Path +import hashlib,json,subprocess +packet=Path(__file__).resolve().parent;owner=Path('/home/worsch/net-kingdom');backup=packet/'publication-backup' +git=lambda *args:subprocess.check_output(['git','-C',str(owner),*args]) +# Verify original staged/worktree brief bytes before changing its index entry. +if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief race') +if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('working brief race') +subprocess.run(['git','-C',str(owner),'restore','--staged','.custodian-brief.md'],check=True) +try: + result=subprocess.run(['statehub','fix-consistency','--repo','net-kingdom','--repo-path',str(owner),'--no-writeback','--json'],capture_output=True,text=True,cwd=owner) + (packet/'normal-sync.stdout.json').write_text(result.stdout) + (packet/'normal-sync.stderr.txt').write_text(result.stderr) + (packet/'normal-sync.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n') + print('statehub exit:',result.returncode) + print(result.stdout[-6000:]) + print(result.stderr[-1000:]) +finally: + # Restore exact pre-existing stage blob, independently of any auto-brief commit. + record=(backup/'index-record.txt').read_text().split() + original_blob=record[1];mode=record[0] + subprocess.run(['git','-C',str(owner),'update-index','--cacheinfo',f'{mode},{original_blob},.custodian-brief.md'],check=True) + (owner/'.custodian-brief.md').write_bytes((backup/'brief.worktree').read_bytes()) + if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief restoration failed') + if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('brief restoration failed') + print('Exact original staged and working brief bytes restored.') diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_writeback_r3.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_writeback_r3.py new file mode 100644 index 00000000..0c5233d5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/sync_owner_writeback_r3.py @@ -0,0 +1,25 @@ +from pathlib import Path +import hashlib,json,subprocess +packet=Path(__file__).resolve().parent;owner=Path('/home/worsch/net-kingdom');backup=packet/'publication-backup' +git=lambda *args:subprocess.check_output(['git','-C',str(owner),*args]) +# Verify original staged/worktree brief bytes before changing its index entry. +if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief race') +if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('working brief race') +subprocess.run(['git','-C',str(owner),'restore','--staged','.custodian-brief.md'],check=True) +try: + result=subprocess.run(['statehub','fix-consistency','--repo','net-kingdom','--repo-path',str(owner),'--json'],capture_output=True,text=True,cwd=owner) + (packet/'normal-writeback.stdout.json').write_text(result.stdout) + (packet/'normal-writeback.stderr.txt').write_text(result.stderr) + (packet/'normal-writeback.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n') + print('statehub exit:',result.returncode) + print(result.stdout[-6000:]) + print(result.stderr[-1000:]) +finally: + # Restore exact pre-existing stage blob, independently of any auto-brief commit. + record=(backup/'index-record.txt').read_text().split() + original_blob=record[1];mode=record[0] + subprocess.run(['git','-C',str(owner),'update-index','--cacheinfo',f'{mode},{original_blob},.custodian-brief.md'],check=True) + (owner/'.custodian-brief.md').write_bytes((backup/'brief.worktree').read_bytes()) + if git('show',':.custodian-brief.md') != (backup/'brief.index').read_bytes():raise SystemExit('staged brief restoration failed') + if (owner/'.custodian-brief.md').read_bytes() != (backup/'brief.worktree').read_bytes():raise SystemExit('brief restoration failed') + print('Exact original staged and working brief bytes restored.') diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/tests.txt b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/tests.txt new file mode 100644 index 00000000..18da8c87 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/tests.txt @@ -0,0 +1,41 @@ +test_access_groups_require_groups_scope (test_keycape_client.ClientRecordTests.test_access_groups_require_groups_scope) ... ok +test_client_from_case_requires_the_type (test_keycape_client.ClientRecordTests.test_client_from_case_requires_the_type) ... ok +test_confidential_record_binds_env_secret_and_pins_mfa (test_keycape_client.ClientRecordTests.test_confidential_record_binds_env_secret_and_pins_mfa) ... ok +test_invalid_params_refused (test_keycape_client.ClientRecordTests.test_invalid_params_refused) ... ok +test_public_record_has_no_secret (test_keycape_client.ClientRecordTests.test_public_record_has_no_secret) ... ok +test_redirects_keycape_would_refuse_at_startup_are_refused (test_keycape_client.ClientRecordTests.test_redirects_keycape_would_refuse_at_startup_are_refused) ... ok +test_vergabe_case_reproduces_the_hand_built_registration (test_keycape_client.ClientRecordTests.test_vergabe_case_reproduces_the_hand_built_registration) ... ok +test_add_appends_only_this_client_and_preserves_bytes (test_keycape_client.MergeTests.test_add_appends_only_this_client_and_preserves_bytes) ... ok +test_identical_is_noop_and_drift_refused (test_keycape_client.MergeTests.test_identical_is_noop_and_drift_refused) ... ok +test_removal_removes_exactly_this_client (test_keycape_client.MergeTests.test_removal_removes_exactly_this_client) ... ok +test_shared_secret_ref_and_duplicates_refused (test_keycape_client.MergeTests.test_shared_secret_ref_and_duplicates_refused) ... ok +test_apply_needs_case_acceptance (test_keycape_client.RunTests.test_apply_needs_case_acceptance) ... ok +test_apply_needs_env_binding_for_confidential_clients (test_keycape_client.RunTests.test_apply_needs_env_binding_for_confidential_clients) ... ok +test_apply_needs_keycape_check_config (test_keycape_client.RunTests.test_apply_needs_keycape_check_config) ... ok +test_apply_patches_reads_back_and_records_acceptance (test_keycape_client.RunTests.test_apply_patches_reads_back_and_records_acceptance) ... ok +test_apply_refuses_a_changed_revision (test_keycape_client.RunTests.test_apply_refuses_a_changed_revision) ... ok +test_apply_refuses_a_registry_keycape_would_refuse (test_keycape_client.RunTests.test_apply_refuses_a_registry_keycape_would_refuse) ... ok +test_inspect_changes_nothing_and_emits_no_values (test_keycape_client.RunTests.test_inspect_changes_nothing_and_emits_no_values) ... ok +test_inspect_reports_live_and_proposed_checks_with_deployment_env (test_keycape_client.RunTests.test_inspect_reports_live_and_proposed_checks_with_deployment_env) ... ok +test_main_contains_failures (test_keycape_client.RunTests.test_main_contains_failures) ... ok +test_actual_add_and_created_only_rollback_preserve_original_bytes (test_keycape_registration_adapter.RegistrationTests.test_actual_add_and_created_only_rollback_preserve_original_bytes) ... ok +test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust (test_keycape_registration_adapter.RegistrationTests.test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust) ... ok +test_binary_or_source_drift_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_binary_or_source_drift_refused_before_tooling) ... ok +test_existing_drift_refused_and_session_revoked (test_keycape_registration_adapter.RegistrationTests.test_existing_drift_refused_and_session_revoked) ... ok +test_failed_readback_is_uncertain_and_never_provides_creation (test_keycape_registration_adapter.RegistrationTests.test_failed_readback_is_uncertain_and_never_provides_creation) ... ok +test_identical_client_is_no_change_and_cannot_be_removed (test_keycape_registration_adapter.RegistrationTests.test_identical_client_is_no_change_and_cannot_be_removed) ... ok +test_initial_case_descriptor_and_target_pins_are_required (test_keycape_registration_adapter.RegistrationTests.test_initial_case_descriptor_and_target_pins_are_required) ... ok +test_invocation_rebinding_refused_before_tooling (test_keycape_registration_adapter.RegistrationTests.test_invocation_rebinding_refused_before_tooling) ... ok +test_kernel_external_revision_change_retains_uncertain_rollback (test_keycape_registration_adapter.RegistrationTests.test_kernel_external_revision_change_retains_uncertain_rollback) ... ok +test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo (test_keycape_registration_adapter.RegistrationTests.test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo) ... ok +test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo (test_keycape_registration_adapter.RegistrationTests.test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo) ... ok +test_process_loss_does_not_reconstruct_created_authority (test_keycape_registration_adapter.RegistrationTests.test_process_loss_does_not_reconstruct_created_authority) ... ok +test_revocation_or_publication_failure_cannot_produce_verified_success (test_keycape_registration_adapter.RegistrationTests.test_revocation_or_publication_failure_cannot_produce_verified_success) ... ok +test_rollback_requires_exact_provenance_and_postcreation_revision (test_keycape_registration_adapter.RegistrationTests.test_rollback_requires_exact_provenance_and_postcreation_revision) ... ok +test_stale_revision_refuses_mutation (test_keycape_registration_adapter.RegistrationTests.test_stale_revision_refuses_mutation) ... ok +test_substituting_caller_selected_checker_under_same_descriptor_is_refused (test_keycape_registration_adapter.RegistrationTests.test_substituting_caller_selected_checker_under_same_descriptor_is_refused) ... ok + +---------------------------------------------------------------------- +Ran 36 tests in 1.363s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/verify_candidate.py b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/verify_candidate.py new file mode 100644 index 00000000..de219faf --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/verify_candidate.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Reconstruct isolated source candidate and exercise synthetic fixtures only.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +p = argparse.ArgumentParser() +p.add_argument('--owner-root', type=Path, default=Path('/home/worsch/net-kingdom')) +p.add_argument('--warden-root', type=Path, default=Path('/home/worsch/ops-warden')) +args = p.parse_args() +packet = Path(__file__).resolve().parent +manifest = json.loads((packet / 'manifest.json').read_text()) +with tempfile.TemporaryDirectory(prefix='nk-registration-review-') as directory: + candidate = Path(directory) + for relative, expected in manifest['fixture_dependencies'].items(): + source = args.owner_root / relative + data = source.read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('fixture dependency drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + for relative, expected in manifest['postimages'].items(): + data = (packet / 'postimages' / relative).read_bytes() + if 'sha256:' + hashlib.sha256(data).hexdigest() != expected: + raise SystemExit('candidate source drift: ' + relative) + target = candidate / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + result = subprocess.run([sys.executable, '-m', 'unittest', 'discover', '-s', + str(candidate / 'sso-mfa/k8s/keycape'), '-p', 'test_keycape*.py', '-v'], + env=dict(os.environ, PYTHONPATH=str(args.warden_root / 'src'))) + raise SystemExit(result.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/README.md b/docs/changes/netkingdom-target-2026-10-16/browser-case/README.md new file mode 100644 index 00000000..1d316df5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/README.md @@ -0,0 +1,71 @@ +# Browser-case source implementation — NK-WP-0048-T06 + +Implemented in isolated `/tmp/netkingdom-browser-target-20261016` from NetKingdom +HEAD `994d11c869daf9624ac6da2adfdaeb86d8cdf537`; canonical owner checkout untouched. +`implementation.patch` changes only owner source, tool documentation, integration +tests and the existing NK-WP-0048 workplan. The separate dirty-baseline patch +retains the two pre-existing generated tracked diffs and is **not** part of the +implementation; no untracked owner files or credentials were copied. + +The existing validator now offers `check-evidence`, using the same declarations, +composer, explicit type-version selection and normal admission. It checks real +Mason structural Request source/projection/inventory and reads real Warden +SQLite journal rows with read-only mode and bounded metadata. Exact descriptor +pins, owner, latest outcome, creation scope, attempt and revocation fingerprints +must agree. Unknown fields, forged native flags, stale source and missing proof +fingerprints refuse. The 19 new integration checks call real Mason.prepare and +Warden.Journal.save; final full scoped result is **103 passed**. `validation.json` +records the check details and isolated sibling-layout fix for existing suites. + +This is a compatibility preflight, not authenticated completion. Existing owner +artifacts cannot carry portable authenticated native receipts: Mason Request is +source-only and Warden journal preserves fingerprints. The checker therefore +never claims native complete or permits execution, even with matching artifacts. +Exit 0 means supplied source artifacts match; it is not an admission token. +Normal 1.1 and 1.2 commands still reject acknowledged/unimplemented types. The +saved preflight JSONs use explicit `--allow-unimplemented` source preparation. + +## Reachable next owner implementation + +1. Warden must adapt browser metadata admission before a native attempt. Its + `src/warden/case_run.py:421` loop currently accepts only public/operational + case parameters. Actual 1.1 `redirect_uris`, `allowed_scopes`, `client_type` + and `mfa_required`, plus 1.2 `consumer_namespace`, are `security_sensitive`. + An actual Executor.admit test with the existing Warden fixture and only its + parameter sensitivity changed to this actual classification returns + `unknown, fixed or secret-sensitivity case parameter`. Test evidence is + `warden-admission-mismatch.json`. No dispatch occurs. The owner fix should + reuse NetKingdom's exact-version validator to validate the complete case, + distinguish security-sensitive metadata from credential material, and project + only the descriptor's accepted target fields. Do not widen the journal to raw + parameters or weaken credential/unknown/fixed-value checks. This source work + belongs to WARDEN-WP-0044-T03; this worker has not edited Warden. +2. Warden/Mason/Platform need an actual authenticated owner-evidence admission + mechanism that binds source, target, actor/lane, attempt, creation ownership + and current native observations. The existing injected fake test verifiers + and unsigned JSON are not that mechanism. NetKingdom's checker deliberately + has no trust=True callback or native admission flag. +3. Owner-accepted exact machine descriptors must be pinned through a reviewed + type revision; current 1.1/1.2 descriptive types have none. Do not hand-edit + them into acknowledged history. Mason still needs explicit 1.2 source-root + adoption and a regenerated six-object request/bundle; its current legacy + bundle is 1.1 and has a known incorrect consumer workload target. Integration + tests correct a copied fixture only, preserving the historical source. + +## Native gates retained + +Native six-object structure/CCR42 claims and created-versus-preexisting receipts; +accepted-case to materialized-case exact raw-hash binding; installed broker and +case/type/source reconciliation; separately authorized mint and two deliveries; +attended groups/env/registry and consumer configuration; actual sign-in and +consumer group denial; distinct broker-grant and parent-session revocation; +created-only rollback with UID/ownership checks and issued-token revocation or +expiry. 1.2 remains proposed-case/acknowledged-type source. T06 remains wait and +T07 retains its first-case dependency. No live or implemented-type claim follows. + +The withheld knowledge orientation was inspected: emergent-security package +0.2.6, digest `8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`. +Current NetKingdom INTENT/SCOPE/Security Companion and actual owner source seams +were read directly. No owner approvals or supervision promotion were authored. +Canonical integration, State Hub reconciliation and aggregate session progress +remain with the parent Custodian session after patch acceptance/publication. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/browser-case/implementation.patch new file mode 100644 index 00000000..356e1a27 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/implementation.patch @@ -0,0 +1,551 @@ +diff --git a/tools/security-case-types/README.md b/tools/security-case-types/README.md +index 45e1106..dc4fc91 100644 +--- a/tools/security-case-types/README.md ++++ b/tools/security-case-types/README.md +@@ -50,3 +50,48 @@ This ordinary command refuses:1.2 is acknowledged, not implemented. For source-o + The Python API is load_type(default_root, exact_reference, version_roots={"1.2": explicit_root}). Each key must be an exact major.minor and each root a Path. CLI --version-root VERSION=PATH may repeat for DIFFERENT versions. A matching explicit version selects exactly one root; a missing/wrong/ambiguous source refuses without trying the default or another revision. Versions without a mapping continue using --types. No directory discovery, highest/latest selection, status promotion or automatic owner-adapter adoption occurs. Duplicate mappings (even identical), duplicate YAML fields, mismatched source ID/version, malformed/traversal references and selected symlinks refuse. Direct case validation also requires the case reference to match the supplied type ID/version. + + An explicit root is source input, not an owner acknowledgment or grant. Callers retain exact source hashes/type/case/render in their own reviewed receipts. Existing two-argument load_type callers, including ops-mason, retain their default behavior until their owner deliberately adopts the optional input. Adapter/global provenance/native first-reference gates remain separate in NK-WP-0048-T06 and the existing ORGREF driver. ++ ++## Check existing owner evidence compatibility ++ ++The normal validator CLI now accepts `check-evidence` using the same exact type ++selection, declarations, validation and deterministic render as `render`: ++ ++```sh ++python3 tools/security-case-types/security_case_validator.py check-evidence \ ++ security-cases/forgejo-netkingdom-oidc.yaml --types security-case-types \ ++ --structure-request /path/to/mason-structure-request.json \ ++ --warden-journal /path/to/existing/journal.sqlite ++``` ++ ++Normal admission still refuses the acknowledged 1.1 and 1.2 types. Explicit ++`--allow-unimplemented` permits source preparation, with the existing ++`--version-root 1.2=security-case-type-revisions/1.2` for that version. No owner ++protocol is automatically adopted, no journal is created, and no status changes. ++ ++This is the cross-owner **compatibility preflight**, not native acceptance. It ++consumes the existing `ops-mason.case-structure-request.v1` and reads latest rows ++from the existing `warden.case.step-journal.v1` SQLite journal in read-only mode. ++Mason source/request hashes, complete current structural inventory and structural ++projection must match the fresh render. Warden case/type and type-declared machine ++descriptor pins, owners, bounded histories, created-object scope and successful ++attempt/revocation fingerprints must match. Unknown or duplicate fields, source ++substitution and missing completed-attempt fingerprints refuse. Running, uncertain, ++failed or rolled-back outcomes retain their exact meanings; an earlier success ++never overrides the latest journal row. Generated-value paths require distinct ++parent-session and broker-grant fingerprint fields. ++ ++Exit 0 means these supplied artifacts are source-compatible; it never means the ++case is complete. No artifact is required solely to render missing gates. Output ++always has `native_complete: false`, `execution.permitted: false`, and explicit ++unresolved native gates. A Mason Request has no native Observation provenance; ++a Warden journal retains fingerprints rather than portable signed/native proof. ++This command deliberately offers no trust callback, native-success flag, dispatch ++or way to promote self-authored JSON into native evidence. Actual independent ++owner evidence admission, status-hash binding, accepted machine descriptors, ++delivery, human sign-in/consumer denial, revocation and created-only rollback ++remain NK-WP-0048-T06 owner integration work. It does not mark any type implemented. ++ ++Integration tests invoke actual sibling Mason `prepare` and Warden `Journal.save` ++with metadata fixtures only. They need those owner source checkouts, and existing ++type suites need the public KeyCape request file and provider declarations beside ++the checkout. `NET_KINGDOM_ROOT` selects the source root for the 1.2 package tests. +diff --git a/tools/security-case-types/security_case_evidence.py b/tools/security-case-types/security_case_evidence.py +new file mode 100644 +index 0000000..0bf0a84 +--- /dev/null ++++ b/tools/security-case-types/security_case_evidence.py +@@ -0,0 +1,200 @@ ++"""Read-only compatibility checks for existing Mason and Warden evidence seams. ++ ++This checks real owner artifacts against a fresh NetKingdom render. These source ++protocols carry no portable authenticated native evidence. A matching request or ++journal can NEVER establish materialized/live/implemented or authorize dispatch. ++""" ++from __future__ import annotations ++ ++import hashlib ++import json ++from pathlib import Path ++import re ++import sqlite3 ++ ++import yaml ++ ++LIMIT = 262144 ++DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") ++ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,191}\Z") ++ ++ ++class EvidenceRefused(ValueError): ++ pass ++ ++ ++def sha(source): ++ return "sha256:" + hashlib.sha256(source).hexdigest() ++ ++ ++def read(path): ++ path = Path(path) ++ if path.is_symlink() or not path.is_file() or path.stat().st_size > LIMIT: ++ raise EvidenceRefused("unsafe or oversized metadata source") ++ return path.read_bytes() ++ ++ ++def _unique(pairs): ++ result = {} ++ for key, value in pairs: ++ if key in result: ++ raise EvidenceRefused("duplicate metadata field") ++ result[key] = value ++ return result ++ ++ ++def decode(source): ++ if type(source) is not bytes or len(source) > LIMIT: ++ raise EvidenceRefused("oversized metadata input") ++ try: ++ return json.loads(source, object_pairs_hook=_unique) ++ except (ValueError, UnicodeError): ++ raise EvidenceRefused("invalid metadata JSON") from None ++ ++ ++def check_structure(request, case_source, type_source, rendered): ++ """Consume ops-mason.case-structure-request.v1, not a new native flag.""" ++ fields = {"schema", "case", "case_digest", "type_ref", "type_digest", ++ "render_digest", "plan_digest", "bundle_digest", "structural_digest", ++ "reviewed_render_digest", "run_source_changed", "objects"} ++ if type(request) is not dict or set(request) != fields: ++ raise EvidenceRefused("unknown Mason structural request fields") ++ if request["schema"] != "ops-mason.case-structure-request.v1": ++ raise EvidenceRefused("unknown Mason structural request protocol") ++ for field in fields & {"case_digest", "type_digest", "render_digest", "plan_digest", ++ "bundle_digest", "structural_digest", "reviewed_render_digest"}: ++ if type(request[field]) is not str or not DIGEST.fullmatch(request[field]): ++ raise EvidenceRefused("malformed structural source pin") ++ if (request["case"], request["case_digest"], request["type_ref"], ++ request["type_digest"], request["render_digest"]) != ( ++ rendered["case"], sha(case_source), rendered["type"], sha(type_source), rendered["digest"]): ++ raise EvidenceRefused("Mason request has stale case/type/render pins") ++ if type(request["run_source_changed"]) is not bool or request["run_source_changed"] != ( ++ request["reviewed_render_digest"] != rendered["digest"]): ++ raise EvidenceRefused("Mason run drift flag disagrees with pins") ++ owners = {"openbao_policy": ("railiance-platform", "native-structure"), ++ "openbao_role": ("railiance-platform", "native-structure"), ++ "kv_path": ("railiance-platform", "layout-only"), ++ "catalog_entry": ("ops-warden", "pointer-source")} ++ expected = [] ++ for item in rendered["materialization"]: ++ if item["kind"] not in owners: ++ raise EvidenceRefused("unsupported structural owner kind") ++ owner, proof = owners[item["kind"]] ++ expected.append({"id": item["id"], "kind": item["kind"], "target": item["template"], ++ "expected_digest": sha(json.dumps(item, sort_keys=True, separators=(",", ":")).encode()), ++ "evidence_owner": owner, "proof": proof}) ++ if request["objects"] != expected: ++ raise EvidenceRefused("Mason structural inventory differs from exact render") ++ case = yaml.safe_load(case_source) ++ acc = case["spec"].get("acceptance") or {} ++ acceptance = ({"record": acc["record"], "at": acc.get("at"), ++ "by": [item.get("role") for item in acc.get("by", [])]} if acc.get("record") else None) ++ structural = {"case": rendered["case"], "type": rendered["type"], ++ "acceptance": acceptance, "materialization": rendered["materialization"]} ++ if request["structural_digest"] != sha(json.dumps(structural, sort_keys=True, separators=(",", ":")).encode()): ++ raise EvidenceRefused("Mason structural projection pin differs from current source") ++ # A request has neither Observation provenance nor StatusBinding authority. ++ return {"request_digest": sha(json.dumps(request, sort_keys=True, separators=(",", ":")).encode()), ++ "objects": len(expected), "run_source_changed": request["run_source_changed"], ++ "native_materialization_verified": False} ++ ++ ++def read_journal(path): ++ """Read bounded Warden rows through SQLite read-only mode; never create a DB.""" ++ path = Path(path) ++ if path.is_symlink() or not path.is_file() or path.stat().st_size > 16 * 1024 * 1024: ++ raise EvidenceRefused("unsafe or oversized Warden journal") ++ try: ++ connection = sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True) ++ try: ++ connection.execute("PRAGMA query_only=ON") ++ connection.execute("PRAGMA trusted_schema=OFF") ++ rows = connection.execute( ++ "SELECT seq,binding,payload FROM receipts ORDER BY seq LIMIT 1025").fetchall() ++ finally: ++ connection.close() ++ if len(rows) > 1024: ++ raise EvidenceRefused("Warden journal inventory exceeds limit") ++ latest = {} ++ for seq, binding, payload in rows: ++ if type(seq) is not int or type(binding) is not str or not ID.fullmatch(binding): ++ raise EvidenceRefused("invalid Warden journal identity") ++ if type(payload) is not str: ++ raise EvidenceRefused("invalid Warden journal payload") ++ latest[binding] = decode(payload.encode()) ++ return tuple(latest.values()) ++ except sqlite3.Error: ++ raise EvidenceRefused("invalid Warden journal database") from None ++ ++ ++def check_steps(rows, case_source, type_source, rendered): ++ """Validate latest durable Warden states; they remain local evidence only.""" ++ expected = {item["id"]: item for item in rendered["follow_through"]} ++ fields = {"step_id", "owner", "pins", "allowed_created", "retry_safe", "receipts", ++ "journal_schema", "evidence_digest", "session_revoked", "session_revocation_digest", ++ "grant_revocation_digest", "attempt_id", "operation"} ++ type_spec = yaml.safe_load(type_source)["spec"] ++ descriptors = type_spec.get("execution_descriptors", {}) ++ seen, result = set(), [] ++ for row in rows: ++ if type(row) is not dict or set(row) != fields or row["journal_schema"] != "warden.case.step-journal.v1": ++ raise EvidenceRefused("unknown Warden journal receipt fields") ++ step = row["step_id"] ++ if type(step) is not str or step not in expected or step in seen or row["owner"] != expected[step]["owner"]: ++ raise EvidenceRefused("unknown, duplicate or wrong-owner Warden step") ++ seen.add(step) ++ pins = row["pins"] ++ if type(pins) is not dict or set(pins) != {"case", "case_type", "adapter", "catalog", "acceptance", "run_sheet"}: ++ raise EvidenceRefused("unknown Warden source pins") ++ if any(type(value) is not str or not DIGEST.fullmatch(value) for value in pins.values()): ++ raise EvidenceRefused("malformed Warden source pin") ++ if (pins["case"], pins["case_type"]) != (sha(case_source), sha(type_source)): ++ raise EvidenceRefused("Warden receipt has stale case/type pins") ++ if descriptors.get(step) != pins["adapter"]: ++ raise EvidenceRefused("Warden receipt has no exact type-pinned machine descriptor") ++ history = row["receipts"] ++ if type(history) is not list or not 1 <= len(history) <= 128: ++ raise EvidenceRefused("invalid Warden receipt history") ++ outcomes = {"running", "verified-success", "verified-no-change", "failed-before-change", ++ "failed-after-change", "uncertain", "rollback-running", "verified-rolled-back", "rollback-uncertain"} ++ allowed = row["allowed_created"] ++ if type(allowed) is not list or len(allowed) > 32 or len(set(allowed)) != len(allowed) or any( ++ type(value) is not str or not ID.fullmatch(value) for value in allowed): ++ raise EvidenceRefused("invalid Warden created-object scope") ++ if type(row["retry_safe"]) is not bool: ++ raise EvidenceRefused("invalid Warden retry policy") ++ for number, receipt in enumerate(history, 1): ++ if type(receipt) is not dict or set(receipt) != {"attempt", "outcome", "created"} or ( ++ type(receipt["attempt"]) is not int or receipt["attempt"] != number or ++ receipt["outcome"] not in outcomes or type(receipt["created"]) is not list or ++ any(type(v) is not str or v not in allowed for v in receipt["created"]) or ++ len(set(receipt["created"])) != len(receipt["created"])): ++ raise EvidenceRefused("invalid Warden receipt state") ++ last = history[-1]["outcome"] ++ if last in {"verified-success", "verified-no-change", "verified-rolled-back"}: ++ proofs = [row["evidence_digest"], row["session_revocation_digest"]] ++ if expected[step]["value_handling"] == "generated_in_custody": ++ proofs.append(row["grant_revocation_digest"]) ++ if row["session_revoked"] is not True or any( ++ type(value) is not str or not DIGEST.fullmatch(value) for value in proofs): ++ raise EvidenceRefused("Warden completed state lacks revocation fingerprints") ++ if type(row["attempt_id"]) is not str or not ID.fullmatch(row["attempt_id"]) or row["operation"] not in {"run", "rollback", "reconcile"}: ++ raise EvidenceRefused("Warden completed state lacks exact attempt identity") ++ result.append({"step": step, "outcome": last, "native_authenticated": False}) ++ return result, sorted(set(expected) - seen) ++ ++ ++def audit(case_source, type_source, rendered, *, structure=None, journal_rows=()): ++ structural = check_structure(structure, case_source, type_source, rendered) if structure is not None else None ++ steps, missing = check_steps(journal_rows, case_source, type_source, rendered) ++ type_spec = yaml.safe_load(type_source)["spec"] ++ return {"schema": "netkingdom.case-evidence-compatibility.v1", ++ "case_digest": sha(case_source), "type_digest": sha(type_source), "render_digest": rendered["digest"], ++ "structure": structural, "steps": steps, "missing_follow_through": missing, ++ "required_verification": [item["id"] for item in rendered["verification"]], ++ "missing_machine_descriptors": [item["id"] for item in rendered["follow_through"] ++ if item["id"] not in type_spec.get("execution_descriptors", {})], ++ "unresolved_native_gates": ["authenticated-owner-evidence-admission", "accepted-to-materialized-status-binding", ++ "human-sign-in", "consumer-group-denial", "parent-session-and-broker-grant-revocation", "created-only-rollback"], ++ "native_complete": False, "execution": {"permitted": False}} +diff --git a/tools/security-case-types/security_case_validator.py b/tools/security-case-types/security_case_validator.py +index 03e00d3..1dc63d6 100644 +--- a/tools/security-case-types/security_case_validator.py ++++ b/tools/security-case-types/security_case_validator.py +@@ -630,6 +630,15 @@ def main(argv: list[str] | None = None) -> int: + rd.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH", + help="select an exact version root; no search or fallback") + rd.add_argument("--allow-unimplemented", action="store_true") ++ ev = sub.add_parser("check-evidence", help="check existing Mason/Warden source artifacts; never native acceptance") ++ ev.add_argument("case") ++ ev.add_argument("declarations", nargs="*") ++ ev.add_argument("--repos-root", default=str(TOOL_DIR.parents[2])) ++ ev.add_argument("--types", required=True) ++ ev.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH") ++ ev.add_argument("--allow-unimplemented", action="store_true") ++ ev.add_argument("--structure-request", help="ops-mason structure-request JSON") ++ ev.add_argument("--warden-journal", help="existing Warden journal.sqlite; opened read-only") + args = ap.parse_args(argv) + + if args.cmd == "validate-type": +@@ -644,7 +653,7 @@ def main(argv: list[str] | None = None) -> int: + selection_issues, version_roots = parse_version_roots(args.version_root) + if has_errors(selection_issues): + return _report(selection_issues) +- issues, type_data, _ = load_type(Path(args.types), (case.get("spec") or {}).get("type", ""), ++ issues, type_data, selected_type_dir = load_type(Path(args.types), (case.get("spec") or {}).get("type", ""), + version_roots=version_roots) + if type_data is None: + return _report(issues) +@@ -663,6 +672,23 @@ def main(argv: list[str] | None = None) -> int: + issues = load_issues + issues + if rendered is None: + return _report(issues) ++ if args.cmd == "check-evidence": ++ import security_case_evidence as evidence ++ try: ++ source = evidence.read(args.case) ++ type_source = evidence.read(selected_type_dir / "type.yaml") ++ # Refuse source changes between ordinary validation/render and audit. ++ if yaml.safe_load(source) != case or yaml.safe_load(type_source) != type_data: ++ raise evidence.EvidenceRefused("case/type source changed during validation") ++ structure = evidence.decode(evidence.read(args.structure_request)) if args.structure_request else None ++ rows = evidence.read_journal(args.warden_journal) if args.warden_journal else () ++ report = evidence.audit(source, type_source, rendered, structure=structure, journal_rows=rows) ++ except (evidence.EvidenceRefused, OSError, ValueError, TypeError, KeyError): ++ print("ERROR evidence: incompatible or unsafe owner metadata (diagnostics withheld)", file=sys.stderr) ++ return 1 ++ json.dump(report, sys.stdout, indent=2, sort_keys=True) ++ print() ++ return 0 + json.dump(rendered, sys.stdout, indent=2, sort_keys=True) + print() + return _report(issues) +diff --git a/tools/security-case-types/tests/test_case_evidence.py b/tools/security-case-types/tests/test_case_evidence.py +new file mode 100644 +index 0000000..135ff60 +--- /dev/null ++++ b/tools/security-case-types/tests/test_case_evidence.py +@@ -0,0 +1,200 @@ ++"""Real owner source API compatibility, with synthetic metadata only; no native evidence.""" ++import json ++from pathlib import Path ++import shutil ++import subprocess ++import sys ++ ++import pytest ++import yaml ++ ++TOOL = Path(__file__).resolve().parents[1] ++REPO = TOOL.parents[1] ++OWNERS = Path("/home/worsch") ++sys.path.insert(0, str(TOOL)) ++import security_case_evidence as e ++import security_case_validator as v ++ ++ ++@pytest.fixture ++def source(): ++ case_path = REPO / "security-cases/forgejo-netkingdom-oidc.yaml" ++ raw = case_path.read_bytes() ++ case = yaml.safe_load(raw) ++ issues, doc, folder = v.load_type(REPO / "security-case-types", case["spec"]["type"]) ++ paths, issues = v.discover_declarations(doc, OWNERS) ++ declarations, issues = v.composer.load_declarations(paths) ++ issues, rendered = v.render_case(case, doc, declarations, require_implemented=False) ++ assert rendered is not None and not v.has_errors(issues) ++ return raw, (folder / "type.yaml").read_bytes(), rendered ++ ++ ++@pytest.fixture ++def mason(tmp_path, source): ++ sys.path.insert(0, str(OWNERS / "ops-mason/src")) ++ from ops_mason import case_structure ++ root = OWNERS / "ops-mason" ++ bundle = tmp_path / "bundle" ++ shutil.copytree(root / "builds/case-forgejo-netkingdom-oidc", bundle) ++ # Existing Mason source tests identify this historic bundle's wrong consumer. ++ rel = "credential-change-requests/CCR-DRAFT-FORGEJO-NETKINGDOM-OIDC-FORGEJO.yaml" ++ ccr = yaml.safe_load((bundle / rel).read_bytes()) ++ ccr["target"]["workload"] = "forgejo" ++ (bundle / rel).write_text(yaml.safe_dump(ccr, sort_keys=False, width=110)) ++ manifest = json.loads((bundle / "manifest.json").read_bytes()) ++ manifest["files"][rel] = e.sha((bundle / rel).read_bytes()).split(":")[1] ++ (bundle / "manifest.json").write_text(json.dumps(manifest)) ++ request = case_structure.prepare(REPO / "security-cases/forgejo-netkingdom-oidc.yaml", ++ root / "plans/case-forgejo-netkingdom-oidc.md", bundle, ++ nk_root=OWNERS / "net-kingdom", allow_unimplemented=True) ++ return json.loads(json.dumps(request.as_dict())) ++ ++ ++def test_real_mason_request_recomputes_all_source_and_projection_pins(source, mason): ++ result = e.audit(*source, structure=mason) ++ assert result["structure"]["objects"] == 5 ++ assert result["native_complete"] is False ++ assert result["structure"]["native_materialization_verified"] is False ++ assert "mint-secret" in result["missing_follow_through"] ++ assert "register-client" in result["missing_machine_descriptors"] ++ assert "consumer-enforces-groups" in result["required_verification"] ++ ++ ++@pytest.mark.parametrize("field", ["case_digest", "type_digest", "render_digest", "structural_digest"]) ++def test_changed_source_or_projection_refused(source, mason, field): ++ mason[field] = "sha256:" + "0" * 64 ++ with pytest.raises(e.EvidenceRefused): ++ e.audit(*source, structure=mason) ++ ++ ++def test_rehashed_structural_inventory_cannot_add_role_or_mint(source, mason): ++ mason["objects"][0]["target"] += "-sibling" ++ with pytest.raises(e.EvidenceRefused): ++ e.audit(*source, structure=mason) ++ ++ ++def test_structure_unknown_native_success_flag_is_refused(source, mason): ++ mason["native_complete"] = True ++ with pytest.raises(e.EvidenceRefused): ++ e.audit(*source, structure=mason) ++ ++ ++@pytest.fixture ++def warden(tmp_path, source): ++ sys.path.insert(0, str(OWNERS / "ops-warden/src")) ++ from warden.case_run import Invocation, Journal, OwnerResult ++ from warden.case_receipt_state import Outcome, Pins, Receipt, StepState ++ raw, type_raw, rendered = source ++ # Test-only migration: current acknowledged real type has no descriptors. ++ doc = yaml.safe_load(type_raw) ++ descriptor_digest = e.sha(b"fixture-descriptor") ++ doc["spec"]["execution_descriptors"] = {"mint-secret": descriptor_digest} ++ type_raw = yaml.safe_dump(doc).encode() ++ pins = Pins(e.sha(raw), e.sha(type_raw), descriptor_digest, *([e.sha(b"fixture")] * 3)) ++ state = StepState("mint-secret", "railiance-platform", pins, (), False, ++ (Receipt(1, Outcome.RUNNING), Receipt(2, Outcome.SUCCESS))) ++ invocation = Invocation("mint-secret", "railiance-platform", "forgejo", "human", "fixture-human", ++ "keycape-client-secret-generate", pins, "attempt-fixture", "run") ++ receipt = OwnerResult(Outcome.SUCCESS, "fixture-evidence", (), True, ++ "fixture-parent-revocation", "fixture-grant-revocation", "attempt-fixture", "run") ++ journal = Journal(tmp_path / "private-journal") ++ journal.save("mint-secret:forgejo", state, receipt, invocation) ++ return (raw, type_raw, rendered), journal ++ ++ ++def test_actual_warden_journal_save_is_read_without_mutation_and_not_native_proof(warden): ++ source, journal = warden ++ path = journal.directory / "journal.sqlite" ++ before = path.read_bytes() ++ rows = e.read_journal(path) ++ result = e.audit(*source, journal_rows=rows) ++ assert path.read_bytes() == before ++ assert result["steps"] == [{"step": "mint-secret", "outcome": "verified-success", "native_authenticated": False}] ++ assert result["native_complete"] is False ++ ++ ++@pytest.mark.parametrize("field", ["session_revocation_digest", "grant_revocation_digest", "evidence_digest"]) ++def test_missing_revocation_or_owner_fingerprint_refused(warden, field): ++ source, journal = warden ++ row = e.read_journal(journal.directory / "journal.sqlite")[0] ++ row[field] = None ++ with pytest.raises(e.EvidenceRefused): ++ e.audit(*source, journal_rows=(row,)) ++ ++ ++def test_unresolved_attempt_not_promoted_to_success(warden): ++ source, journal = warden ++ row = e.read_journal(journal.directory / "journal.sqlite")[0] ++ row["receipts"][-1]["outcome"] = "uncertain" ++ assert e.audit(*source, journal_rows=(row,))["steps"][0]["outcome"] == "uncertain" ++ ++ ++def test_no_descriptor_in_actual_acknowledged_type_refuses_journal_success(source, warden): ++ _, journal = warden ++ row = e.read_journal(journal.directory / "journal.sqlite")[0] ++ row["pins"]["case_type"] = e.sha(source[1]) ++ with pytest.raises(e.EvidenceRefused, match="machine descriptor"): ++ e.audit(*source, journal_rows=(row,)) ++ ++ ++def test_missing_or_symlink_journal_never_created(tmp_path): ++ path = tmp_path / "missing.sqlite" ++ with pytest.raises(e.EvidenceRefused): ++ e.read_journal(path) ++ assert not path.exists() ++ target = tmp_path / "target.sqlite" ++ target.write_bytes(b"not sqlite") ++ path.symlink_to(target) ++ with pytest.raises(e.EvidenceRefused): ++ e.read_journal(path) ++ ++ ++def test_duplicate_and_oversized_json_refused(): ++ for source in (b'{"schema":1,"schema":2}', b"x" * (e.LIMIT + 1)): ++ with pytest.raises(e.EvidenceRefused): ++ e.decode(source) ++ ++ ++@pytest.mark.parametrize("version", ["1.1", "1.2"]) ++def test_existing_cli_exact_source_selection_and_normal_admission_refusal(version): ++ case = REPO / ("security-cases/forgejo-netkingdom-oidc.yaml" if version == "1.1" else ++ "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/forgejo.yaml") ++ argv = [sys.executable, str(TOOL / "security_case_validator.py"), "check-evidence", str(case), ++ "--types", str(REPO / "security-case-types"), "--repos-root", str(OWNERS), ++ "--version-root", "1.2=" + str(REPO / "security-case-type-revisions/1.2")] ++ refused = subprocess.run(argv, capture_output=True, text=True) ++ assert refused.returncode == 1 and "only implemented types" in refused.stderr ++ result = subprocess.run(argv + ["--allow-unimplemented"], capture_output=True, text=True) ++ assert result.returncode == 0, result.stderr ++ report = json.loads(result.stdout) ++ assert report["native_complete"] is False and report["execution"]["permitted"] is False ++ assert ("deliver-consumer-secret" in report["missing_follow_through"]) == (version == "1.2") ++ ++ ++def test_cli_consumes_actual_mason_request_and_withholds_rejected_payload(tmp_path, mason): ++ request = tmp_path / "request.json" ++ request.write_text(json.dumps(mason)) ++ args = [sys.executable, str(TOOL / "security_case_validator.py"), "check-evidence", ++ str(REPO / "security-cases/forgejo-netkingdom-oidc.yaml"), "--types", str(REPO / "security-case-types"), ++ "--repos-root", str(OWNERS), "--allow-unimplemented", "--structure-request", str(request)] ++ result = subprocess.run(args, capture_output=True, text=True) ++ assert result.returncode == 0, result.stderr ++ assert json.loads(result.stdout)["structure"]["objects"] == 5 ++ mason["unexpected"] = "sensitive-fixture-diagnostic" ++ request.write_text(json.dumps(mason)) ++ result = subprocess.run(args, capture_output=True, text=True) ++ assert result.returncode == 1 and "sensitive-fixture-diagnostic" not in result.stderr + result.stdout ++ ++ ++def test_actual_journal_latest_uncertainty_does_not_replay_prior_success(warden): ++ source, journal = warden ++ from warden.case_receipt_state import Pins, Receipt, StepState, Outcome ++ row = e.read_journal(journal.directory / "journal.sqlite")[0] ++ history = tuple(Receipt(item["attempt"], Outcome(item["outcome"]), tuple(item["created"])) ++ for item in row["receipts"]) ++ state = StepState(row["step_id"], row["owner"], Pins(**row["pins"]), (), False, ++ history + (Receipt(3, Outcome.UNCERTAIN),)) ++ journal.save("mint-secret:forgejo", state) ++ rows = e.read_journal(journal.directory / "journal.sqlite") ++ assert len(rows) == 1 ++ assert e.audit(*source, journal_rows=rows)["steps"][0]["outcome"] == "uncertain" +diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md +index d50a1cb..074efd1 100644 +--- a/workplans/NK-WP-0048-security-case-types.md ++++ b/workplans/NK-WP-0048-security-case-types.md +@@ -559,3 +559,28 @@ docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-vali + 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. diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/manifest.json b/docs/changes/netkingdom-target-2026-10-16/browser-case/manifest.json new file mode 100644 index 00000000..c2865b31 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/manifest.json @@ -0,0 +1,58 @@ +{ + "artifact_hashes": { + "README.md": "sha256:c9cd28373fef972045401d9022a613b1b231ccebc3caa9c9eb45cacdad4bd606", + "implementation.patch": "sha256:d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05", + "owner-dirty-baseline.patch": "sha256:2a2f779ab3fdb57e66a3c446423931baee5fda054c731fa47bbc01e5bd38f663", + "source-preflight-1.1.json": "sha256:5417e0d78566d8690e6d3bb5f7c943632c97c76fca37014dc93a745eec770ffa", + "source-preflight-1.2.json": "sha256:e61ea4c19cd1c372302aa415431015288a11b385b8ef7138ff7e738f334c2da7", + "validation.json": "sha256:6affc009008211c9630401356b015c98de0c072f66fcbf46a65fabe0fb69eabb", + "warden-admission-mismatch.json": "sha256:79bb22a3a52f314c721d998c5e4846e460d2d9125c8161045f6c0c6fa1c4cc27" + }, + "base_commit": "994d11c869daf9624ac6da2adfdaeb86d8cdf537", + "checkout": "/tmp/netkingdom-browser-target-20261016", + "files": { + "tools/security-case-types/README.md": "sha256:00fc8c411f6de8ca6bec73f900731c503bea805359c42fe47c5a187d75b61ab1", + "tools/security-case-types/security_case_evidence.py": "sha256:3c5d021ca7f1fc41fba4b2ff42fcb036b916a2a09ffc5e147cc3db1874dd2401", + "tools/security-case-types/security_case_validator.py": "sha256:cbb9ab532d328f78cf37aca57891400e2e65d63a8ab3c5fe7bd1a4ff21dee56f", + "tools/security-case-types/tests/test_case_evidence.py": "sha256:8b89b3c12627f56d9e7a75ca2f580d803bf27aafa9289135d1f804b668d6fedd", + "workplans/NK-WP-0048-security-case-types.md": "sha256:9ce1cb4d9a720d5744c2a7d9a0e806123b50b92ccc7fd1e80d2609198fb994f1" + }, + "implementation_patch_sha256": "sha256:d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05", + "live_changes": false, + "native_complete": false, + "owner": "net-kingdom", + "preserved_dirty_tracked_paths": [ + ".custodian-brief.md", + "WORK-RECORDS.md" + ], + "schema": "netkingdom-target-patch-manifest.v1", + "source_dependencies": { + "net-kingdom": { + "files": { + "INTENT.md": "sha256:fd2ba8dff9dfd5583ec6d8b0348f273bfe00c59923b5d5d47cd22b1fb4658c54", + "SCOPE.md": "sha256:0f896db98d03aa7460d88999b738ea00be87dd34fe0b250f11bf0b6f170b0172", + "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml": "sha256:9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578", + "security-case-types/oidc-browser-relying-party/type.yaml": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "security-cases/forgejo-netkingdom-oidc.yaml": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d" + }, + "head": "994d11c869daf9624ac6da2adfdaeb86d8cdf537" + }, + "ops-mason": { + "files": { + "src/ops_mason/case_plan.py": "sha256:c964524c97b4c579a3ff2e890f40180fc469925f123a3b891f10a6dad0edbec0", + "src/ops_mason/case_structure.py": "sha256:71032a5ea613f5226f552f796656599c1a7447a38428f890595d8f252831ce80" + }, + "head": "e4761b843cd881a672cbb33a5b451de31a61d730" + }, + "ops-warden": { + "files": { + "src/warden/case_receipt_state.py": "sha256:a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc", + "src/warden/case_run.py": "sha256:3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8" + }, + "head": "d2cd3fa9405f0766eef67a8b0e897239d8817fb7" + } + }, + "status": "source-ready; canonical integration and owner-native admission pending", + "untracked_owner_files_copied": false, + "work_item": "NK-WP-0048-T06" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/browser-case/owner-dirty-baseline.patch new file mode 100644 index 00000000..c9dda46b --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/owner-dirty-baseline.patch @@ -0,0 +1,220 @@ +diff --git a/.custodian-brief.md b/.custodian-brief.md +index 8a5b338..bae8343 100644 +--- a/.custodian-brief.md ++++ b/.custodian-brief.md +@@ -2,44 +2,49 @@ + # Custodian Brief — net-kingdom + + **Domain:** infotech +-**Last synced:** 2026-10-14 22:34 UTC ++**Last synced:** 2026-10-15 23:38 UTC + **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + + ## Active Workstreams + +-### Implement deterministic posture and evidence feedback +-Progress: 4/5 done | workplan_id: `9d7b04f9-3803-5613-b7a5-8bd606c77f5a` +- +-**Open tasks:** +-- ! Obtain audit-core freshness adoption `7d2029d2` +- *(wait: audit-core tenancy.yaml must publish structured authoritative owner, observation/expiry and replacement-action metadata for WH-ENG-20260822-AUDIT-E2-03. Re-evaluate with explicit current as-of; later recovery prose does not renew E2.)* +- + ### Publish the NetKingdom emission-cadence security profile +-Progress: 4/5 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc` ++Progress: 5/6 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc` + + **Open tasks:** +-- ! Bind and hand off the published contract `e3fbc8b8` +- *(wait: Approval Engine and Qonto owner declarations now pass the published schema and supplied-inventory profile; local-identity incompatibility is retained. Requires actual sender cadence mappings, lifecycle delivery/readback/reconciliation and Kings Guard operational feed evidence.)* ++- ► Bind and hand off the published contract `e3fbc8b8` + +-### Reconcile reef placement and security-zone canon dependencies +-Progress: 3/6 done | workplan_id: `965ad365-6b81-50a1-a2a3-2d0c1fcce0b4` ++### Register Playwright human and agent consumers through governed identity delivery ++Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377` + + **Open tasks:** +-- ! T02 — Extend provider declarations to reefs `5b35b646` +- *(wait: Requires repo-manager acceptance of the reef provider carrier/schema, then railiance-infra authored ceilings, conditions and evidence in the authoritative reef declaration.)* +-- ! T03 — Reconcile reef ceilings mechanically `e3f0bb0f` +- *(wait: Mechanical join of reef ceilings to consumer V declarations waits on T02.)* +-- ! T06 — Resolve the `DataClassification` mismatch `bf8b3e1d` +- *(wait: Requires InfoTechCanon ruling on public disclosure classification versus synthetic provenance, then ops-warden authoritative dataclass_floor mapping and tests; no public-to-synthetic alias.)* ++- ! Verify issuer and real consumer acceptance `5a3aaf57` ++ *(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)* ++- ► Deliver custody integration and guarded registration activation `a9dadd07` + + ### Remove bootstrap secret output and verify separated recovery custody + Progress: 1/3 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804` + + **Open tasks:** + - ! Verify owner-approved recovery and current native callers `d68cd4d3` +- *(wait: Source caller reconciliation and exact owner-scoped isolated recovery ceremony are required; no live bootstrap or root retrieval is authorized by this source receipt.)* ++ *(wait: The actual off-host PI database decrypted and restored with the independently held key; wrong-key refusal and cleanup passed. Operator reports no known full identity bundle and requested missing coverage preparation. Full-profile source adapters, bootstrap handoff and discovery are implemented/tested, but CCR-2026-0048/0049 owner admission, first actual full capture/off-site proof and native token/audit/directory/issuer recovery remain open, along with T02 current-owner and legacy-credential disposition. No live bootstrap/root retrieval or deferred exposure rotation is authorized.)* + - ► Reconcile other legacy custody callers and root-output boundaries `479d6970` + ++### Establish SecurityCaseTypes so that known security cases are light ++Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb` ++ ++**Open tasks:** ++- ! T05 — Acknowledge the type through informed-decision `d9b26bcf` ++ *(wait: 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.)* ++- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae` ++ *(wait: The next types follow once the first one is proven end to end.)* ++ ++### Define an execution-attribution receipt for Railiance runs ++Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487` ++ ++**Open tasks:** ++- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1` ++ *(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)* ++ + ### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker + Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07` + +@@ -60,48 +65,6 @@ Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07` + *(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)* + - … and 1 more open tasks + +-### Establish SecurityCaseTypes so that known security cases are light +-Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb` +- +-**Open tasks:** +-- ! T05 — Acknowledge the type through informed-decision `d9b26bcf` +- *(wait: 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.)* +-- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae` +- *(wait: The next types follow once the first one is proven end to end.)* +- +-### Show who a workload sign-in actually is +-Progress: 3/6 done | workplan_id: `c1771c07-4466-5aa2-ad43-df63a471946b` +- +-**Open tasks:** +-- ! Account site shows both sessions `bdc8b414` +- *(wait: Implemented in user-engine 80f5870 (USER-WP-0039). Waits for USER-WP-0039-T02: deploy and a mismatched-session check.)* +-- ! Enroll the negative-login factor `afb5816f` +- *(wait: After T04 names the fixture uid. Founder enrolls its one-time password at https://pink-account.coulomb.social/, realm coulomb.)* +-- ! Hand the OpenBao proof back to its owners `86fb4a6b` +- *(wait: Implemented in ops-mason fca5591 (MASON-WP-0012) and railiance-platform 0f0282a (RPF-WP-0057). Waits for CCR-2026-0045 apply, overlay deploy, and the MASON-WP-0012-T04 live proof.)* +- +-### Register Playwright human and agent consumers through governed identity delivery +-Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377` +- +-**Open tasks:** +-- ! Verify issuer and real consumer acceptance `5a3aaf57` +- *(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)* +-- ► Deliver custody integration and guarded registration activation `a9dadd07` +- +-### Define an execution-attribution receipt for Railiance runs +-Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487` +- +-**Open tasks:** +-- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1` +- *(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)* +- +-### Cut over NetKingdom identity to railiance01 and retire CoulombCore +-Progress: 7/8 done | workplan_id: `d76ddccc-00c8-548a-b141-2cd660fa38da` +- +-**Open tasks:** +-- ! T08 - Final deletion and closure `42a3b4c0` +- *(wait: Founder 2026-10-11: coulombcore is retired and the machine reused (D4 batch approved). Order: host-side inventory (containers, on-disk manifests; no kube API, certs expired 2026-08-31), then stop k3s for good and wipe for reuse. Executes after Sol's passing railiance01 restore/restart drill receipt. Never rotate certs or restart k3s there first.)* +- + --- + ## MCP Orientation (when available) + +diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md +index 76a6b7f..96ed45c 100644 +--- a/WORK-RECORDS.md ++++ b/WORK-RECORDS.md +@@ -18,16 +18,16 @@ + | workplan | NK-WP-0009 | finished | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | + | workplan | NK-WP-0011 | active | — | workplans/NK-WP-0011-enterprise-federation-saml.md | + | workplan | NK-WP-0021 | finished | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | +-| workplan | NK-WP-0022 | blocked | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | ++| workplan | NK-WP-0022 | finished | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | + | workplan | NK-WP-0023 | finished | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | + | workplan | NK-WP-0024 | finished | — | workplans/NK-WP-0024-user-engine-portal-integration-expansion.md | + | workplan | NK-WP-0025 | finished | — | workplans/NK-WP-0025-public-self-registration-and-application-jit.md | + | workplan | NK-WP-0026 | finished | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | +-| workplan | NK-WP-0027 | active | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | ++| workplan | NK-WP-0027 | backlog | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | + | workplan | NK-WP-0028 | finished | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | + | workplan | NK-WP-0029 | finished | — | workplans/NK-WP-0029-scope-and-intent-reconciliation.md | + | workplan | NK-WP-0030 | finished | — | workplans/NK-WP-0030-deterministic-security-scenario-composition.md | +-| workplan | NK-WP-0031 | active | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | ++| workplan | NK-WP-0031 | finished | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | + | workplan | NK-WP-0032 | finished | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | + | workplan | NK-WP-0033 | finished | — | workplans/NK-WP-0033-keycape-secret-exposure-rotation.md | + | workplan | NK-WP-0034 | finished | — | workplans/NK-WP-0034-verification-that-verifies.md | +@@ -45,6 +45,9 @@ + | workplan | NK-WP-0046 | finished | — | workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md | + | workplan | NK-WP-0047 | finished | — | workplans/NK-WP-0047-platform-password-reset-mail.md | + | workplan | NK-WP-0048 | active | — | workplans/NK-WP-0048-security-case-types.md | ++| workplan | NK-WP-0049 | active | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | ++| workplan | NK-WP-0050 | active | — | workplans/NK-WP-0050-playwright-consumer-registration.md | ++| workplan | NK-WP-0051 | finished | — | workplans/NK-WP-0051-sign-in-diagnosis.md | + | task | NK-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | + | task | NK-WP-ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md | + | task | NK-WP-ADHOC-2026-08-14-T01 | done | — | workplans/ADHOC-2026-08-14.md | +@@ -92,7 +95,7 @@ + | task | NK-WP-0022-T05 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | + | task | NK-WP-0022-T06 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | + | task | NK-WP-0022-T07 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | +-| task | NK-WP-0022-T08 | wait | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | ++| task | NK-WP-0022-T08 | done | — | workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md | + | task | NK-WP-0023-T01 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | + | task | NK-WP-0023-T02 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | + | task | NK-WP-0023-T03 | done | — | workplans/NK-WP-0023-user-engine-portal-platform-integration.md | +@@ -113,11 +116,11 @@ + | task | NK-WP-0026-T01 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | + | task | NK-WP-0026-T02 | done | — | workplans/NK-WP-0026-flex-auth-caller-identity-rollout.md | + | task | NK-WP-0027-T01 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +-| task | NK-WP-0027-T02 | wait | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +-| task | NK-WP-0027-T03 | wait | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | ++| task | NK-WP-0027-T02 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | ++| task | NK-WP-0027-T03 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | + | task | NK-WP-0027-T04 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | + | task | NK-WP-0027-T05 | done | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | +-| task | NK-WP-0027-T06 | wait | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | ++| task | NK-WP-0027-T06 | todo | — | workplans/NK-WP-0027-reef-placement-reconciliation.md | + | task | NK-WP-0028-T01 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | + | task | NK-WP-0028-T02 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | + | task | NK-WP-0028-T03 | done | — | workplans/NK-WP-0028-canon-publication-and-grouping-semantics.md | +@@ -134,7 +137,7 @@ + | task | NK-WP-0031-T01 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | + | task | NK-WP-0031-T02 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | + | task | NK-WP-0031-T03 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | +-| task | NK-WP-0031-T04 | wait | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | ++| task | NK-WP-0031-T04 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | + | task | NK-WP-0031-T05 | done | — | workplans/NK-WP-0031-deterministic-posture-feedback.md | + | task | NK-WP-0032-T01 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | + | task | NK-WP-0032-T02 | done | — | workplans/NK-WP-0032-openbao-operator-loopback-callback.md | +@@ -152,8 +155,9 @@ + | task | NK-WP-0035-T01 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | + | task | NK-WP-0035-T02 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | + | task | NK-WP-0035-T03 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | +-| task | NK-WP-0035-T04 | wait | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | ++| task | NK-WP-0035-T04 | progress | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | + | task | NK-WP-0035-T05 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | ++| task | NK-WP-0035-T06 | done | — | workplans/NK-WP-0035-emission-cadence-security-profile.md | + | task | NK-WP-0036-T01 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | + | task | NK-WP-0036-T02 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | + | task | NK-WP-0036-T03 | done | — | workplans/NK-WP-0036-restore-user-portal-client-registration.md | +@@ -206,6 +210,18 @@ + | task | NK-WP-0048-T07 | wait | — | workplans/NK-WP-0048-security-case-types.md | + | task | NK-WP-0048-T08 | done | — | workplans/NK-WP-0048-security-case-types.md | + | task | NK-WP-0048-T09 | done | — | workplans/NK-WP-0048-security-case-types.md | ++| task | NK-WP-0049-T01 | done | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | ++| task | NK-WP-0049-T02 | progress | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | ++| task | NK-WP-0049-T03 | wait | — | workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md | ++| task | NK-WP-0050-T01 | done | — | workplans/NK-WP-0050-playwright-consumer-registration.md | ++| task | NK-WP-0050-T02 | progress | — | workplans/NK-WP-0050-playwright-consumer-registration.md | ++| task | NK-WP-0050-T03 | wait | — | workplans/NK-WP-0050-playwright-consumer-registration.md | ++| task | NK-WP-0051-T01 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | ++| task | NK-WP-0051-T02 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | ++| task | NK-WP-0051-T03 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | ++| task | NK-WP-0051-T04 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | ++| task | NK-WP-0051-T05 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | ++| task | NK-WP-0051-T06 | done | — | workplans/NK-WP-0051-sign-in-diagnosis.md | + | intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | + | intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | + | intake | NET-IN-0001 | open | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.1.json b/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.1.json new file mode 100644 index 00000000..01c9d089 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.1.json @@ -0,0 +1,46 @@ +{ + "case_digest": "sha256:bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d", + "execution": { + "permitted": false + }, + "missing_follow_through": [ + "bind-env", + "deliver-secret", + "ensure-groups", + "hand-over", + "mint-secret", + "register-client" + ], + "missing_machine_descriptors": [ + "ensure-groups", + "mint-secret", + "deliver-secret", + "bind-env", + "register-client", + "hand-over" + ], + "native_complete": false, + "render_digest": "sha256:e64b56e4590b57b858303fcda1a474693e3dd821bc83df26646fe78fc345498a", + "required_verification": [ + "authorize-accepts-redirects", + "authorize-refuses-other-redirect", + "custody-denied-to-default", + "custody-allowed-to-consumer", + "groups-present", + "consumer-enforces-groups", + "secret-delivered", + "mfa-pinned" + ], + "schema": "netkingdom.case-evidence-compatibility.v1", + "steps": [], + "structure": null, + "type_digest": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c", + "unresolved_native_gates": [ + "authenticated-owner-evidence-admission", + "accepted-to-materialized-status-binding", + "human-sign-in", + "consumer-group-denial", + "parent-session-and-broker-grant-revocation", + "created-only-rollback" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.2.json b/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.2.json new file mode 100644 index 00000000..8583a434 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/source-preflight-1.2.json @@ -0,0 +1,49 @@ +{ + "case_digest": "sha256:1c868c1da7d9077fae97bc6bc029706ec3dc418cfeaae30dc0f92b5b4ba6f2ab", + "execution": { + "permitted": false + }, + "missing_follow_through": [ + "bind-env", + "deliver-consumer-secret", + "deliver-secret", + "ensure-groups", + "hand-over", + "mint-secret", + "register-client" + ], + "missing_machine_descriptors": [ + "ensure-groups", + "mint-secret", + "deliver-secret", + "bind-env", + "register-client", + "deliver-consumer-secret", + "hand-over" + ], + "native_complete": false, + "render_digest": "sha256:c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35", + "required_verification": [ + "authorize-accepts-redirects", + "authorize-refuses-other-redirect", + "custody-denied-to-default", + "custody-allowed-to-consumer", + "groups-present", + "consumer-enforces-groups", + "secret-delivered", + "mfa-pinned", + "consumer-secret-delivered" + ], + "schema": "netkingdom.case-evidence-compatibility.v1", + "steps": [], + "structure": null, + "type_digest": "sha256:9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578", + "unresolved_native_gates": [ + "authenticated-owner-evidence-admission", + "accepted-to-materialized-status-binding", + "human-sign-in", + "consumer-group-denial", + "parent-session-and-broker-grant-revocation", + "created-only-rollback" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/validation.json b/docs/changes/netkingdom-target-2026-10-16/browser-case/validation.json new file mode 100644 index 00000000..a29d69dd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/validation.json @@ -0,0 +1,22 @@ +{ + "checks": [ + { + "command": "NET_KINGDOM_ROOT=/tmp/netkingdom-browser-target-20261016 python3 -m pytest tools/security-case-types/tests security-case-types/oidc-browser-relying-party/tests security-case-type-revisions/1.2/oidc-browser-relying-party/tests -q", + "result": "103 passed in 5.35s", + "scope": "source tests; 19 new checks invoke real Mason.prepare and Warden.Journal.save with fixture metadata" + }, + { + "command": "python3 -m compileall -q tools/security-case-types/security_case_evidence.py tools/security-case-types/security_case_validator.py", + "result": "passed" + }, + { + "command": "git diff --check -- tools/security-case-types workplans/NK-WP-0048-security-case-types.md", + "result": "passed" + }, + { + "command": "ruff check ", + "result": "unavailable: ruff executable not installed" + } + ], + "initial_test_failure": "Isolated checkout lacked expected sibling net-kingdom declarations and public key-cape request. Test-only /tmp/net-kingdom symlink to isolated checkout and exact tracked KeyCape public request installed for existing suites; final suite passes. No credentials or untracked owner material copied." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/browser-case/warden-admission-mismatch.json b/docs/changes/netkingdom-target-2026-10-16/browser-case/warden-admission-mismatch.json new file mode 100644 index 00000000..d73ae39a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/browser-case/warden-admission-mismatch.json @@ -0,0 +1,10 @@ +{ + "test_only": true, + "owner_source": "ops-warden/src/warden/case_run.py:421", + "fixture": "ops-warden/tests/test_case_run.py:rig", + "reproduction": "Call real rig.__wrapped__(temporary directory); change its declared client_id sensitivity from operational to security_sensitive; accept only the exact changed type digest in the fixture's type-machine-seam test verifier; call Executor.admit with its existing case/descriptor/catalog and kwargs; never call run.", + "actual_Warden_admit_error": "unknown, fixed or secret-sensitivity case parameter", + "actual_browser_parameters_affected": ["redirect_uris", "allowed_scopes", "client_type", "mfa_required", "consumer_namespace (1.2)"], + "native_effects": false, + "owner_change_required": "Reuse exact-version NetKingdom validation and project reviewed safe metadata; retain credential and undeclared/fixed-parameter refusal." +}