ensure-groups and hand-over accept 1.3 via explicit version_root only (1.1
stays default; hand-over applies 1.2 owner handling to 1.3).
deliver-consumer-secret accepts exactly {1.2, 1.3}. compose-1.2-descriptors
gains --version {1.2,1.3} (default 1.2) and refuses cleanly when
security-case-type-revisions/<version> is absent. The committed 1.2
composition artifact is regenerated with its recorded args: only the adapter
source and descriptor digests of the three edited adapters change.
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
268 lines
12 KiB
Python
268 lines
12 KiB
Python
"""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.
|
|
|
|
Generalised (NK-WP-0048) to a selectable successor version via --version
|
|
('1.2' default, or '1.3' = 1.2 content plus execution_descriptors). The type is
|
|
loaded from security-case-type-revisions/<version>/ through the explicit version
|
|
root, and each version-aware adapter must expose SELECTABLE_TYPES[<version>].
|
|
The file name is kept so review targets do not move; with the default the
|
|
output is byte-identical to the 1.2-only composer.
|
|
"""
|
|
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'
|
|
REVISIONS = REPO / 'security-case-type-revisions'
|
|
SUCCESSOR_VERSIONS = ('1.2', '1.3')
|
|
DEFAULT_VERSION = '1.2'
|
|
VERSION_ROOTS = {'1.2': 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),
|
|
}
|
|
|
|
# Names (T25-T29) that the type does not fix. Worker-proposed, then confirmed by
|
|
# the founder on 2026-10-17; the artifact records them as names_confirmed.
|
|
NAMES_CONFIRMED = {'by': 'founder', 'on': '2026-10-17'}
|
|
PROPOSED = {
|
|
# Actor 'human' (founder, 2026-10-17): the kernel refuses agent mint; only the schema is open.
|
|
'mint-secret': {'task': 'NK-WP-0048-T25',
|
|
'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', '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 _version(version):
|
|
if version not in SUCCESSOR_VERSIONS:
|
|
raise Refused(f'unsupported successor version {version!r}; allowed {list(SUCCESSOR_VERSIONS)}')
|
|
return version
|
|
|
|
|
|
def _type_ref(version):
|
|
return f'{TYPE_ID}@{_version(version)}'
|
|
|
|
|
|
def load_types(default_root=DEFAULT_ROOT, version_roots=None, version=DEFAULT_VERSION):
|
|
"""Return (<version> spec, canonical 1.1 file digest) via explicit selection only."""
|
|
version, type_ref = _version(version), _type_ref(version)
|
|
if version_roots is None:
|
|
root = REVISIONS / version
|
|
if not root.is_dir():
|
|
raise Refused(f'revision directory security-case-type-revisions/{version} absent')
|
|
version_roots = {version: root}
|
|
v = _validator()
|
|
roots = dict(version_roots)
|
|
if set(roots) != {version}:
|
|
raise Refused(f'only an explicit {version} version root may be selected')
|
|
issues, data, _ = v.load_type(Path(default_root), type_ref, version_roots=roots)
|
|
if issues or str(data['spec'].get('version')) != version:
|
|
raise Refused(f'{version} type not loadable through explicit version-root selection')
|
|
issues, default_12, _ = v.load_type(Path(default_root), type_ref)
|
|
if not issues:
|
|
raise Refused(f'{version} 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, version=DEFAULT_VERSION):
|
|
"""Ask every step adapter for its descriptor; T13 needs the checker digest."""
|
|
type_ref = _type_ref(version)
|
|
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(version) != type_ref:
|
|
raise Refused(f'{step}: adapter has no explicit {version} version-root path')
|
|
if step == 'register-client':
|
|
desc, source = m.descriptor(check_config_digest), m.source_digest(check_config_digest)
|
|
elif step in ('deliver-consumer-secret', 'hand-over'):
|
|
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,
|
|
version=DEFAULT_VERSION, version_roots=None):
|
|
"""Return {step: descriptor digest} for the complete <version> 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(f'{version} 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 {version} type")
|
|
if 'lane' in entry and desc.lane != entry['lane']:
|
|
raise Refused(f"{entry['id']}: lane {desc.lane!r} does not match the {version} type")
|
|
_, after = load_types(default_root, version_roots, version)
|
|
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, version=DEFAULT_VERSION):
|
|
"""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(f'step not in {version} 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': None,
|
|
'names_confirmed': dict(proposed, **NAMES_CONFIRMED) if proposed else None})
|
|
return rows
|
|
|
|
|
|
def artifact(check_config_digest, params, version=DEFAULT_VERSION):
|
|
type_spec, canonical = load_types(version=version)
|
|
candidates = adapter_descriptors(check_config_digest, params['consumer_owner_repo'], version)
|
|
try:
|
|
composed, refusal = compose(type_spec, candidates, params, canonical, version=version), 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(version),
|
|
'selection': (f'explicit version root {version}=security-case-type-revisions/{version}; '
|
|
'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, version),
|
|
'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)
|
|
p.add_argument('--version', choices=SUCCESSOR_VERSIONS, default=DEFAULT_VERSION)
|
|
a = p.parse_args(argv)
|
|
try:
|
|
doc = artifact(a.check_config_digest, {'consumer_owner_repo': a.consumer_owner_repo}, a.version)
|
|
except Refused as exc:
|
|
sys.stderr.write(f'refused: {exc}\n')
|
|
return 3
|
|
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())
|