docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
b53eed4073
commit
042ac52ac2
50 changed files with 8075 additions and 0 deletions
|
|
@ -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()
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0 .custodian-brief.md
|
||||
|
|
@ -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
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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.
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue