Bind typed execution context across authorization requests
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 16:28:54 +02:00
parent e24437607b
commit d8edd9e516
6 changed files with 475 additions and 4 deletions

View file

@ -47,8 +47,10 @@ for the first proof; none is selected or approved by this design.
## Exact request binding
Use the existing `authorization.build_action_request(entry, 'apply', ...)` shape
and canonical digest rules. Add a reviewed `context.execution_contract` object
Use `authorization.build_action_request(entry, 'execute-ccr', ...)` with the
existing canonical digest rules. `apply` is a distinct legacy action: its v2
policy ignores this metadata contract and must never stand in for execute-ccr.
Add a reviewed `context.execution_contract` object
before observing the claim or calling Check. Its fields are:
| Field | Binding |
@ -246,3 +248,66 @@ failure/recovery rules and native counterexamples. No success in the initial
metadata pilot unblocks these rows by default. Full T06 adoption requires the
actual selected engine production workflow to run through the corresponding
reviewed entitlement, with native effect, denial/no-fallback and cleanup proof.
## Source preparation: typed request propagation (2026-10-11)
The staged ExecutionContract descriptor is deliberately a closed, immutable
non-secret value. `parse` validates shape and captures JSON; every `as_dict`
revalidates even directly constructed instances and returns a detached copy.
It is not an owner approval, source authenticity attestation, principal
representation or proof that the requested origin is admitted. Compiler and
mediator must independently reconstruct and authenticate sources and bodies.
The exact proposed descriptor serialization refines the table above:
- Digests use `sha256:` plus 64 lowercase hexadecimal characters. UUIDs use
canonical lowercase hyphenated form. CCR and grant IDs must be distinct.
- `source_bundle` has exactly engine_dispatcher, platform_mediator, typed_ccr and
policy_auth; each has exactly an absolute normalized `path` and `sha256`.
This parses paths without reading files; pin verification remains unimplemented.
- `operations` is an ordered list of exactly method/path/payload_sha256 objects.
It accepts only GET/PUT metadata policy and auth-role endpoint shapes, with
normalized relative paths. This schema does not reconstruct bodies or prove
that all before/write/readback operations occur or target exactly two paths.
A later owner compiler must enforce those semantics before any execution.
- `backend_origin` is an explicit-port HTTP(S) origin without credentials, path,
query or fragment. Syntactic acceptance creates no approved route or network
call. Estate pinning and server authentication remain mediator obligations.
- `backend_budget` has exactly ttl_seconds (integer 30–120) and uses (integer
1–12); booleans fail. Listed calls must leave at least one cleanup use.
Renewal, remint and live per-call accounting remain outside this descriptor.
- expected_before/expected_after are non-secret state digests; operation_deadline
is timezone-aware. Before claim, refuse expired deadlines using the existing
trusted-time window. After claim and Check, refuse a deadline later than either
served approval or decision expiry. A future mediator must recheck before every
effect and provide separately authorized cleanup after business expiry.
- catalog_sha256 is a descriptor-local sorted compact JSON fingerprint of the
complete existing context.catalog_target, distinct from canonical PDP digests.
Resource catalog ID/stage, subject, tenant and purpose remain bound separately
through the unchanged canonical request hashing implementation.
The same typed descriptor passes through build_action_request, _expected_request,
resolve_approval_observation and authorize_action. Any contract on apply or another
legacy action refuses. execute-ccr without a typed valid contract, prod stage or
platform tenant refuses. execute-ccr always binds human_control=true, even on a
legacy bootstrap catalog, and cannot use an unserved/demo fallback. Invalid or
catalog-mismatched descriptors refuse before claim/PDP/CAS. Returned requests are
independent copies; later caller mutation cannot change the stored descriptor.
99 focused source tests pass across context propagation, the existing approval
join, decision validation, readiness, consume and engine auth. New tests exercise
all four actual builders, actual consume request formatting and claim-free/full
joins; mutation of operation, CCR/grant/source/body/method/path/budget/deadline/
estate/state/principal/purpose/catalog/tenant/stage refuses the original approval.
Transport/PDP stand-ins deliberately supply hypothetical human control and an
ALLOW: they prove propagation, not a shipped policy or real principal admission.
No execute-ccr CLI, identity-owner verified initiating-principal interface,
new access policy package, typed owner compiler, task-grant materializer,
independent authenticated consume-confirmation integration, durable admission
journal, restricted backend, per-call deadline checks or native entitlement is
supplied by this stage. Existing cfg subject strings remain legacy provenance;
this change does not make them verified human representation. Existing generic
consume receipt validation limitations remain explicitly separate. service-jwt
readiness guard/default and ordinary actions remain unchanged. T01 stays in
progress, T03 stays wait, WP13 stays proposed, and WP8-T06 stays open.

View file

@ -19,6 +19,7 @@ from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from secrets_engine.approval_auth import approval_auth_configured, approval_token, credential_urlopen
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision
@ -148,6 +149,7 @@ def _expected_request(
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
) -> dict[str, Any]:
"""Build the exact CheckRequest both steps must agree on.
@ -175,6 +177,7 @@ def _expected_request(
fields=fields,
policy_targets=policy_targets,
auth_targets=auth_targets,
execution_contract=execution_contract,
)
@ -230,6 +233,7 @@ def resolve_approval_observation(
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
opener: Callable[..., Any] | None = None,
) -> ApprovalObservation | None:
"""Observe a fresh fact and prepare the claim-bearing request (step 1).
@ -238,20 +242,30 @@ def resolve_approval_observation(
human control raises instead, so no stage/demo fallback can discharge it.
Correspondence and a consume binding require authorize_action's PDP step.
"""
execution_request = None
if action == "execute-ccr" or execution_contract is not None:
execution_request = _expected_request(
cfg, entry, action, fields=fields, policy_targets=policy_targets,
auth_targets=auth_targets, execution_contract=execution_contract,
)
# The caller's descriptor is not authenticated principal/owner evidence.
# This requirement cannot disappear on an unserved/demo lane.
base_url = str(getattr(cfg, "approval_url", "") or "")
auth_configured = approval_auth_configured(cfg)
authorization_id = _authorization_id(entry, decision)
required_human_control = human_control_required(entry)
required_human_control = action == "execute-ccr" or human_control_required(entry)
if not base_url or not auth_configured or not authorization_id:
if required_human_control:
raise DecisionError("human_control requires a served approval claim and exact action binding")
return None
expected_request = _expected_request(
expected_request = execution_request or _expected_request(
cfg, entry, action,
fields=fields, policy_targets=policy_targets, auth_targets=auth_targets,
execution_contract=execution_contract,
)
require_execution_deadline(expected_request, time_window=read_window(cfg))
claim = fetch_approval_claim(
base_url=base_url,
token_provider=lambda: approval_token(cfg, scope="approval:read"),
@ -260,6 +274,8 @@ def resolve_approval_observation(
)
observe_pdp_approval_claim(claim, approval_id=authorization_id,
require_human_control=required_human_control, time_window=read_window(cfg))
require_execution_deadline(expected_request, time_window=read_window(cfg),
bounds=(claim["validity"]["expires_at"],))
expected_request["context"]["approval"] = claim
return ApprovalObservation(authorization_id, expected_request, claim)
@ -273,6 +289,7 @@ def authorize_action(
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
opener: Callable[..., Any] | None = None,
pdp_opener: Callable[..., Any] | None = None,
) -> AuthorizedAction | None:
@ -288,6 +305,7 @@ def authorize_action(
fields=fields,
policy_targets=policy_targets,
auth_targets=auth_targets,
execution_contract=execution_contract,
opener=opener,
)
if observation is None:
@ -334,6 +352,8 @@ def authorize_action(
require_human_control=expected_request["context"].get("human_control") is True,
time_window=window,
)
require_execution_deadline(expected_request, time_window=window,
bounds=(observation.claim["validity"]["expires_at"], validated.expires_at))
if validated.action != action:
raise DecisionError("access-engine decision does not bind this action")
return AuthorizedAction(

View file

@ -18,6 +18,7 @@ from typing import Any
from secrets_engine.catalog import CatalogEntry, human_control_required
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, attach_execution_contract
from secrets_engine.application_time import validity_bounds
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@ -67,6 +68,7 @@ def build_action_request(
auth_targets: list[str] | tuple[str, ...] = (),
request_id: str = "",
tenant: str = REQUEST_TENANT,
execution_contract: ExecutionContract | None = None,
) -> dict[str, Any]:
"""Build the exact normalized secrets-engine profile for flex-auth.
@ -121,6 +123,7 @@ def build_action_request(
},
}
)
attach_execution_contract(request, execution_contract)
if human_control_required(entry):
# Bind the requested control to the exact PDP submission. This is a
# requirement on the observed approval, not an authorization verdict.

View file

@ -0,0 +1,176 @@
"""Closed, immutable metadata descriptor; shape is never execution authority.
This source-only compiler input cannot grant an action, authenticate a principal,
prove owner source authenticity or open a backend. Those integration gates remain
outside this type. Existing actions cannot borrow the new descriptor.
"""
from __future__ import annotations
import copy
import hashlib
import json
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import PurePosixPath
from urllib.parse import urlsplit
from uuid import UUID
from secrets_engine.errors import DecisionError
VERSION = "SECRETS-EXEC-20261011-v1"
HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
KEYS = {
"contract_version", "operation_id", "operation", "execution_actor", "ccr_id",
"ccr_immutable_sha256", "execution_grant_ccr_id", "grant_immutable_sha256",
"catalog_sha256", "source_bundle", "backend_origin", "cluster_uid",
"operations", "operation_deadline", "backend_budget", "expected_before",
"expected_after",
}
SOURCE_ROLES = {"engine_dispatcher", "platform_mediator", "typed_ccr", "policy_auth"}
def _refuse() -> None:
# Do not interpolate untrusted descriptors into messages/evidence.
raise DecisionError("invalid execution_contract descriptor")
def _closed(value, keys):
if type(value) is not dict or set(value) != set(keys):
_refuse()
return value
def _text(value):
if type(value) is not str or not value or len(value) > 4096 or any(ord(c) < 32 for c in value):
_refuse()
return value
def _hash(value):
if type(value) is not str or not HASH.fullmatch(value):
_refuse()
def _path(value, *, absolute=False):
_text(value)
if (value.startswith("/") != absolute or "\\" in value or "%" in value
or "?" in value or "#" in value or ":" in value
or any(p in {"", ".", ".."} for p in value.lstrip("/").split("/"))
or str(PurePosixPath(value)) != value):
_refuse()
def _validated(value):
value = copy.deepcopy(_closed(value, KEYS))
if value['contract_version'] != VERSION or value['operation'] != 'metadata-apply-ccr' or value['execution_actor'] != 'service:secrets-engine':
_refuse()
try:
if str(UUID(_text(value['operation_id']))) != value['operation_id']:
_refuse()
except (ValueError, AttributeError):
_refuse()
for key in ['ccr_id', 'execution_grant_ccr_id', 'cluster_uid']:
_text(value[key])
if value['ccr_id'] == value['execution_grant_ccr_id']:
_refuse()
for key in ['ccr_immutable_sha256', 'grant_immutable_sha256', 'catalog_sha256', 'expected_before', 'expected_after']:
_hash(value[key])
sources = _closed(value['source_bundle'], SOURCE_ROLES)
for source in sources.values():
_closed(source, {'path', 'sha256'})
_path(source['path'], absolute=True)
_hash(source['sha256'])
try:
origin = urlsplit(_text(value['backend_origin']))
if (origin.scheme not in {'http', 'https'} or not origin.hostname
or origin.username is not None or origin.password is not None
or origin.path or origin.query or origin.fragment or origin.port is None):
_refuse()
except ValueError:
_refuse()
try:
deadline = datetime.fromisoformat(_text(value['operation_deadline']).replace('Z', '+00:00'))
if deadline.tzinfo is None:
_refuse()
except ValueError:
_refuse()
budget = _closed(value['backend_budget'], {'ttl_seconds', 'uses'})
if (type(budget['ttl_seconds']) is not int or not 30 <= budget['ttl_seconds'] <= 120
or type(budget['uses']) is not int or not 1 <= budget['uses'] <= 12):
_refuse()
operations = value['operations']
if type(operations) is not list or not 1 <= len(operations) < budget['uses']:
_refuse() # Reserve at least one cleanup use; no silent retries.
for operation in operations:
_closed(operation, {'method', 'path', 'payload_sha256'})
if type(operation['method']) is not str or operation['method'] not in {'GET', 'PUT'}:
_refuse()
_path(operation['path'])
# Only metadata pilot endpoint shapes, never KV/mount/token/secretid.
if not re.fullmatch(r'(sys/policies/acl/[A-Za-z0-9_-]+|auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+)', operation['path']):
_refuse()
_hash(operation['payload_sha256'])
return value
def catalog_target_digest(target: dict) -> str:
"""Descriptor-local sorted JSON fingerprint, distinct from PDP digest."""
encoded = json.dumps(target, sort_keys=True, ensure_ascii=False, allow_nan=False,
separators=(',', ':')).encode('utf-8')
return 'sha256:' + hashlib.sha256(encoded).hexdigest()
@dataclass(frozen=True)
class ExecutionContract:
_serialized: str
@classmethod
def parse(cls, descriptor: dict) -> 'ExecutionContract':
validated = _validated(descriptor)
return cls(json.dumps(validated, sort_keys=True, separators=(',', ':'), allow_nan=False))
def as_dict(self) -> dict:
# Revalidate even a directly constructed instance: no trusted label bypass.
try:
return _validated(json.loads(self._serialized))
except (ValueError, TypeError, RecursionError):
_refuse()
def attach_execution_contract(request: dict, contract: ExecutionContract | None) -> None:
action = request['action']
if action != 'execute-ccr':
if contract is not None:
raise DecisionError('execution_contract requires execute-ccr action')
return
if type(contract) is not ExecutionContract:
raise DecisionError('execute-ccr requires typed execution_contract')
value = contract.as_dict()
if request['resource']['attributes']['stage'] != 'prod':
raise DecisionError('execute-ccr requires prod stage')
if request['tenant'] != 'tenant:platform':
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
request['context']['execution_contract'] = value
request['context']['human_control'] = True
def require_execution_deadline(request: dict, *, time_window=None, bounds=()) -> None:
"""Check trusted-time freshness and served upper bounds before CAS binding."""
contract = request.get('context', {}).get('execution_contract')
if contract is None:
return
from secrets_engine.application_time import validity_bounds
deadline = datetime.fromisoformat(contract['operation_deadline'].replace('Z', '+00:00'))
_, upper = validity_bounds(None, time_window)
if upper >= deadline:
raise DecisionError('execution_contract deadline has expired')
for bound in bounds:
try:
limit = datetime.fromisoformat(bound.replace('Z', '+00:00'))
if limit.tzinfo is None or deadline > limit:
raise DecisionError('execution_contract deadline exceeds served authority')
except (ValueError, TypeError, AttributeError):
raise DecisionError('execution_contract requires served deadline') from None

View file

@ -0,0 +1,197 @@
"""Source-only builder tests; mocks confer no real grant or principal admission."""
import copy
from dataclasses import replace
import json
from datetime import datetime, timedelta, timezone
import pytest
from secrets_engine.approval_consume import consume_approval, _expected_request, authorize_action, resolve_approval_observation
from secrets_engine.authorization import approval_binding_digest, build_action_request, request_digest
from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest
from tests.test_catalog import VALID
from tests.test_consume_binding_join import _Cfg, _token, _opener, AUTH_ID
from tests.authorization_stub import AuthorizationStub
H = 'sha256:' + 'a' * 64
def entry():
raw = copy.deepcopy(VALID)
raw['stage'] = 'prod'
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': AUTH_ID, 'purpose': 'metadata-test'}
return validate_entry(raw)
def descriptor(e):
baseline = build_action_request(e, 'apply', subject_id='secrets-engine', subject_type='service', purpose='metadata-test')
return {
'contract_version': 'SECRETS-EXEC-20261011-v1', 'operation_id': 'cbfbced0-09d5-48c8-a1e7-a2ea4dc69153',
'operation': 'metadata-apply-ccr', 'execution_actor': 'service:secrets-engine',
'ccr_id': 'CCR-2099-0001', 'ccr_immutable_sha256': H, 'execution_grant_ccr_id': 'CCR-2099-0002',
'grant_immutable_sha256': H, 'catalog_sha256': catalog_target_digest(baseline['context']['catalog_target']),
'source_bundle': {name: {'path': '/owner/'+name+'.py', 'sha256': H}
for name in ['engine_dispatcher', 'platform_mediator', 'typed_ccr', 'policy_auth']},
'backend_origin': 'http://127.0.0.1:18200', 'cluster_uid': 'test-estate-not-live',
'operations': [{'method': 'GET', 'path': 'sys/policies/acl/test-policy', 'payload_sha256': H},
{'method': 'PUT', 'path': 'auth/netkingdom/role/test-role', 'payload_sha256': H}],
'operation_deadline': (datetime.now(timezone.utc)+timedelta(seconds=60)).isoformat(),
'backend_budget': {'ttl_seconds': 120, 'uses': 12}, 'expected_before': H, 'expected_after': H,
}
def request(e, contract):
return build_action_request(e, 'execute-ccr', subject_id='secrets-engine', subject_type='service',
purpose='metadata-test', execution_contract=contract)
def served(pdp):
claim = pdp.claim()
claim['binding']['human_control'] = True
return claim
def test_four_builders_preserve_context_and_binding(tmp_path):
e = entry(); raw = descriptor(e); contract = ExecutionContract.parse(raw)
cfg = _Cfg(_token(tmp_path), authorization_subject_id='secrets-engine', authorization_subject_type='service')
expected = request(e, contract)
assert expected['context']['human_control'] is True
assert _expected_request(cfg,e,'execute-ccr',execution_contract=contract) == expected
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp.bind_request(expected); claim = served(pdp)
observed = resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=contract,opener=_opener(claim))
assert observed.request['context']['execution_contract'] == raw
assert approval_binding_digest(observed.request) == request_digest(expected)
assert request_digest(observed.request) != request_digest(expected)
submitted=[]
def check(req, timeout=None):
value=json.loads(req.data); submitted.append(value)
return _opener(pdp.decision(value))(req)
authorized = authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
assert authorized.binding.request_digest == request_digest(submitted[0])
assert submitted[0] == observed.request
from tests.test_approval_consume import _Response
consume_bodies = []
def consume(req, timeout=None):
value = json.loads(req.data); consume_bodies.append(value)
return _Response(200, {'status':'consumed', 'approval_id':AUTH_ID,
'request_digest':value['request_digest'], 'decision_id':value['decision_id'],
'consumed_at':datetime.now(timezone.utc).isoformat(), 'idempotent':False})
confirmed = consume_approval(base_url=cfg.approval_url, token_file=cfg.approval_token_file,
binding=authorized.binding, opener=consume)
assert consume_bodies == [{'request_digest':request_digest(submitted[0]), 'decision_id':authorized.decision_id}]
assert confirmed.request_digest != request_digest(expected)
raw['operations'][0]['path']='sys/policies/acl/changed'
observed.request['context']['execution_contract']['source_bundle']['typed_ccr']['path']='/changed.py'
assert contract.as_dict()['operations'][0]['path']=='sys/policies/acl/test-policy'
assert contract.as_dict()['source_bundle']['typed_ccr']['path']=='/owner/typed_ccr.py'
@pytest.mark.parametrize('change', ['operation_id','ccr','grant','source','body','method','path','budget','deadline','state','cluster'])
def test_changed_binding_cannot_use_original_claim(tmp_path,change):
e=entry(); original=descriptor(e); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,ExecutionContract.parse(original))); claim=served(pdp)
changed=copy.deepcopy(original)
if change=='operation_id': changed[change]='775ab8f4-4731-47c8-bd98-342af22885dd'
elif change=='ccr': changed['ccr_immutable_sha256']='sha256:'+'b'*64
elif change=='grant': changed['grant_immutable_sha256']='sha256:'+'b'*64
elif change=='source': changed['source_bundle']['typed_ccr']['sha256']='sha256:'+'b'*64
elif change=='body': changed['operations'][1]['payload_sha256']='sha256:'+'b'*64
elif change=='method': changed['operations'][0]['method']='PUT'
elif change=='path': changed['operations'][0]['path']='sys/policies/acl/changed'
elif change=='budget': changed['backend_budget']['uses']=11
elif change=='deadline': changed['operation_deadline']=(datetime.now(timezone.utc)+timedelta(seconds=50)).isoformat()
elif change=='state': changed['expected_before']='sha256:'+'b'*64
elif change=='cluster': changed['cluster_uid']='different-estate'
typed=ExecutionContract.parse(changed)
assert approval_binding_digest(request(e,typed)) != pdp._pdp_digest
def check(req,timeout=None): return _opener(pdp.decision(json.loads(req.data)))(req)
with pytest.raises(DecisionError,match='digest'):
authorize_action(cfg,e,'execute-ccr',execution_contract=typed,opener=_opener(claim),pdp_opener=check)
@pytest.mark.parametrize('mutation', ['unknown','extra_nested','missing','wrong_actor','boolean_budget','oversize_budget','sibling_endpoint','path_traversal','source_relative','wrong_hash','bad_deadline','method_object'])
def test_closed_schema_refuses(mutation):
raw=descriptor(entry())
if mutation=='unknown': raw['approved']=True
elif mutation=='extra_nested': raw['backend_budget']['renewable']=True
elif mutation=='missing': del raw['source_bundle']['typed_ccr']
elif mutation=='wrong_actor': raw['execution_actor']='user:admin'
elif mutation=='boolean_budget': raw['backend_budget']['uses']=True
elif mutation=='oversize_budget': raw['backend_budget']['ttl_seconds']=121
elif mutation=='sibling_endpoint': raw['operations'][0]['path']='secret/data/x'
elif mutation=='path_traversal': raw['operations'][0]['path']='auth/a/role/../x'
elif mutation=='source_relative': raw['source_bundle']['typed_ccr']['path']='foo.py'
elif mutation=='wrong_hash': raw['expected_after']='unsafe'
elif mutation=='bad_deadline': raw['operation_deadline']='2026-10-11T10:00:00'
elif mutation=='method_object': raw['operations'][0]['method']={}
with pytest.raises(DecisionError): ExecutionContract.parse(raw)
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
authorize_action(cfg,e,action,execution_contract=contract,opener=fetch,pdp_opener=fetch)
assert calls==[]
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
cfg=_Cfg(_token(tmp_path)); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
e=entry(); cfg.approval_url=''
with pytest.raises(DecisionError,match='human_control'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=ExecutionContract.parse(descriptor(e)))
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
cfg=_Cfg(_token(tmp_path)); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@pytest.mark.parametrize('changed', ['subject', 'purpose', 'catalog', 'tenant', 'stage'])
def test_request_correspondence_changes_refuse_before_authorization(tmp_path, changed):
e = entry(); contract = ExecutionContract.parse(descriptor(e))
cfg = _Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp = AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); claim = served(pdp)
if changed == 'subject': cfg.authorization_subject_id = 'foreign-service'
elif changed == 'purpose': e = replace(e,approval={**e.approval, 'purpose':'different-purpose'})
elif changed == 'catalog': e = replace(e,path='changed/path')
elif changed == 'stage': e = replace(e,stage='test')
calls = []
def check(req,timeout=None):
calls.append('check'); return _opener(pdp.decision(json.loads(req.data)))(req)
with pytest.raises(DecisionError):
if changed == 'tenant':
build_action_request(e,'execute-ccr',subject_id='secrets-engine',subject_type='service',purpose='metadata-test',tenant='tenant:foreign',execution_contract=contract)
else:
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
if changed in {'catalog','stage','tenant'}: assert not calls
def test_raw_dictionary_and_direct_invalid_type_cannot_bypass_validation():
e = entry()
with pytest.raises(DecisionError): request(e,descriptor(e))
with pytest.raises(DecisionError): request(e,ExecutionContract('{}'))
def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)+timedelta(hours=2)).isoformat()
contract=ExecutionContract.parse(raw); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); calls=[]
with pytest.raises(DecisionError,match='exceeds served authority'):
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(served(pdp)),pdp_opener=lambda *a,**k:calls.append('check'))
assert not calls

View file

@ -35,6 +35,16 @@ owner-held ephemeral two-path token, bounded TTL/uses, durable admission and
cleanup. Independent Platform/Secrets Engine/identity/approval/PDP owner returns
are required. No foreign review is written by this draft.
2026-10-11 source-only preparation: typed execution_contract is staged through
all four request builders. The new descriptor is restricted to execute-ccr,
requires prod/platform/human control and binds the catalog fingerprint before
claim. Existing apply/other actions cannot borrow this descriptor. 99 focused
source checks verify propagation, canonical approval/full consume joins,
immutable copies and changed-binding refusals. Detailed schema/limitations are
in the execution-contract document. No source owner assent, initiating-principal
admission, new policy package, ledger/mediator/backend or native acceptance is
claimed. Independent source review remains required. Task statuses are unchanged.
## Prevent consumption without execution readiness
```task