feat(execution): bind immutable task grants before native effects
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-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
codex 2026-10-16 07:51:42 +02:00
parent 4021ca5e79
commit d042d468df
7 changed files with 460 additions and 7 deletions

View file

@ -0,0 +1,85 @@
# Proposed task-execution-grant immutable projection
RPF-WP-0035-T12 source preparation, 2026-10-16. This proposes and implements the
owner's pure projection/binding code; it approves no grant and registers no native
or legacy delegated applier. Existing production readiness remains refused.
Use the existing `task-execution-grant` CCR type proposed by Platform and
SECRETS-WP-0013. Use the existing execution descriptor's
`execution_grant_ccr_id` and `grant_immutable_sha256` join. No second consumer
execution descriptor, authorization token or broker is introduced.
`scripts/task_execution_grant.py` defines a closed CCR root with existing identity,
status/date/requester/review/target fields, effectful `execution` and `openbao`
objects, and an explicitly typed evidence event list. `immutable_digest` uses
SHA256 over sorted compact ASCII JSON of the complete validated projection.
It includes status, updated, title, requester, review requirements/approvers,
target coordinates, all execution inputs, generated HCL, token body and cleanup.
It excludes exactly `review.comments` and `evidence.events` by replacing those
lists with empty lists. No generic evidence-prefix stripping or caller-selected
exclusion exists. Unknown root/review/evidence fields refuse. Fields nested in
execution/openbao remain fully hashed and must exactly match independently
reconstructed effects when bound. Projection alone is not semantic admission.
Reviews have exactly at/reviewer/decision/comment; evidence events have exactly
at/kind/result/reference_sha256. These append-only annotations cannot supply a
runtime approval or become policy inputs. `require_append_only` independently
checks that each old list is an exact prefix of the new list and that the
immutable projection is unchanged. Removal, replacement and reordering refuse.
The owner must use known history when checking an update; a current file cannot
prove its own history. Updating `updated` or status changes the digest and requires
a new exact join. There is no guessed exclusion for convenience.
The compiler's optional independently installed `OwnerConfig.grant_filename`
selects one different CCR file under credential-change-requests. It adds exactly
that record and the pure validator source to the closed owner source-pin set.
The compiler reads verified bytes and executes the pinned validator source directly,
without a module-cache substitute. It reconstructs the entire effectful grant:
- The existing descriptor, excluding only its own `grant_immutable_sha256` to avoid
self-reference, plus action/catalogue and the actual owner source pins. The
record's own raw file hash is omitted from its own source-pin projection; all
other source hashes, including the validator, remain included.
- Deterministic `cleanup_specs(operation_id)`, its fixed private credential slots,
exact revoke/verification and delete/verification paths/methods, declared
no-issue/no-create branches, one reserved cleanup use and the cleanup obligation
after business expiry.
- Exact operation-specific task policy HCL on the two independently compiled
metadata endpoints plus revoke-self, and the exact parented service token body:
nonrenewable, no default policies, no identity policies, 30–120s TTL and twelve
uses. No caller HCL, body, handle, wildcard or mount administration is accepted.
It compares those structures with the owner record and compares the resulting
immutable SHA and grant ID with the existing descriptor fields before returning
a bound Plan. Unknown or changed effectful fields fail equality even if the caller
re-pins the record and recomputes its digest. The complete descriptor must still
match independently installed owner descriptor bytes. Raw source file hashes and
immutable hashes serve different purposes: even an annotation-only file edit
requires owner source-pin refresh and Plan reconstruction; it does not silently
change the grant immutable digest or restore an admission.
The disposable native adapter now refuses missing/unbound or proposed grants,
rebuilds the exact bound Plan from pinned owner sources at construction, checks
all source pins immediately before each native request, reconstructs each actual
metadata body via the compiler and compares actual token/ACL bodies with the bound
projection before issuing them. It still requires the same live postconsume
journal admission and the live owner-spawned disposable OpenBao process handle.
Native fault tests explicitly use synthetic approved copies in temporary directories;
no real owner CCR status, policy or grant is created or changed.
The normal credential-change CLI deliberately continues to reject this type for
execution. Adding it to LANE_MODULES would expose generic applier methods and is
not part of this source preparation. The pure validator is actually called by
the compiler/native fixture path; it is not a paper-only schema. An owner can
review this projection and later integrate source validation/recording into the
CCR tooling without enabling execution.
Remaining production work is now specific: approve/adopt this owner CCR projection;
verify owner source/grant admission and immutable target CCR semantics; integrate
authenticated principal, served exact claim/PDP/package and independent signed
consume; finish total native baseline/readback normalization/conflict control;
install bounded isolated owner runtime, trusted served deadlines/use accounting,
audit and protected recovery; then run the exact founder-scoped attended trial.
Neither an approved string nor this projection authenticates human approval.
Approval replay prevention and uncertain backend-effect reconciliation remain
separate. T12 and Secrets WP13 stay open.

View file

@ -0,0 +1,115 @@
"""Proposed task-execution-grant CCR immutable projection; no native admission.
The only excluded paths are typed append-only review.comments and evidence.events.
Status, updated, review requirements, effectful bytes and source revisions remain
immutable. A digest is provenance, never served approval or execution authority.
"""
from __future__ import annotations
import copy
import hashlib
import json
import re
from datetime import datetime
class Refused(Exception): pass
ROOT_KEYS={'id','kind','schema_version','request_type','title','status','created','updated',
'requester','review','target','execution','openbao','evidence'}
APPEND_ONLY_PATHS=(('review','comments'),('evidence','events'))
HASH=re.compile(r'sha256:[0-9a-f]{64}')
def need(value):
if not value:raise Refused('invalid task execution grant')
def encoded(value):
def json_value(item):
if type(item) is dict:
need(all(type(key) is str for key in item))
for child in item.values():json_value(child)
elif type(item) is list:
for child in item:json_value(child)
else:need(item is None or type(item) in {str,int,float,bool})
try:
json_value(value)
return json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=True,allow_nan=False).encode()
except (TypeError,ValueError,RecursionError):raise Refused('invalid task execution grant') from None
def closed(value,keys):
need(type(value) is dict and set(value)==set(keys));return value
def text(value):
need(type(value) is str and 0<len(value)<=4096 and '\x00' not in value);return value
def timestamp(value):
try:need(datetime.fromisoformat(text(value).replace('Z','+00:00')).tzinfo is not None)
except (ValueError,TypeError):raise Refused('invalid task execution grant') from None
def projection(record):
"""Validate and capture every field except the two explicit append-only lists.
Unknown keys never become implicitly excluded evidence. The independently
derived exact execution/openbao comparison belongs to bind(), below.
"""
closed(record,ROOT_KEYS)
need(record['kind']=='credential-change-request' and type(record['schema_version']) is int
and record['schema_version']==1 and record['request_type']=='task-execution-grant')
need(type(record['id']) is str and re.fullmatch(r'[A-Z0-9][A-Z0-9_.-]*',record['id']))
need(type(record['status']) is str and record['status'] in {'proposed','approved','denied','cancelled','superseded'})
for field in ('title','created','updated'):text(record[field])
requester=closed(record['requester'],{'agent','reason'})
for value in requester.values():text(value)
review=closed(record['review'],{'required','required_approvers','comments'})
need(review['required'] is True and type(review['required_approvers']) is list
and review['required_approvers']==['railiance-platform-owner','secrets-engine-owner','access-engine-owner','approval-engine-owner'])
need(type(review['comments']) is list and len(review['comments'])<=1024)
for comment in review['comments']:
closed(comment,{'at','reviewer','decision','comment'})
timestamp(comment['at']);text(comment['reviewer']);text(comment['comment'])
need(type(comment['decision']) is str and comment['decision'] in {'approved','changes_requested','declined'})
target=closed(record['target'],{'domain','tenant','workload','environment','purpose'})
need(target['tenant']=='platform' and target['workload']=='secrets-engine' and target['environment']=='production')
for value in target.values():text(value)
need(type(record['execution']) is dict and type(record['openbao']) is dict)
evidence=closed(record['evidence'],{'events'})
need(type(evidence['events']) is list and len(evidence['events'])<=1024)
for event in evidence['events']:
closed(event,{'at','kind','result','reference_sha256'})
timestamp(event['at']);text(event['kind'])
need(type(event['result']) is str and event['result'] in {'pending','passed','failed','unknown'})
need(type(event['reference_sha256']) is str and HASH.fullmatch(event['reference_sha256']))
# Deep capture refuses NaN/non-JSON structures rather than silently coercing.
value=json.loads(encoded(record))
for parent,leaf in APPEND_ONLY_PATHS:value[parent][leaf]=[]
return value
def immutable_digest(record):
return 'sha256:'+hashlib.sha256(encoded(projection(record))).hexdigest()
def require_append_only(previous,current):
need(immutable_digest(previous)==immutable_digest(current))
for parent,leaf in APPEND_ONLY_PATHS:
old=previous[parent][leaf];new=current[parent][leaf]
need(len(new)>=len(old) and new[:len(old)]==old)
def bind(record,*,grant_id,expected_execution,expected_openbao,expected_digest):
"""Compare an owner source record with independently reconstructed effects.
Does not trust status/comments as approval, read source files, contact engines,
register a delegated applier or issue any backend request.
"""
value=projection(record)
need(record['id']==grant_id and type(expected_digest) is str and HASH.fullmatch(expected_digest))
need(encoded(record['execution'])==encoded(expected_execution))
need(encoded(record['openbao'])==encoded(expected_openbao))
need(immutable_digest(record)==expected_digest)
return value

View file

@ -14,6 +14,7 @@ import http.client
import json
import subprocess
import time
from pathlib import Path
from urllib.parse import urlsplit
from task_execution_cleanup import CleanupResult
@ -44,6 +45,8 @@ class NativeFixtureAdapter:
self.compiler,self.plan,self.journal,self.binding=compiler,plan,journal,binding
self.admission=admission
self.meta=plan.metadata()
if self.meta.get('execution_grant',{}).get('status')!='approved':
raise Refused('independently source-bound synthetic approved grant required')
descriptor=json.loads(admission.descriptor)
fresh=compiler.bind_execution(compiler.compile(self.meta['request']),descriptor,admission.descriptor)
if fresh.serialized != plan.serialized or binding.plan_digest != plan.content_digest:
@ -73,6 +76,8 @@ class NativeFixtureAdapter:
paths=sorted({call.path for call in calls})
self.hcl=''.join('path '+json.dumps(path)+' {\n capabilities = ["read", "create", "update"]\n}\n' for path in paths)
self.hcl+='path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'
if self.meta['execution_grant']['openbao']['policy']!={'name':self.policy_name,'hcl':self.hcl}:
raise Refused('native grant body differs from bound owner projection')
self.cleanup=tuple(Cleanup(item['kind'],item['call_digest']) for item in cleanup_specs(binding.operation_id))
if self.meta['cleanup'] != cleanup_specs(binding.operation_id): raise Refused('cleanup plan mismatch')
self.task=None; self._mint_attempted=False; self._policy_attempted=False; self._ordinal=0; self._next_metadata=0
@ -86,7 +91,11 @@ class NativeFixtureAdapter:
self._live()
if min((self.deadline-datetime.now(timezone.utc)).total_seconds(),self.monotonic_deadline-time.monotonic())<=5:
raise Refused('business deadline exhausted')
self.compiler.before_call(self.plan,0)
# The constructor independently rebuilt the complete immutable Plan and
# grant. Recheck every pinned byte before each native effect. Metadata
# dispatch additionally reconstructs its exact ordinal/body below; grant
# creation reconstructs and compares its fixed body before issuance.
for path in self.compiler.pins:self.compiler.read(Path(path))
def _http(self, method, path, token, body=b''):
# Fixed caller-generated paths only; http.client has no proxy or redirect
@ -148,6 +157,8 @@ class NativeFixtureAdapter:
body={'policies':[self.policy_name],'no_default_policy':True,'renewable':False,
'ttl':str(self.ttl)+'s','explicit_max_ttl':str(self.ttl)+'s','num_uses':self.uses,
'type':'service','display_name':'task-exec-'+self.binding.operation_id,'no_parent':False}
if body!=self.meta['execution_grant']['openbao']['token_create_body']:
raise Refused('native token body differs from bound owner projection')
self._mint_attempted=True
status,data=self._business('POST','auth/token/create',root,encoded(body));self._ok(status,data,{200})
auth=data.get('auth',{})

View file

@ -53,6 +53,7 @@ class OwnerConfig:
catalog_directory: Path
# Exact path/hash pairs, configured independently by the owner. No request paths.
source_pins: tuple[tuple[str,str], ...]
grant_filename: str | None = None
@dataclass(frozen=True)
class Plan:
@ -111,11 +112,18 @@ class Compiler:
self.validator=config.platform_repo/'scripts/credential-change.py'
self.catalog_code=config.secrets_repo/'src/secrets_engine/catalog.py'
self.dependencies=[config.secrets_repo/'src/secrets_engine'/name for name in ('__init__.py','errors.py','redact.py')]
self.grant_record=None;self.grant_validator=None
if config.grant_filename is not None:
need(type(config.grant_filename) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*\.yaml',config.grant_filename)
and config.grant_filename!=config.ccr_filename,'owner_grant_filename_invalid')
self.grant_record=config.platform_repo/'credential-change-requests'/config.grant_filename
self.grant_validator=Path(__file__).resolve().parent/'task_execution_grant.py'
pins=dict(config.source_pins)
self.pins=types.MappingProxyType(pins)
need(len(self.pins)==len(config.source_pins) and all(type(k) is str and digest_ok(v) for k,v in self.pins.items()),'owner_pins_invalid')
# Compiler source is part of the owner plan, not just generated body hashes.
required={str(p) for p in [Path(__file__).resolve(),self.ccr,self.catalog,self.validator,self.catalog_code,*self.dependencies]}
if self.grant_record is not None:required.update({str(self.grant_record),str(self.grant_validator)})
need(required <= set(self.pins),'owner_source_pin_missing')
extra=set(self.pins)-required
need(len(extra)==1,'closed_owner_source_set_required')
@ -254,6 +262,51 @@ class Compiler:
'source_pins':dict(self.pins),'task_token_max_ttl':120,'task_token_max_uses':12,
'cleanup_uses_reserved':1,'cleanup':cleanup_specs(request['operation_id'])}))
def grant_effects(self,plan,descriptor):
"""Independently derive the one proposed grant's effectful immutable body."""
need(type(plan) is Plan and type(descriptor) is dict,'typed_grant_inputs_required')
meta=plan.metadata();req=meta['request']
need(self.compile(req).serialized==plan.serialized,'immutable_plan_changed')
_,_,calls=self._derive()
budget=descriptor['backend_budget']
need(type(budget) is dict and set(budget)=={'ttl_seconds','uses'}
and type(budget['ttl_seconds']) is int and 30<=budget['ttl_seconds']<=120
and type(budget['uses']) is int and budget['uses']==12,'fixed_grant_budget_required')
operation_id=req['operation_id'];policy_name='task-exec-'+operation_id
paths=sorted({call.path for call in calls})
hcl=''.join('path '+json.dumps(path)+' {\n capabilities = ["read", "create", "update"]\n}\n' for path in paths)
hcl+='path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'
token_body={'policies':[policy_name],'no_default_policy':True,'renewable':False,
'ttl':str(budget['ttl_seconds'])+'s','explicit_max_ttl':str(budget['ttl_seconds'])+'s',
'num_uses':12,'type':'service','display_name':policy_name,'no_parent':False}
# A grant cannot include its own immutable hash or raw file SHA. The
# existing descriptor joins it by ID+immutable SHA after this projection.
descriptor_binding={k:v for k,v in descriptor.items() if k!='grant_immutable_sha256'}
pins={path:digest for path,digest in self.pins.items() if self.grant_record is None or path!=str(self.grant_record)}
execution={'action':'execute-ccr','catalog_id':req['catalog_id'],
'descriptor_binding':descriptor_binding,'source_pins':pins,
'cleanup':cleanup_specs(operation_id),'cleanup_uses_reserved':1,
'cleanup_after_business_deadline':True}
openbao={'policy':{'name':policy_name,'hcl':hcl},'token_create_body':token_body,
'owner_parent_custody':'platform','identity_policies':[]}
return execution,openbao
def _bind_grant(self,plan,descriptor):
need(self.grant_record is not None,'owner_grant_source_required')
# Execute exactly the pinned pure projection source, never a cached module.
module=types.ModuleType('_owner_task_execution_grant')
raw=self.read(self.grant_validator)
exec(compile(raw,str(self.grant_validator),'exec'),module.__dict__)
record=yaml.load(self.read(self.grant_record),Loader=UniqueLoader)
execution,openbao=self.grant_effects(plan,descriptor)
try:
projection=module.bind(record,grant_id=descriptor['execution_grant_ccr_id'],
expected_execution=execution,expected_openbao=openbao,
expected_digest=descriptor['grant_immutable_sha256'])
except Exception:raise Refused('owner_grant_binding_mismatch') from None
need(self.read(self.grant_validator)==raw,'owner_grant_source_changed')
return projection
def bind_execution(self, plan, descriptor, owner_descriptor):
"""Bind exact engine descriptor to owner plan; no authorization is created.
@ -275,9 +328,11 @@ class Compiler:
need(all(descriptor.get(k)==v for k,v in expected.items()),'execution_descriptor_plan_mismatch')
bound = dict(meta, execution_descriptor=descriptor,
execution_descriptor_sha256=tagged(owner_descriptor))
# The full descriptor's independently admitted grant immutable digest must
# cover these cleanup commitments. No grant schema exists here yet, so this
# source binding cannot itself admit a grant/native production execution.
if self.grant_record is not None:
bound['execution_grant']=self._bind_grant(plan,descriptor)
bound['grant_immutable_sha256']=descriptor['grant_immutable_sha256']
# Without an independently pinned grant this remains preparation only.
# The native fixture adapter refuses to materialize such an unbound plan.
return Plan(encoded(bound))
def before_call(self,plan,ordinal):

View file

@ -0,0 +1,98 @@
"""Pure proposed grant projection: source provenance, never approval/native proof."""
import copy
from pathlib import Path
import sys
import pytest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
import task_execution_grant as grant
@pytest.fixture
def record():
return {'id':'CCR-FIXTURE-GRANT','kind':'credential-change-request','schema_version':1,
'request_type':'task-execution-grant','title':'Proposed source fixture','status':'proposed',
'created':'2026-10-16','updated':'2026-10-16','requester':{'agent':'fixture','reason':'Local proof'},
'review':{'required':True,'required_approvers':['railiance-platform-owner','secrets-engine-owner','access-engine-owner','approval-engine-owner'],'comments':[]},
'target':{'domain':'infotech','tenant':'platform','workload':'secrets-engine','environment':'production','purpose':'fixture'},
'execution':{'action':'execute-ccr','descriptor_binding':{'operation_deadline':'2026-10-16T12:00:00Z','ccr_immutable_sha256':'sha256:'+'a'*64},
'source_pins':{'/fixture/source':'sha256:'+'b'*64},'cleanup':[{'kind':'task-token-revoke','call_digest':'c'*64}]},
'openbao':{'policy':{'name':'task-exec-fixture','hcl':'exact owner generated HCL'},
'token_create_body':{'ttl':'120s','num_uses':12,'renewable':False}},'evidence':{'events':[]}}
def comment():return {'at':'2026-10-16T12:00:00Z','reviewer':'railiance-platform-owner','decision':'approved','comment':'Source only; no execution authority'}
def event():return {'at':'2026-10-16T12:00:00Z','kind':'test','result':'unknown','reference_sha256':'sha256:'+'e'*64}
def test_only_typed_review_comments_and_evidence_events_are_excluded(record):
changed=copy.deepcopy(record);changed['review']['comments'].append(comment());changed['evidence']['events'].append(event())
assert grant.immutable_digest(changed)==grant.immutable_digest(record)
grant.require_append_only(record,changed)
assert record['review']['comments']==[] # detached immutable projection
@pytest.mark.parametrize('field',['status','updated','title','requester','target','execution','openbao','review'])
def test_every_other_root_or_effectful_field_changes_digest(record,field):
changed=copy.deepcopy(record)
if field=='status':changed[field]='approved'
elif field=='updated':changed[field]='2026-10-17'
elif field=='title':changed[field]='different title'
elif field=='requester':changed[field]['reason']='different requester reason'
elif field=='target':changed[field]['purpose']='different purpose'
elif field=='execution':changed[field]['source_pins']['/fixture/source']='sha256:'+'f'*64
elif field=='openbao':changed[field]['policy']['hcl']='different HCL'
elif field=='review':changed[field]['required_approvers'].reverse()
if field=='review':
with pytest.raises(grant.Refused):grant.immutable_digest(changed)
else:
assert grant.immutable_digest(changed)!=grant.immutable_digest(record)
with pytest.raises(grant.Refused):grant.require_append_only(record,changed)
@pytest.mark.parametrize('location,key',[('root','audit'),('root','receipts'),('root','evidence_exclusions'),('evidence','files'),('evidence','policy'),('review','approval_ref'),('review','exclusions')])
def test_guessed_evidence_exclusions_or_unknown_fields_refuse(record,location,key):
changed=copy.deepcopy(record)
(changed if location=='root' else changed[location])[key]=[]
with pytest.raises(grant.Refused):grant.immutable_digest(changed)
def test_append_only_rejects_removal_rewrite_and_reordering(record):
previous=copy.deepcopy(record);previous['review']['comments']=[comment(),dict(comment(),comment='Second')]
previous['evidence']['events']=[event()]
for field,value in [('comments',[]),('comments',[dict(comment(),decision='declined')]),('comments',list(reversed(previous['review']['comments']))),('events',[])]:
changed=copy.deepcopy(previous);changed['review' if field=='comments' else 'evidence'][field]=value
assert grant.immutable_digest(changed)==grant.immutable_digest(previous)
with pytest.raises(grant.Refused):grant.require_append_only(previous,changed)
@pytest.mark.parametrize('change',['policy','token_budget','cleanup','source','deadline','target_ccr','wrong_hash','grant_id'])
def test_bind_reconstructs_effects_and_exact_existing_descriptor_join(record,change):
expected=copy.deepcopy(record);changed=copy.deepcopy(record);digest=grant.immutable_digest(record);grant_id=record['id']
if change=='policy':changed['openbao']['policy']['hcl']='path "*" {}'
elif change=='token_budget':changed['openbao']['token_create_body']['num_uses']=13
elif change=='cleanup':changed['execution']['cleanup'][0]['kind']='kv-delete'
elif change=='source':changed['execution']['source_pins']['/fixture/source']='sha256:'+'f'*64
elif change=='deadline':changed['execution']['descriptor_binding']['operation_deadline']='2027-01-01T00:00:00Z'
elif change=='target_ccr':changed['execution']['descriptor_binding']['ccr_immutable_sha256']='sha256:'+'f'*64
elif change=='wrong_hash':digest='sha256:'+'f'*64
elif change=='grant_id':grant_id='CCR-OTHER'
with pytest.raises(grant.Refused):grant.bind(changed,grant_id=grant_id,expected_execution=expected['execution'],expected_openbao=expected['openbao'],expected_digest=digest)
def test_proposed_source_record_can_bind_without_becoming_approved(record):
projection=grant.bind(record,grant_id=record['id'],expected_execution=record['execution'],expected_openbao=record['openbao'],expected_digest=grant.immutable_digest(record))
assert projection['status']=='proposed'
@pytest.mark.parametrize('change',['schema_bool','comment_extra','event_extra','unaware_comment','event_missing','nan','tuple','integer_key','status_list'])
def test_closed_types_refuse_instead_of_silent_coercion(record,change):
if change=='schema_bool':record['schema_version']=True
elif change=='comment_extra':record['review']['comments']=[dict(comment(),policy='unreviewed')]
elif change=='event_extra':record['evidence']['events']=[dict(event(),token='never accepted')]
elif change=='unaware_comment':record['review']['comments']=[dict(comment(),at='2026-10-16T12:00:00')]
elif change=='event_missing':record['evidence']['events']=[{'kind':'test'}]
elif change=='nan':record['execution']['value']=float('nan')
elif change=='tuple':record['execution']['value']=(1,2)
elif change=='integer_key':record['execution'][1]='invalid'
elif change=='status_list':record['status']=[]
with pytest.raises(grant.Refused):grant.immutable_digest(record)

View file

@ -3,6 +3,8 @@
These tests do not contact estate OpenBao or claim production owner acceptance.
"""
import hashlib
from dataclasses import replace
import yaml
import http.client
import json
import shutil
@ -20,6 +22,7 @@ from test_task_execution_plan import compiler,request,m
from task_execution_journal import Binding,Cleanup,Journal,Refused
from task_execution_cleanup import run_cleanup
from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter,PrivateSession
import task_execution_grant as grant
@pytest.fixture
@ -79,8 +82,29 @@ def owner(compiler,bao_dev,tmp_path):
# grant/expected-state digests remain deliberately synthetic owner fixtures.
from secrets_engine.execution_contract import ExecutionContract
descriptor=ExecutionContract.parse(descriptor).as_dict()
# Install an independently pinned synthetic grant source and its pure
# validator. No real CCR is written or approved in the owner checkout.
grantfile=compiler.config.platform_repo/'credential-change-requests/CCR-FIXTURE-GRANT.yaml'
grantfile.write_text('placeholder: true\n')
grantcode=Path(grant.__file__).resolve()
pins=dict(compiler.config.source_pins)
pins.update({str(grantfile):m.tagged(grantfile.read_bytes()),str(grantcode):m.tagged(grantcode.read_bytes())})
compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items()),grant_filename=grantfile.name))
plan=compiler.compile(req)
expected_execution,expected_openbao=compiler.grant_effects(plan,descriptor)
record={'id':descriptor['execution_grant_ccr_id'],'kind':'credential-change-request',
'schema_version':1,'request_type':'task-execution-grant','title':'Synthetic disposable fixture grant',
'status':'approved','created':'2026-10-16','updated':'2026-10-16',
'requester':{'agent':'fixture','reason':'Synthetic disposable proof; no founder approval'},
'review':{'required':True,'required_approvers':['railiance-platform-owner','secrets-engine-owner','access-engine-owner','approval-engine-owner'],'comments':[]},
'target':{'domain':'infotech','tenant':'platform','workload':'secrets-engine','environment':'production','purpose':'Synthetic metadata primitive proof'},
'execution':expected_execution,'openbao':expected_openbao,'evidence':{'events':[]}}
grantfile.write_text(yaml.safe_dump(record,sort_keys=False))
pins[str(grantfile)]=m.tagged(grantfile.read_bytes())
compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
descriptor['grant_immutable_sha256']=grant.immutable_digest(record)
installed=m.encoded(descriptor)
plan=compiler.bind_execution(plan,descriptor,installed)
plan=compiler.bind_execution(compiler.compile(req),descriptor,installed)
binding=Binding('SYNTHETIC-APPROVAL-NOT-PRODUCTION',req['request_digest'],req['operation_id'],plan.content_digest)
state=tmp_path/'private-journal';state.mkdir(mode=0o700);journal=Journal(state)
admission=FixtureAdmission(installed,root,identity,process)
@ -225,8 +249,62 @@ def test_actual_native_policy_retirement_does_not_prove_failed_token_revoke(owne
def test_nonloopback_estate_origin_is_refused_before_native_transport(owner):
native,journal,binding,call,compiler,descriptor=owner
changed=dict(descriptor,backend_origin='https://estate.invalid:8200')
# The owner grant now also binds origin. Build a matching synthetic grant so
# this case reaches the adapter's independent nonloopback refusal; ordinary
# origin substitution is already refused earlier by the grant join.
record=yaml.safe_load(compiler.grant_record.read_text())
record['execution'],record['openbao']=compiler.grant_effects(compiler.compile(native.meta['request']),changed)
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed['grant_immutable_sha256']=grant.immutable_digest(record)
installed=m.encoded(changed)
plan=compiler.bind_execution(compiler.compile(native.meta['request']),changed,installed)
plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed,installed)
changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest)
admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process)
with pytest.raises(Refused):NativeFixtureAdapter(compiler,plan,journal,changed_binding,admission)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
def test_changed_grant_immutable_join_refuses_before_any_native_grant(owner):
native,journal,binding,call,compiler,descriptor=owner
changed=dict(descriptor,grant_immutable_sha256='sha256:'+'0'*64)
with pytest.raises(m.Refused):compiler.bind_execution(compiler.compile(native.meta['request']),changed,m.encoded(changed))
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_grant_body_repin_cannot_replace_independently_derived_effects(owner):
native,journal,binding,call,compiler,descriptor=owner
record=yaml.safe_load(compiler.grant_record.read_text())
record['openbao']['policy']['hcl']='path "*" { capabilities = ["sudo"] }'
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed_descriptor=dict(descriptor,grant_immutable_sha256=grant.immutable_digest(record))
with pytest.raises(m.Refused):changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed_descriptor,m.encoded(changed_descriptor))
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_proposed_source_grant_never_materializes_even_with_matching_digest(owner):
native,journal,binding,call,compiler,descriptor=owner
record=yaml.safe_load(compiler.grant_record.read_text());record['status']='proposed'
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed_descriptor=dict(descriptor,grant_immutable_sha256=grant.immutable_digest(record))
installed=m.encoded(changed_descriptor)
plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed_descriptor,installed)
changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest)
admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_grant_source_drift_after_admission_prevents_protected_effect(owner):
native,journal,binding,call,compiler,*_=owner
with journal.locked():
admit(native,journal,binding)
record=yaml.safe_load(compiler.grant_record.read_text());record['execution']['cleanup'][0]['call_digest']='f'*64
compiler.grant_record.write_text(yaml.safe_dump(record))
with pytest.raises(m.Refused):native.materialize_grant()
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
assert run_cleanup(journal,binding,native)['confirmed']

View file

@ -1251,3 +1251,14 @@ identity/approval/consume/grant/state/source admission in this fixture is synthe
no production authority or complete native owner acceptance is claimed. Exact
remaining owner schema/admission and mediator work: docs/task-execution-native-fixture.md.
T12 stays progress; no live grant, credential/default change or owner sync performed.
2026-10-16 T12 third source package: implemented proposed task-execution-grant
immutable projection and exact compiler/native-fixture integration through the
existing descriptor grant ID/SHA fields. Only typed review.comments/evidence.events
are excluded; separate append-only checks reject removal/rewrite/reordering and
unknown guessed evidence exclusions refuse. Owner-pinned grant bytes and validator
source must match independently reconstructed descriptor/source/cleanup/ACL/token
bodies before a native fixture can materialize. Proposed source records gain no
execution authority; no real grant, policy or legacy applier registration added.
Details and remaining production gates: docs/task-execution-grant-projection.md.
T12 remains progress; no owner sync or production effect performed by this package.