docs: preserve reviewed NetKingdom session artifacts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 11s

This commit is contained in:
codex 2026-11-01 22:48:04 +01:00
parent b53eed4073
commit 042ac52ac2
50 changed files with 8075 additions and 0 deletions

View file

@ -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()

View file

@ -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.<decision>.<memo>` 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_ID>_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 <binary>` 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
<list> --members <group=user+user;...>`. 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.

View file

@ -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.<decision>.<memo>` 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_ID>_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 <binary>` 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
<list> --members <group=user+user;...>`. 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.

View file

@ -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"
}
]

View file

@ -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": []
}
]

View file

@ -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"}

View file

@ -0,0 +1,100 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# 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.

View file

@ -0,0 +1,85 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# 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.

View file

@ -0,0 +1 @@
100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0 .custodian-brief.md

View file

@ -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

View file

@ -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 |

View file

@ -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.')

View file

@ -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.

View file

@ -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))

View file

@ -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.

View file

@ -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()

View file

@ -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"}

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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"
}
}

View file

@ -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))

View file

@ -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.

View file

@ -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()

View file

@ -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"}

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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"
}
}

View file

@ -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))

View file

@ -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.

View file

@ -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()

View file

@ -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.<decision>.<memo>` 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_ID>_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 <binary>` 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
<list> --members <group=user+user;...>`. 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.

View file

@ -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"}

View file

@ -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

View file

@ -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)

View file

@ -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.')

View file

@ -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.')

View file

@ -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.')

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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"
}

View file

@ -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 |

View file

@ -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"
]
}

View file

@ -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"
]
}

View file

@ -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 <new source files>",
"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."
}

View file

@ -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."
}