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:
parent
8787e1a14a
commit
80ef8408ed
5 changed files with 97 additions and 9 deletions
|
|
@ -192,7 +192,9 @@ class IntakeService:
|
|||
metadata = {"memo_class": submission.memo_class, "review_group": submission.review_group,
|
||||
"drafted_by": submission.drafted_by, "serves": submission.serves}
|
||||
if profile is not None:
|
||||
metadata.update(profile="present_existing", approval_requester=profile.approval_actor)
|
||||
# approval_action: the verified binding.action, for the PDP (compact-sitting v14).
|
||||
metadata.update(profile="present_existing", approval_requester=profile.approval_actor,
|
||||
approval_action=submission.approval_binding["action"])
|
||||
recovered = None
|
||||
# Reserve before any network side effect. SQLite serializes concurrent poses.
|
||||
with self.store._transaction() as db:
|
||||
|
|
|
|||
|
|
@ -57,12 +57,18 @@ def submitted_digest(request):
|
|||
return "sha256:" + hashlib.sha256(encoded.encode()).hexdigest()
|
||||
|
||||
|
||||
INTAKE_CONTEXT = frozenset({"review_group", "memo_class", "intake_submitter", "approval_requester",
|
||||
"approval_human_control"})
|
||||
|
||||
|
||||
def build_request(session: HumanSession, memo, action, policy_version, *, intake_context=None):
|
||||
if action not in ACTIONS:
|
||||
raise ValueError("unsupported review action")
|
||||
extra = {} if intake_context is None else dict(intake_context)
|
||||
if extra and set(extra) != {"review_group", "memo_class", "intake_submitter", "approval_requester", "approval_human_control"}:
|
||||
if extra and set(extra) not in (INTAKE_CONTEXT, INTAKE_CONTEXT | {"approval_action"}):
|
||||
raise ValueError("exact intake policy context required")
|
||||
if "approval_action" in extra and extra["approval_requester"] == "informed-decision":
|
||||
raise ValueError("approval_action is sent only for a present-existing requester")
|
||||
if extra and action == "list":
|
||||
extra["review_recipient"] = memo.binding.principal.id
|
||||
return {"id": str(uuid.uuid4()), "tenant": "tenant:platform",
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ class ReviewController:
|
|||
if context is not None:
|
||||
current = self._approval(session, memo)
|
||||
if (current['binding'].get('actor') != context['approval_requester']
|
||||
or current['binding'].get('human_control') is not True):
|
||||
or current['binding'].get('human_control') is not True
|
||||
or ('approval_action' in context
|
||||
and current['binding'].get('action') != context['approval_action'])):
|
||||
raise ReviewError(409, 'binding_changed')
|
||||
request = build_request(session, memo, action, self.policy.version, intake_context=context)
|
||||
observation = self.policy.check(request)
|
||||
|
|
|
|||
|
|
@ -281,11 +281,17 @@ class Store:
|
|||
if (receipt['approval_id'] != memo.approval_id
|
||||
or receipt['approval_binding_digest'] != memo.approval_binding_digest):
|
||||
raise Conflict('intake receipt does not match memo')
|
||||
return {'review_group': metadata['review_group'], 'memo_class': metadata['memo_class'],
|
||||
'intake_submitter': row['submitter'],
|
||||
# present_existing names the approval's own actor; the create path is informed-decision.
|
||||
'approval_requester': metadata.get('approval_requester', 'informed-decision'),
|
||||
'approval_human_control': True}
|
||||
context = {'review_group': metadata['review_group'], 'memo_class': metadata['memo_class'],
|
||||
'intake_submitter': row['submitter'],
|
||||
# present_existing names the approval's own actor; the create path is informed-decision.
|
||||
'approval_requester': metadata.get('approval_requester', 'informed-decision'),
|
||||
'approval_human_control': True}
|
||||
if metadata.get('profile') == 'present_existing':
|
||||
# compact-sitting v14 admits a present-existing requester only for a named action.
|
||||
# A row recorded before this key existed fails closed at the PDP instead of guessing.
|
||||
if 'approval_action' in metadata:
|
||||
context['approval_action'] = metadata['approval_action']
|
||||
return context
|
||||
|
||||
def intake_context(self, memo):
|
||||
with self._connection() as db:
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue