NK-WP-0048-T30: source composer for the seven-step 1.2 descriptor set

Composes through the explicit 1.2 version root, closes each adapter
descriptor through Warden Descriptor.decode and checks step/owner/lane
against the 1.2 follow_through. Refuses partial, duplicate, unknown,
mismatched and 1.1-default-changing compositions. The generated candidate
artifact is not an admission: the current adapters do not compose
(ensure-groups, mint-secret, hand-over refused) and worker-proposed names
are marked owner_confirmation_required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 903097@bnt-lap001
Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
This commit is contained in:
tegwick 2026-10-17 08:07:59 +02:00
parent 71a7c06249
commit 60b41a1f51
3 changed files with 583 additions and 0 deletions

View file

@ -0,0 +1,231 @@
"""Compose the seven-step source descriptor set for oidc-browser-relying-party@1.2.
Source candidate for NK-WP-0048-T30. Read-only: it loads the 1.2 type only
through the validator's explicit version-root selection, asks each step adapter
for its descriptor, closes each one through Warden's own Descriptor.decode, and
checks step, owner and lane against the 1.2 follow_through. It refuses a partial
map, a duplicate or unknown step, any mismatch, and any change to canonical 1.1.
Nothing here writes a type file, pins a descriptor, admits anything or touches
a live system. A composed map is a successor candidate that needs a reviewed
revision under canon section 6 and owner review (T16) before any pinning.
"""
from __future__ import annotations
from dataclasses import asdict
import argparse
import importlib.util
import json
from pathlib import Path
import sys
from warden.case_run import Descriptor, digest
from warden.case_receipt_state import Refused
ROOT = Path(__file__).resolve().parent
REPO = ROOT.parents[2]
DEFAULT_ROOT = REPO / 'security-case-types'
VERSION_ROOTS = {'1.2': REPO / 'security-case-type-revisions' / '1.2'}
TYPE_ID = 'oidc-browser-relying-party'
TYPE_REF = f'{TYPE_ID}@1.2'
CANONICAL_REF = f'{TYPE_ID}@1.1'
SECTION6 = ('successor candidate only: new descriptor bytes need a reviewed successor '
'revision under canon section 6 and owner review (NK-WP-0048-T16) before pinning')
# step -> (adapter file, needs explicit 1.2 version-root support)
ADAPTERS = {
'ensure-groups': ('ensure-groups-adapter.py', True),
'mint-secret': ('mint-secret-adapter.py', False),
'deliver-secret': ('deliver-secret-adapter.py', False),
'bind-env': ('bind-env-adapter.py', False),
'register-client': ('keycape-registration-adapter.py', False),
'deliver-consumer-secret': ('deliver-consumer-secret-adapter.py', False),
'hand-over': ('handover-adapter.py', True),
}
# Worker-proposed names (T25-T29) that the type does not fix. Each needs owner
# confirmation before any pin; the artifact marks them owner_confirmation_required.
PROPOSED = {
'mint-secret': {'task': 'NK-WP-0048-T25', 'actor': 'warden',
'schema': 'railiance-platform.custody-mint.v1'},
'deliver-secret': {'task': 'NK-WP-0048-T26', 'lane': 'platform-secret-delivery',
'schema': 'netkingdom.deliver-secret.v1'},
'bind-env': {'task': 'NK-WP-0048-T27', 'schema': 'netkingdom.keycape-bind-env.attempt.v1'},
'deliver-consumer-secret': {'task': 'NK-WP-0048-T28', 'lane': 'consumer-owner-deployment',
'schema': 'netkingdom.consumer-secret-delivery.v1'},
'hand-over': {'task': 'NK-WP-0048-T29', 'owner': 'apps', 'lane': 'apps-consumer-handover',
'schema': 'netkingdom.apps-handover.attempt.v1'},
}
def _load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _validator():
return _load('security_case_validator',
REPO / 'tools' / 'security-case-types' / 'security_case_validator.py')
def encode(desc: Descriptor) -> bytes:
return json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
def load_types(default_root=DEFAULT_ROOT, version_roots=None):
"""Return (1.2 spec, canonical 1.1 file digest) via explicit selection only."""
v = _validator()
roots = dict(VERSION_ROOTS if version_roots is None else version_roots)
if set(roots) != {'1.2'}:
raise Refused('only an explicit 1.2 version root may be selected')
issues, data, _ = v.load_type(Path(default_root), TYPE_REF, version_roots=roots)
if issues or data['spec'].get('version') != '1.2':
raise Refused('1.2 type not loadable through explicit version-root selection')
issues, default_12, _ = v.load_type(Path(default_root), TYPE_REF)
if not issues:
raise Refused('1.2 is reachable by default; canonical 1.1 default changed')
issues, canonical, type_dir = v.load_type(Path(default_root), CANONICAL_REF)
if issues or str(canonical['spec'].get('version')) != '1.1':
raise Refused('canonical 1.1 default not loadable')
return data['spec'], digest((type_dir / 'type.yaml').read_bytes())
def _resolve(value, params):
try:
return value.format(**params) if isinstance(value, str) else value
except (KeyError, IndexError, ValueError):
raise Refused('type owner template has no bound parameter') from None
def adapter_descriptors(check_config_digest, consumer_owner):
"""Ask every step adapter for its descriptor; T13 needs the checker digest."""
out = []
for step, (filename, needs_12) in ADAPTERS.items():
m = _load(f'compose_{step.replace("-", "_")}', ROOT / filename)
if needs_12 and getattr(m, 'SELECTABLE_TYPES', {}).get('1.2') != TYPE_REF:
raise Refused(f'{step}: adapter has no explicit 1.2 version-root path')
if step == 'register-client':
desc, source = m.descriptor(check_config_digest), m.source_digest(check_config_digest)
elif step == 'deliver-consumer-secret':
desc, source = m.descriptor(consumer_owner), m.source_digest()
else:
desc, source = m.descriptor(), m.source_digest()
if desc.adapter_source != source:
raise Refused(f'{step}: descriptor does not pin its own source digest')
out.append((step, desc))
return out
def compose(type_spec, candidates, params, canonical_digest_before, *, default_root=DEFAULT_ROOT):
"""Return {step: descriptor digest} for the complete 1.2 set, or raise Refused."""
steps = [s['id'] for s in type_spec.get('follow_through', ())]
if len(steps) != 7 or len(set(steps)) != 7:
raise Refused('1.2 type does not declare exactly seven distinct steps')
by_step, sources = {}, {}
for step, desc in candidates:
if step in by_step:
raise Refused(f'duplicate step {step!r}')
if step not in steps:
raise Refused(f'unknown step {step!r}')
try:
source = encode(desc)
Descriptor.decode(source)
except (TypeError, ValueError, Refused):
raise Refused(f'{step}: descriptor refused by the Warden v1 kernel') from None
if desc.step != step:
raise Refused(f'{step}: descriptor names step {desc.step!r}')
by_step[step], sources[step] = desc, source
missing = [s for s in steps if s not in by_step]
if missing:
raise Refused(f'partial descriptor map; missing {missing}')
for entry in type_spec['follow_through']:
desc = by_step[entry['id']]
if desc.owner != _resolve(entry['owner'], params):
raise Refused(f"{entry['id']}: owner {desc.owner!r} does not match the 1.2 type")
if 'lane' in entry and desc.lane != entry['lane']:
raise Refused(f"{entry['id']}: lane {desc.lane!r} does not match the 1.2 type")
_, after = load_types(default_root)
if after != canonical_digest_before:
raise Refused('canonical 1.1 type changed during composition')
return {s: digest(sources[s]) for s in steps}
def review(type_spec, candidates, params):
"""Per-step findings without stopping at the first refusal (for the artifact)."""
entries = {e['id']: e for e in type_spec.get('follow_through', ())}
rows = []
for step, desc in candidates:
entry, findings = entries.get(step), []
source = encode(desc)
try:
Descriptor.decode(source)
except Refused:
findings.append('refused by Warden v1 Descriptor.decode')
if entry is None:
findings.append('step not in 1.2 type')
else:
try:
owner = _resolve(entry['owner'], params)
except Refused as exc:
owner = None
findings.append(str(exc))
if owner is not None and desc.owner != owner:
findings.append(f'owner {desc.owner!r} != type owner {owner!r}')
if 'lane' in entry and desc.lane != entry['lane']:
findings.append(f"lane {desc.lane!r} != type lane {entry['lane']!r}")
proposed = dict(PROPOSED.get(step, {}))
if entry is not None and 'lane' not in entry:
proposed.setdefault('lane', desc.lane)
rows.append({'step': step, 'descriptor': asdict(desc),
'candidate_descriptor_digest': digest(source),
'type_owner': entry and entry['owner'], 'type_lane': entry and entry.get('lane'),
'findings': findings,
'owner_confirmation_required': proposed or None})
return rows
def artifact(check_config_digest, params):
type_spec, canonical = load_types()
candidates = adapter_descriptors(check_config_digest, params['consumer_owner_repo'])
try:
composed, refusal = compose(type_spec, candidates, params, canonical), None
except Refused as exc:
composed, refusal = None, str(exc)
return {
'schema': 'netkingdom.descriptor-composition-candidate.v1',
'task': 'NK-WP-0048-T30',
'type': TYPE_REF,
'selection': 'explicit version root 1.2=security-case-type-revisions/1.2; default root unchanged',
'canonical_default': {'type': CANONICAL_REF, 'type_yaml_digest': canonical,
'unchanged': True},
'status': 'not_an_admission',
'review_requirement': SECTION6,
'check_config_digest': check_config_digest,
'params': params,
'complete_executable_descriptor_set': composed is not None,
'execution_descriptors': composed,
'refusal': refusal,
'steps': review(type_spec, candidates, params),
'owner_review': False, 'admitted': False, 'dispatch_permitted': False,
}
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument('--check-config-digest', required=True)
p.add_argument('--consumer-owner-repo', required=True)
p.add_argument('--out', type=Path)
a = p.parse_args(argv)
doc = artifact(a.check_config_digest, {'consumer_owner_repo': a.consumer_owner_repo})
text = json.dumps(doc, indent=2, sort_keys=True) + '\n'
if a.out:
a.out.write_text(text)
else:
sys.stdout.write(text)
return 0 if doc['complete_executable_descriptor_set'] else 2
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,234 @@
{
"admitted": false,
"canonical_default": {
"type": "oidc-browser-relying-party@1.1",
"type_yaml_digest": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c",
"unchanged": true
},
"check_config_digest": "sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5",
"complete_executable_descriptor_set": false,
"dispatch_permitted": false,
"execution_descriptors": null,
"owner_review": false,
"params": {
"consumer_owner_repo": "railiance-apps"
},
"refusal": "ensure-groups: descriptor refused by the Warden v1 kernel",
"review_requirement": "successor candidate only: new descriptor bytes need a reviewed successor revision under canon section 6 and owner review (NK-WP-0048-T16) before pinning",
"schema": "netkingdom.descriptor-composition-candidate.v1",
"selection": "explicit version root 1.2=security-case-type-revisions/1.2; default root unchanged",
"status": "not_an_admission",
"steps": [
{
"candidate_descriptor_digest": "sha256:0a35e4bf7ce1b98a7538bda8b36bdce88d152880df05218020274a38c0f89c16",
"descriptor": {
"actor": "human",
"adapter": "net-kingdom/ensure-groups@v1",
"adapter_source": "sha256:e38067a40486379620d8ea37de48d436e9e7eac193541ddd73808e98af54be32",
"allowed_created": [
"service-group",
"service-group-membership"
],
"effect": "change",
"lane": "key-cape-oidc-login",
"owner": "net-kingdom",
"retry_safe": false,
"revocation_scopes": [],
"step": "ensure-groups",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [
"refused by Warden v1 Descriptor.decode"
],
"owner_confirmation_required": null,
"step": "ensure-groups",
"type_lane": "key-cape-oidc-login",
"type_owner": "net-kingdom"
},
{
"candidate_descriptor_digest": "sha256:d33f16653dfa5d226d8cfb82bc5b95f447a3d8ad439d65b9437b1656e1abb875",
"descriptor": {
"actor": "warden",
"adapter": "net-kingdom/keycape-mint-secret@v1",
"adapter_source": "sha256:ede3c2b3edfa453233fc6fd4eb4f713b26240ff38b497d38d63d90b01d797cf4",
"allowed_created": [
"custody-secret-version"
],
"effect": "mint",
"lane": "keycape-client-secret-generate",
"owner": "railiance-platform",
"retry_safe": false,
"revocation_scopes": [
"parent-session",
"broker-grant"
],
"step": "mint-secret",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [
"refused by Warden v1 Descriptor.decode"
],
"owner_confirmation_required": {
"actor": "warden",
"schema": "railiance-platform.custody-mint.v1",
"task": "NK-WP-0048-T25"
},
"step": "mint-secret",
"type_lane": "keycape-client-secret-generate",
"type_owner": "railiance-platform"
},
{
"candidate_descriptor_digest": "sha256:1e23b05058a61b3558070805329b6906f0ba9b989b98d9df35c621471a7c4938",
"descriptor": {
"actor": "human",
"adapter": "net-kingdom/deliver-secret@v1",
"adapter_source": "sha256:0e5ea5440f7f51d499c75b44e047aaeb9ddeb8eba1ce58ae4457cffb072c223b",
"allowed_created": [
"delivered-secret"
],
"effect": "change",
"lane": "platform-secret-delivery",
"owner": "railiance-platform",
"retry_safe": false,
"revocation_scopes": [
"parent-session"
],
"step": "deliver-secret",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [],
"owner_confirmation_required": {
"lane": "platform-secret-delivery",
"schema": "netkingdom.deliver-secret.v1",
"task": "NK-WP-0048-T26"
},
"step": "deliver-secret",
"type_lane": null,
"type_owner": "railiance-platform"
},
{
"candidate_descriptor_digest": "sha256:55898e15f5851525910f6ee3b2802d9167f0c0dd1bfcf921a81470161773f70a",
"descriptor": {
"actor": "human",
"adapter": "net-kingdom/keycape-bind-env@v1",
"adapter_source": "sha256:80bb389a2c91b32c7295f22c56ea7451d4bd9a0bf27d4b6519661d5eb9be0371",
"allowed_created": [
"keycape-env-binding"
],
"effect": "change",
"lane": "key-cape-oidc-login",
"owner": "net-kingdom",
"retry_safe": false,
"revocation_scopes": [
"parent-session"
],
"step": "bind-env",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [],
"owner_confirmation_required": {
"schema": "netkingdom.keycape-bind-env.attempt.v1",
"task": "NK-WP-0048-T27"
},
"step": "bind-env",
"type_lane": "key-cape-oidc-login",
"type_owner": "net-kingdom"
},
{
"candidate_descriptor_digest": "sha256:3ccc8eed545cf31f8a48feb8afa0c1355e126d9549eef9737d92d0c1b87708a8",
"descriptor": {
"actor": "human",
"adapter": "net-kingdom/keycape-registration@v1",
"adapter_source": "sha256:8af23cb5d5fff963d2eff65d8b16dcfd395931ad860357846151019a70b2ed34",
"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"
},
"findings": [],
"owner_confirmation_required": null,
"step": "register-client",
"type_lane": "key-cape-oidc-login",
"type_owner": "net-kingdom"
},
{
"candidate_descriptor_digest": "sha256:a74220eddab2709e90e6349b45940f68233c169009cccd0cdeea25a5541774a6",
"descriptor": {
"actor": "agent",
"adapter": "net-kingdom/deliver-consumer-secret@v1",
"adapter_source": "sha256:7dd5509d7dee83c2a8ff1bcd4704c336f8dbc4586223830cad34b92e6fa0bfff",
"allowed_created": [
"consumer-eso-sa",
"consumer-secretstore",
"consumer-externalsecret"
],
"effect": "change",
"lane": "consumer-owner-deployment",
"owner": "railiance-apps",
"retry_safe": false,
"revocation_scopes": [
"parent-session"
],
"step": "deliver-consumer-secret",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [],
"owner_confirmation_required": {
"lane": "consumer-owner-deployment",
"schema": "netkingdom.consumer-secret-delivery.v1",
"task": "NK-WP-0048-T28"
},
"step": "deliver-consumer-secret",
"type_lane": null,
"type_owner": "{consumer_owner_repo}"
},
{
"candidate_descriptor_digest": "sha256:366e8829dd6363e69abd817ea0d4ea966ac1ebcf994493b9bf9eee1a648a6ce0",
"descriptor": {
"actor": "human",
"adapter": "net-kingdom/apps-handover@v1",
"adapter_source": "sha256:4ad0396928f71bfb57760c414237b6cfa06aea57ccca2ef1293e63ab22f2953a",
"allowed_created": [
"handover-receipt"
],
"effect": "change",
"lane": "apps-consumer-handover",
"owner": "apps",
"retry_safe": false,
"revocation_scopes": [],
"step": "hand-over",
"target_parameter": "client_id",
"version": "warden.case.execution.v1"
},
"findings": [
"refused by Warden v1 Descriptor.decode",
"owner 'apps' != type owner 'railiance-apps'"
],
"owner_confirmation_required": {
"lane": "apps-consumer-handover",
"owner": "apps",
"schema": "netkingdom.apps-handover.attempt.v1",
"task": "NK-WP-0048-T29"
},
"step": "hand-over",
"type_lane": null,
"type_owner": "{consumer_owner_repo}"
}
],
"task": "NK-WP-0048-T30",
"type": "oidc-browser-relying-party@1.2"
}

View file

@ -0,0 +1,118 @@
"""Synthetic positive and refusal fixtures for the T30 1.2 descriptor composer."""
from dataclasses import replace
import importlib.util
from pathlib import Path
import unittest
from warden.case_run import Descriptor
from warden.case_receipt_state import Refused
ROOT = Path(__file__).parent
spec = importlib.util.spec_from_file_location('compose_12', ROOT / 'compose-1.2-descriptors.py')
c = importlib.util.module_from_spec(spec)
spec.loader.exec_module(c)
CHECKER = 'sha256:' + 'b' * 64
PARAMS = {'consumer_owner_repo': 'railiance-apps'}
def synthetic(type_spec):
"""One kernel-valid descriptor per 1.2 step, owner/lane taken from the type."""
out = []
for entry in type_spec['follow_through']:
out.append((entry['id'], Descriptor(
'warden.case.execution.v1', entry['id'], entry['owner'].format(**PARAMS),
f"net-kingdom/{entry['id']}@v1", 'sha256:' + 'a' * 64, 'human',
entry.get('lane', 'proposed-lane'), 'client_id', 'change', ('created',),
False, ('parent-session',))))
return out
class ComposeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.spec, cls.canonical = c.load_types()
cls.good = synthetic(cls.spec)
def compose(self, candidates, canonical=None):
return c.compose(self.spec, candidates, PARAMS, canonical or self.canonical)
def test_complete_seven_step_map(self):
composed = self.compose(self.good)
self.assertEqual(list(composed), [e['id'] for e in self.spec['follow_through']])
self.assertEqual(len(composed), 7)
self.assertTrue(all(v.startswith('sha256:') for v in composed.values()))
def test_partial_map_refused(self):
with self.assertRaisesRegex(Refused, 'partial'):
self.compose(self.good[:-1])
def test_duplicate_step_refused(self):
with self.assertRaisesRegex(Refused, 'duplicate'):
self.compose(self.good + [self.good[0]])
def test_unknown_step_refused(self):
bogus = ('rotate-secret', replace(self.good[0][1], step='rotate-secret'))
with self.assertRaisesRegex(Refused, 'unknown'):
self.compose(self.good[:-1] + [bogus])
def test_step_name_mismatch_refused(self):
bad = [(s, replace(d, step='bind-env') if s == 'ensure-groups' else d)
for s, d in self.good]
with self.assertRaisesRegex(Refused, 'names step'):
self.compose(bad)
def test_owner_mismatch_refused(self):
bad = [(s, replace(d, owner='apps') if s == 'hand-over' else d) for s, d in self.good]
with self.assertRaisesRegex(Refused, 'owner'):
self.compose(bad)
def test_lane_mismatch_refused(self):
bad = [(s, replace(d, lane='other-lane') if s == 'register-client' else d)
for s, d in self.good]
with self.assertRaisesRegex(Refused, 'lane'):
self.compose(bad)
def test_kernel_invalid_descriptor_refused(self):
bad = [(s, replace(d, revocation_scopes=()) if s == 'ensure-groups' else d)
for s, d in self.good]
with self.assertRaisesRegex(Refused, 'kernel'):
self.compose(bad)
def test_canonical_change_refused(self):
with self.assertRaisesRegex(Refused, '1.1'):
self.compose(self.good, canonical='sha256:' + '0' * 64)
def test_only_explicit_12_root(self):
with self.assertRaisesRegex(Refused, 'explicit 1.2'):
c.load_types(version_roots={'1.1': c.VERSION_ROOTS['1.2']})
with self.assertRaisesRegex(Refused, 'explicit 1.2'):
c.load_types(version_roots={})
def test_canonical_default_bytes_untouched(self):
path = c.DEFAULT_ROOT / c.TYPE_ID / 'type.yaml'
before = path.read_bytes()
c.artifact(CHECKER, PARAMS)
self.assertEqual(path.read_bytes(), before)
def test_current_adapters_do_not_compose(self):
# Pins the present source findings; this test must change when they are fixed.
doc = c.artifact(CHECKER, PARAMS)
self.assertFalse(doc['complete_executable_descriptor_set'])
self.assertIsNone(doc['execution_descriptors'])
self.assertFalse(doc['admitted'] or doc['owner_review'] or doc['dispatch_permitted'])
findings = {r['step']: r['findings'] for r in doc['steps']}
self.assertTrue(findings['ensure-groups'])
self.assertTrue(findings['mint-secret'])
self.assertTrue(any('owner' in f for f in findings['hand-over']))
self.assertEqual(findings['register-client'], [])
flagged = {r['step'] for r in doc['steps'] if r['owner_confirmation_required']}
self.assertTrue({'mint-secret', 'deliver-secret', 'bind-env', 'hand-over'} <= flagged)
def test_adapters_need_bounded_inputs(self):
with self.assertRaises(Refused):
c.adapter_descriptors('not-a-digest', 'railiance-apps')
if __name__ == '__main__':
unittest.main()