Lane key-cape-oidc-login -> keycape-attended-case-change in the register-client, ensure-groups and bind-env descriptors; deliver-consumer-secret actor agent -> human; verify_consumer reports a value-free, exactly-once probe record list bound into the observation digest. Composition artifacts, type.yaml and admit/compose tests are left for the orchestrator to regenerate. 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
219 lines
12 KiB
Python
219 lines
12 KiB
Python
"""Unexercised, injection-only Apps-owned hand-over seam for Warden's v1 kernel.
|
|
|
|
Source candidate for NK-WP-0048-T29. No CLI, credentials, consumer access or
|
|
live receipt store is installed here. Consumer verification, hand-over receipt
|
|
recording/removal and owner evidence publication/authentication are mandatory
|
|
injected capabilities. Apps owner source review precedes any pinning.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, replace
|
|
import json
|
|
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 = ('handover-adapter.py',)
|
|
REFERENCE = 'net-kingdom/apps-handover@v1'
|
|
CANONICAL_TYPE = 'oidc-browser-relying-party@1.1'
|
|
# 1.2 and 1.3 are reachable only by explicit version-root selection, never by default.
|
|
# 1.3 is 1.2 plus execution_descriptors, so 1.2 owner handling applies to it.
|
|
SELECTABLE_TYPES = {'1.1': CANONICAL_TYPE, '1.2': 'oidc-browser-relying-party@1.2',
|
|
'1.3': 'oidc-browser-relying-party@1.3'}
|
|
OWNER_ROOTS = frozenset({'1.2', '1.3'})
|
|
RECEIPT = 'handover-receipt'
|
|
# Value-free probe records verify_consumer must report, each exactly once (RAPPS-1.3-C2).
|
|
PROBES = frozenset({'member-login', 'non-member-refused', 'membership-add-grants',
|
|
'membership-remove-revokes'})
|
|
SEEN_KEYS = frozenset({'client_id', 'enforces_groups', 'groups', 'login_verified', 'observations'})
|
|
|
|
|
|
def _observations(value):
|
|
"""Return the probe records in canonical order, or None if the contract differs."""
|
|
if not isinstance(value, list) or len(value) != len(PROBES):
|
|
return None
|
|
records = []
|
|
for rec in value:
|
|
if not (isinstance(rec, dict) and set(rec) == {'probe', 'evidence'}
|
|
and isinstance(rec['probe'], str) and rec['probe'] in PROBES
|
|
and isinstance(rec['evidence'], str) and IDENTIFIER.fullmatch(rec['evidence'])):
|
|
return None
|
|
records.append({'probe': rec['probe'], 'evidence': rec['evidence']})
|
|
if {r['probe'] for r in records} != PROBES:
|
|
return None
|
|
return sorted(records, key=lambda r: r['probe'])
|
|
|
|
|
|
def source_digest():
|
|
composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}}
|
|
return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode())
|
|
|
|
|
|
def descriptor(owner):
|
|
"""The owner is the case's consumer_owner_repo (type 1.2 '{consumer_owner_repo}')."""
|
|
if not isinstance(owner, str) or not IDENTIFIER.fullmatch(owner):
|
|
raise Refused('bounded consumer owner required')
|
|
return Descriptor('warden.case.execution.v1', 'hand-over', owner, REFERENCE,
|
|
source_digest(), 'human', 'apps-consumer-handover', 'client_id',
|
|
'change', (RECEIPT,), False, ('parent-session',))
|
|
|
|
|
|
def _case(case_source, version_root):
|
|
case = yaml.safe_load(case_source)
|
|
spec = case.get('spec') if isinstance(case, dict) else None
|
|
params = spec.get('parameters') if isinstance(spec, dict) else None
|
|
if not isinstance(params, dict) or spec.get('type') != SELECTABLE_TYPES.get(version_root):
|
|
raise Refused('case type differs from selected version root')
|
|
client_id, groups = params.get('client_id'), params.get('access_groups')
|
|
if not isinstance(client_id, str) or not IDENTIFIER.fullmatch(client_id):
|
|
raise Refused('bounded client_id required')
|
|
groups = tuple(sorted(g.strip() for g in groups.split(','))) if isinstance(groups, str) else ()
|
|
if not groups or not all(IDENTIFIER.fullmatch(g) for g in groups):
|
|
raise Refused('hand-over requires access_groups the consumer enforces')
|
|
owner = params.get('consumer_owner_repo')
|
|
if version_root in OWNER_ROOTS and (not isinstance(owner, str) or not IDENTIFIER.fullmatch(owner)):
|
|
raise Refused('1.2/1.3 hand-over requires a bounded consumer_owner_repo')
|
|
return client_id, groups, owner if version_root in OWNER_ROOTS else None
|
|
|
|
|
|
def build(*, admitted: Invocation, case_source: bytes, case_acceptance: str,
|
|
verify_consumer, record_handover, remove_handover, publish, authenticate,
|
|
revoke_session, version_root: str = '1.1'):
|
|
"""Bind one admitted case; return a closed kernel Adapter.
|
|
|
|
verify_consumer(client_id, groups)->dict observes the consumer and must report
|
|
{'client_id', 'enforces_groups', 'groups', 'login_verified', 'observations'} for
|
|
exactly this case; 'observations' is a value-free list of {probe, evidence}
|
|
records, one per probe in PROBES, evidence an owner evidence IDENTIFIER. The
|
|
observation digest covers the whole report, observations included. record_handover(client_id, observation_digest)->(identifier, created)
|
|
durably records the hand-over receipt; remove_handover(identifier) removes only
|
|
that receipt. publish/authenticate are owner evidence capabilities.
|
|
revoke_session(invocation)->identifier revokes this attempt's parent session
|
|
in a finally block, so even a failed attempt ends the session.
|
|
|
|
The descriptor owner is admitted.owner; a 1.2 case must name the same
|
|
consumer_owner_repo. A 1.1 case carries no such parameter and is bound to
|
|
the admitted owner alone.
|
|
|
|
Only a receipt verifiably created by this process may be rolled back. Process
|
|
loss, ambiguity or publication failure require owner reconciliation.
|
|
"""
|
|
reviewed = source_digest()
|
|
if type(admitted) is not Invocation or not all(
|
|
isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in
|
|
(admitted.step, admitted.owner, admitted.target, admitted.actor,
|
|
admitted.principal, admitted.lane)):
|
|
raise Refused('bounded invocation required')
|
|
desc = descriptor(admitted.owner)
|
|
if (admitted.step, admitted.actor, admitted.lane) != (desc.step, desc.actor, desc.lane):
|
|
raise Refused('hand-over 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')
|
|
desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
|
if admitted.pins.adapter != digest(desc_bytes):
|
|
raise Refused('descriptor pin differs')
|
|
if not isinstance(case_acceptance, str) or digest(case_acceptance.encode()) != admitted.pins.acceptance:
|
|
raise Refused('acceptance evidence binding differs')
|
|
if not all(callable(x) for x in (verify_consumer, record_handover, remove_handover, publish, authenticate,
|
|
revoke_session)):
|
|
raise Refused('owner verification, receipt and evidence capabilities required')
|
|
with contained_output():
|
|
client_id, groups, owner = _case(case_source, version_root)
|
|
if admitted.target != client_id:
|
|
raise Refused('case target differs')
|
|
if owner is not None and owner != admitted.owner:
|
|
raise Refused('case consumer_owner_repo differs from admitted owner')
|
|
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:
|
|
raise Refused('owner source changed')
|
|
|
|
def finish(inv, outcome, objects, body, revocation):
|
|
payload = json.dumps({'schema': 'netkingdom.apps-handover.attempt.v1',
|
|
'invocation': asdict(inv), 'source': reviewed,
|
|
'outcome': outcome.value, 'created': objects,
|
|
'session_revocation': revocation, **body},
|
|
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)
|
|
return result
|
|
|
|
def invoke(inv):
|
|
binding(inv, 'run')
|
|
with contained_output():
|
|
seen = verify_consumer(client_id, groups)
|
|
if not (isinstance(seen, dict) and set(seen) == SEEN_KEYS
|
|
and seen['client_id'] == client_id and seen['enforces_groups'] is True
|
|
and seen['login_verified'] is True and tuple(sorted(seen['groups'] or ())) == groups):
|
|
raise Refused('consumer verification contract differs')
|
|
probes = _observations(seen['observations'])
|
|
if probes is None:
|
|
raise Refused('consumer observation records differ')
|
|
seen = {'client_id': client_id, 'enforces_groups': True, 'groups': list(groups),
|
|
'login_verified': True, 'observations': probes}
|
|
observation = digest(json.dumps(seen, sort_keys=True, separators=(',', ':')).encode())
|
|
with contained_output():
|
|
receipt = record_handover(client_id, observation)
|
|
if not (isinstance(receipt, tuple) and len(receipt) == 2 and isinstance(receipt[0], str)
|
|
and IDENTIFIER.fullmatch(receipt[0]) and type(receipt[1]) is bool):
|
|
raise Refused('hand-over receipt contract differs')
|
|
ref, created = receipt
|
|
objects = (RECEIPT,) if created else ()
|
|
return (Outcome.SUCCESS if created else Outcome.NO_CHANGE, objects,
|
|
{'observation': observation, 'receipt': ref}, ref if created else None)
|
|
|
|
def rollback(inv, created):
|
|
binding(inv, 'rollback')
|
|
if created != (RECEIPT,) or creation is None or not verify(*creation[:2]):
|
|
raise Refused('no authenticated case-created hand-over receipt')
|
|
ref = creation[2]
|
|
with contained_output():
|
|
removed = remove_handover(ref)
|
|
if removed is not True:
|
|
raise Refused('hand-over receipt removal unconfirmed')
|
|
return Outcome.ROLLED_BACK, (), {'removed_receipt': ref}, None
|
|
|
|
def execute(inv, operation, created=()):
|
|
nonlocal creation
|
|
try:
|
|
outcome, objects, body, ref = (invoke(inv) if operation == 'run'
|
|
else rollback(inv, created))
|
|
finally:
|
|
# Even failure must end the parent session; failures propagate to the
|
|
# kernel as 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')
|
|
result = finish(inv, outcome, objects, body, revocation)
|
|
if ref is not None:
|
|
creation = (inv, result, ref)
|
|
elif operation == 'rollback':
|
|
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, lambda inv: execute(inv, 'run'), verify,
|
|
lambda inv, created: execute(inv, 'rollback', created))
|