From 60b41a1f51e96ea6e13c5060fffb3358fc39dc18 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 17 Oct 2026 08:07:59 +0200 Subject: [PATCH] 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 Assistant: claude-code Assistant-Model: opus Assistant-Process: 903097@bnt-lap001 Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23 --- .../k8s/keycape/compose-1.2-descriptors.py | 231 +++++++++++++++++ .../descriptor-composition-1.2.candidate.json | 234 ++++++++++++++++++ .../keycape/test_compose_1_2_descriptors.py | 118 +++++++++ 3 files changed, 583 insertions(+) create mode 100644 sso-mfa/k8s/keycape/compose-1.2-descriptors.py create mode 100644 sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json create mode 100644 sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py diff --git a/sso-mfa/k8s/keycape/compose-1.2-descriptors.py b/sso-mfa/k8s/keycape/compose-1.2-descriptors.py new file mode 100644 index 0000000..0fb76f9 --- /dev/null +++ b/sso-mfa/k8s/keycape/compose-1.2-descriptors.py @@ -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()) diff --git a/sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json b/sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json new file mode 100644 index 0000000..3d53fe2 --- /dev/null +++ b/sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json @@ -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" +} diff --git a/sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py b/sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py new file mode 100644 index 0000000..4dff5d3 --- /dev/null +++ b/sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py @@ -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()