INFD-WP-0010: send approval_action to the PDP for present-existing memos (compact-sitting v14)

access-engine v14 (2de0f86) admits requester service:nk-bind-executor only
when the context carries approval_action == bounded-execution. Intake records
the verified binding.action; the intake context emits it for present-existing
memos only (create-path requests keep the v13 shape); build_request admits the
key only for a non-informed-decision requester; review refuses a changed live
binding.action as binding_changed before asking the PDP. Deployed pin (v13)
unchanged.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
tegwick 2026-11-01 04:02:49 +01:00
parent 8787e1a14a
commit 80ef8408ed
5 changed files with 97 additions and 9 deletions

View file

@ -106,6 +106,7 @@ def test_presents_existing_without_creating(setup):
context = store.intake_context(memo)
assert context['approval_requester'] == ACTOR
assert context['intake_submitter'] == 'nk-bind-executor'
assert context['approval_action'] == 'bounded-execution' # compact-sitting v14
# Replay is served from the ledger without another read.
assert request(service, envelope(), token='executor-token') == (200, receipt)
assert reads == [APPROVAL]
@ -175,7 +176,9 @@ def test_existing_submitters_keep_create_path(setup):
status, receipt = request(service, base_envelope(), token='hub-token')
assert status == 201 and len(created) == 1 and reads == []
assert created[0]['binding']['actor'] == 'informed-decision'
assert store.intake_context(store.memo(receipt['memo_id']))['approval_requester'] == 'informed-decision'
context = store.intake_context(store.memo(receipt['memo_id']))
assert context['approval_requester'] == 'informed-decision'
assert 'approval_action' not in context # v13 shape unchanged for the create path
# A create-path submitter still cannot pin an approval.
assert request(service, envelope(), token='hub-token')[0] == 400
@ -248,3 +251,72 @@ def test_real_engine_object_is_presentable(setup, tmp_path):
other['idempotency_key'] = 'bea-2026-0001-r9'
other['memo']['id'] = 'bea-2026-0001-r9'
assert request(service, other, token='executor-token') == (409, {'error': 'approval_already_presented'})
class _StopAtPolicy(Exception):
pass
class _RecordingPolicy:
version = 'v14'
def __init__(self):
self.requests = []
def check(self, body):
self.requests.append(body)
raise _StopAtPolicy()
def _review(store, live):
from dataclasses import replace
from informed_decision.review import ReviewController
from test_policy_client import human
class Engine:
def get_approval(self, approval_id):
return json.loads(json.dumps(live))
policy = _RecordingPolicy()
controller = ReviewController(store, policy, lambda session: Engine(), intake_attributes=True)
return controller, policy, replace(human(), subject='keycape-human')
def test_review_sends_approval_action_to_the_pdp(setup):
service, store, _, _, _ = setup
assert request(service, envelope(), token='executor-token')[0] == 201
controller, policy, session = _review(store, approval())
with pytest.raises(_StopAtPolicy):
controller.open(session, 'bea-2026-0001-r1')
context = policy.requests[0]['context']
assert context['approval_action'] == 'bounded-execution'
assert context['approval_requester'] == ACTOR and context['intake_submitter'] == 'nk-bind-executor'
def test_review_refuses_changed_live_action_before_policy(setup):
from informed_decision.review import ReviewError
service, store, _, _, _ = setup
assert request(service, envelope(), token='executor-token')[0] == 201
controller, policy, session = _review(store, approval(**{'binding.action': 'deploy'}))
with pytest.raises(ReviewError, match='binding_changed'):
controller.open(session, 'bea-2026-0001-r1')
assert policy.requests == []
def test_build_request_admits_approval_action_only_for_present_existing(setup):
from dataclasses import replace
from informed_decision.policy import build_request
from test_policy_client import human
service, store, _, _, _ = setup
assert request(service, envelope(), token='executor-token')[0] == 201
memo = store.memo('bea-2026-0001-r1')
session = replace(human(), subject='keycape-human')
context = store.intake_context(memo)
assert build_request(session, memo, 'read', 'v14', intake_context=context)['context']['approval_action'] \
== 'bounded-execution'
with pytest.raises(ValueError):
build_request(session, memo, 'read', 'v14',
intake_context={**context, 'approval_requester': 'informed-decision'})
with pytest.raises(ValueError):
build_request(session, memo, 'read', 'v14', intake_context={**context, 'approval_scope': 'x'})
without = {k: v for k, v in context.items() if k != 'approval_action'}
assert 'approval_action' not in build_request(session, memo, 'read', 'v13', intake_context=without)['context']