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,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):