Verify exact task metadata baseline and readback
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
codex 2026-10-16 10:48:21 +02:00
parent 5f30ddd90c
commit d870596cc7
8 changed files with 274 additions and 14 deletions

View file

@ -54,9 +54,10 @@ with a preceding durable CAS/confirmation sequence.
native calls continue at2. Pending/unknown/cleanup-closed state cannot supply a
next ordinal. Each native request retains its own durable intent and outcome.
8. Cleanup stays under the original distinct obligations even if business work
fails. Returned evidence explicitly says native apply is unverified: six successful
native calls do not establish total approved baseline/readback normalization,
atomic CAS, service-login provenance or native production owner acceptance.
fails. Wave 5 requires exact committed before/after snapshots before reporting
native apply verified; six HTTP successes alone remain insufficient. This is
sequential fixture acceptance, without atomic CAS, service-login provenance or
native production owner acceptance.
The owner configuration selects provider route/read credential and the existing
Secrets configuration. These are independently installed runtime inputs, never
@ -105,3 +106,10 @@ per-call served-time/use enforcement, audit/outbox and protected uncertain/crash
recovery. Then obtain and execute the exact founder-scoped attended trial.
Approval replay prevention and uncertain native-effect reconciliation remain
separate guarantees. T12, Secrets WP13 and native adoption stay open.
Wave 5 adds owner-installed exact metadata snapshots committed through the
existing expected_before/expected_after descriptor fields, per-target baseline
and complete role/policy readback comparison, durable readback/verified evidence,
and a separate cleanup-confirmed transition. The sequence remains sequential,
without native CAS or independently recoverable cleanup handles. See
[fixture acceptance](task-execution-native-fixture.md#exact-disposable-metadata-acceptance-2026-10-16-wave-5).

View file

@ -33,7 +33,7 @@ metadata calls use independently recompiled source/method/path/body. A task use
reserved for revoke; native token limits also enforce the ceiling. Business calls
check wall and monotonic deadlines before effect, with a five-second cleanup reserve.
No retry/remint, caller HCL/JSON/path, mount/AppRole/KV/SecretID or shell API exists.
The adapter cannot claim complete baseline CAS or normalized readback acceptance.
Wave 5 below adds exact sequential snapshot acceptance; native baseline CAS remains absent.
Every business/grant request records durable intent and closed status evidence.
Native cleanup has its own durable per-request intents beneath the original
@ -89,3 +89,35 @@ native token, then performs explicit synthetic owner recovery. No blind retry oc
No umbrella broker, general executor, new credential catalogue or standing service
admin entitlement is introduced. Approval CAS prevents approval replay; it cannot
make uncertain native effects exactly-once. RPF T12 and Secrets WP13 remain open.
### Exact disposable metadata acceptance (2026-10-16, wave 5)
The owner-installed `FixtureAdmission` now carries canonical JSON before/after
snapshot bytes. Their SHA-256 values must equal the existing descriptor's
`expected_before` and `expected_after`, and both snapshots must name exactly the
two compiled metadata paths. Explicit JSON null means absence. Only the actual
OpenBao empty-errors 404 is accepted as absence; denied/malformed reads fail.
These inputs contain metadata, never KV values or private token handles.
Policy comparison retains name, HCL, version and cas_required. Only the
modification timestamp is excluded, after requiring an aware timestamp and the
closed observed policy field set. Role comparison retains every returned field,
including aliases, token defaults and unexpected additions, using canonical JSON
bytes (therefore false cannot equal zero). The disposable Kubernetes fixture pins
the complete OpenBao 2.5.5 response shape. Another plugin/version/default requires
an independently reviewed owner snapshot; nothing infers new accepted defaults.
Each baseline gates its corresponding write. Policy readback occurs before role
baseline/write/readback. A role conflict can therefore leave the policy applied;
that operation remains unknown. This sequence has no native CAS, aggregate
baseline gate, concurrent-writer exclusion, or simultaneous final-state proof.
After all six calls match the committed snapshots, the same-live adapter records
readback and verified with a value-free evidence digest. The mediator reports
native_apply verified; cleanup-confirmed requires all separate cleanup receipts.
Readback success with unconfirmed cleanup cannot reach cleanup-confirmed.
Reopening SQLite cannot dispatch, reconsume, restore token/accessor handles or
retry cleanup. Native effects can survive process loss. Cleanup handles remain
memory-only: no independently authorized recovery mechanism is implemented here.
This closes disposable sequential apply acceptance only, not TSA03 or native
production recovery/admission.

View file

@ -169,12 +169,16 @@ def run_native_fixture_task(*,compiler:Compiler,journal:Journal,plan_request:dic
_fresh(secrets_cfg,handoff,descriptor)
native.metadata_call(ordinal)
result['native_calls_completed']+=1
result['readback_evidence_digest']=native.verify_metadata()
result['native_apply']='verified'
except Exception:
_unknown(journal,binding)
result['native_apply']='unknown'
finally:
result['cleanup_confirmed']=run_cleanup(journal,binding,native)['confirmed']
# Native total expected-state normalization/acceptance remains explicitly
# absent. Six successful calls cannot be relabelled a verified owner trial.
if result['native_apply']=='verified' and result['cleanup_confirmed']:
journal.transition(binding,'verified','cleanup-confirmed',result['readback_evidence_digest'])
# Readback and cleanup are independent outcomes; no recovery authority
# or native compare-and-swap is implied by either fixture result.
result['business_state']=journal.observe(binding)['state']
return result

View file

@ -32,6 +32,8 @@ class PrivateSession:
class FixtureAdmission:
"""Independently installed owner fixture inputs, not a request schema."""
descriptor: bytes
expected_before: bytes
expected_after: bytes
owner_token: str = field(repr=False)
identity_session: PrivateSession = field(repr=False)
server_process: subprocess.Popen = field(repr=False)
@ -78,6 +80,22 @@ class NativeFixtureAdapter:
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')
# Installed owner snapshots use the existing descriptor commitments. No
# request-supplied expectation or response may authorize itself.
self.expected={}
for phase in ('before','after'):
raw=getattr(admission,'expected_'+phase)
if type(raw) is not bytes or 'sha256:'+content_hash(raw)!=descriptor['expected_'+phase]:
raise Refused('owner expected-state digest mismatch')
value=json.loads(raw)
if type(value) is not dict or set(value)!=set(paths) or encoded(value)!=raw:
raise Refused('canonical exact target snapshot required')
if any(item is not None and type(item) is not dict for item in value.values()):
raise Refused('metadata snapshot object or explicit absence required')
if phase=='after' and any(item is None for item in value.values()):
raise Refused('present final metadata required')
self.expected[phase]=value
self._observed={'before':{},'after':{}}
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
@ -183,11 +201,53 @@ class NativeFixtureAdapter:
call=self.compiler.before_call(self.plan,ordinal)
status,data=self._business(call.method,call.path,self.task.token,call.payload,task=True)
self._next_metadata+=1
self._ok(status,data,{200,204,404} if ordinal in {0,3} else {200,204})
# Metadata only. The trusted owner caller must compare native normalized
# baseline/readback to separately admitted expected state. Not KV values.
try:
self._ok(status,data,{200,204,404} if ordinal in {0,3} else {200,204})
if ordinal in {0,2,3,5}:
phase='before' if ordinal in {0,3} else 'after'
value=self._metadata_snapshot(call.path,status,data)
if encoded(value)!=encoded(self.expected[phase][call.path]):
raise Refused('native exact expected-state mismatch')
self._observed[phase][call.path]=value
except Exception:
state=self.journal.observe(self.binding)['state']
if state!='unknown': self.journal.transition(self.binding,state,'unknown')
raise Refused('native baseline or readback unconfirmed') from None
return status,data
@staticmethod
def _metadata_snapshot(path,status,response):
# HTTP errors/denials are never absence. OpenBao's exact empty 404
# metadata response is the only absent representation admitted here.
if status==404 and response=={'errors':[]}:
return None
if status!=200 or response.get('errors') or type(response.get('data')) is not dict:
raise Refused('metadata read not confirmed')
value=dict(response['data'])
if path.startswith('sys/policies/acl/'):
if set(value)!={'cas_required','modified','name','policy','version'}:
raise Refused('unrecognized policy metadata fields')
modified=value.pop('modified')
if type(modified) is not str or datetime.fromisoformat(modified.replace('Z','+00:00')).tzinfo is None:
raise Refused('invalid policy modification timestamp')
# All role fields, aliases, defaults and unknown additions participate in
# exact canonical JSON comparison. True never equals integer 1 here.
return value
def verify_metadata(self):
self._fresh()
if self._next_metadata!=6 or self.journal.observe(self.binding)['state']!='applying':
raise Refused('complete same-live metadata sequence required')
for phase in ('before','after'):
if encoded(self._observed[phase])!=encoded(self.expected[phase]):
raise Refused('complete expected metadata evidence required')
evidence=content_hash(encoded({'expected_before':'sha256:'+content_hash(self.admission.expected_before),
'expected_after':'sha256:'+content_hash(self.admission.expected_after),
'scope':'per-target-sequential-readback-no-native-cas'}))
self.journal.transition(self.binding,'applying','readback',evidence)
self.journal.transition(self.binding,'readback','verified',evidence)
return evidence
def _cleanup_admitted(self, kind, digest):
self._live()
expected=next(item for item in self.cleanup if item.kind==kind)

View file

@ -99,7 +99,9 @@ def test_whole_strict_consume_confirmation_precedes_real_native_effect(owner,tmp
with whole_fixture(owner,tmp_path) as fixture:
result=invoke(fixture)
assert result['native_calls_completed']==6 and result['cleanup_confirmed']
assert result['native_apply']=='unverified' # total owner baseline/readback acceptance remains separate
assert result['native_apply']=='verified'
assert result['business_state']=='cleanup-confirmed'
assert len(result['readback_evidence_digest'])==64
obj=fixture.approval.engine.get(fixture.approval_id)
assert obj.status=='consumed'
assert obj.consumed_digest==result['request_digest']

View file

@ -78,6 +78,19 @@ def owner(compiler,bao_dev,tmp_path):
'backend_origin':origin,'cluster_uid':'disposable-fixture-only',
'operations':req['operations'],'operation_deadline':(datetime.now(timezone.utc)+timedelta(seconds=240)).isoformat(),
'backend_budget':{'ttl_seconds':120,'uses':12},'expected_before':'sha256:'+'0'*64,'expected_after':'sha256:'+'1'*64}
# Independently installed synthetic expected state, before any native write.
policy_call=compiler.before_call(plan,1);role_call=compiler.before_call(plan,4)
body=json.loads(role_call.payload)
before=m.encoded({policy_call.path:None,role_call.path:None})
after=m.encoded({policy_call.path:{'cas_required':False,'name':policy_call.path.rsplit('/',1)[1],
'policy':json.loads(policy_call.payload)['policy'],'version':1},role_call.path:{
'alias_name_source':'serviceaccount_uid','bound_service_account_names':body['bound_service_account_names'],
'bound_service_account_namespace_selector':'','bound_service_account_namespaces':body['bound_service_account_namespaces'],
'policies':body['policies'].split(','),'token_bound_cidrs':[],'token_explicit_max_ttl':0,
'token_max_ttl':0,'token_no_default_policy':False,'token_num_uses':0,'token_period':0,
'token_policies':body['policies'].split(','),'token_strictly_bind_ip':False,
'token_ttl':900,'token_type':'default','ttl':900}})
descriptor.update(expected_before=m.tagged(before),expected_after=m.tagged(after))
# Exercise the actual consumer closed descriptor parser as well; source pins,
# grant/expected-state digests remain deliberately synthetic owner fixtures.
from secrets_engine.execution_contract import ExecutionContract
@ -107,7 +120,7 @@ def owner(compiler,bao_dev,tmp_path):
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)
admission=FixtureAdmission(installed,before,after,root,identity,process)
native=NativeFixtureAdapter(compiler,plan,journal,binding,admission)
try: yield native,journal,binding,call,compiler,descriptor
finally: journal.close()
@ -140,6 +153,7 @@ def test_native_fixed_grant_metadata_denials_and_confirmed_cleanup(owner):
assert role[1]['data']['bound_service_account_names']==expected['bound_service_account_names']
assert role[1]['data']['bound_service_account_namespaces']==expected['bound_service_account_namespaces']
assert role[1]['data']['token_policies']==expected['policies'].split(',')
assert len(native.verify_metadata())==64
# Root probes native capabilities without spending the limited task token.
for path in ['sys/policies/acl/sibling','auth/kubernetes/role/sibling','secret/data/sibling','auth/token/create','sys/mounts/secret','auth/approle/role/sibling/secret-id']:
status,data=call('POST','sys/capabilities',{'token':native.task.token,'paths':[path]})
@ -152,7 +166,7 @@ def test_native_fixed_grant_metadata_denials_and_confirmed_cleanup(owner):
assert status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n'])
assert result['confirmed']
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
assert journal.observe(binding)['state']=='applying' # native success is not full owner verification
assert journal.observe(binding)['state']=='verified'
with pytest.raises(Refused): native.metadata_call(6)
with pytest.raises(Refused): run_cleanup(journal,binding,native)
@ -261,7 +275,7 @@ def test_nonloopback_estate_origin_is_refused_before_native_transport(owner):
installed=m.encoded(changed)
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)
admission=replace(native.admission,descriptor=installed)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
@ -294,7 +308,7 @@ def test_proposed_source_grant_never_materializes_even_with_matching_digest(owne
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)
admission=replace(native.admission,descriptor=installed)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404

View file

@ -0,0 +1,127 @@
"""Exact disposable native acceptance, partial effects and restart refusal."""
import json
from dataclasses import replace
import pytest
from test_task_execution_native_fixture import owner,bao_dev,compiler,admit
from task_execution_native_fixture import NativeFixtureAdapter
from task_execution_journal import Refused,Journal
from task_execution_cleanup import run_cleanup
def start(native,journal,binding):
admit(native,journal,binding);native.materialize_grant()
journal.transition(binding,'grant-issued','applying')
def test_expectation_digest_pin_rejected_before_reserve(owner):
n,j,b,c,compiler,*_=owner
with pytest.raises(Refused,match='expected-state digest'):
NativeFixtureAdapter(compiler,n.plan,j,b,replace(n.admission,expected_after=b'{}'))
assert c('GET','sys/policies/acl/'+n.policy_name)[0]==404
@pytest.mark.parametrize('target',[0,3])
def test_actual_conflicting_baseline_gates_its_target_write(owner,target):
n,j,b,c,compiler,*_=owner
call=compiler.before_call(n.plan,target+1)
existing={'policy':'path "unrelated" { capabilities = ["read"] }'} if target==0 else json.loads(call.payload)
assert c('PUT',call.path,existing)[0] in {200,204}
with j.locked():
start(n,j,b)
for i in range(target): n.metadata_call(i)
with pytest.raises(Refused,match='baseline or readback'):n.metadata_call(target)
assert j.observe(b)['state']=='unknown'
if target==3:
# Policy effect already happened: this is deliberately no aggregate CAS.
assert c('GET',compiler.before_call(n.plan,1).path)[1]['data']['policy']==json.loads(compiler.before_call(n.plan,1).payload)['policy']
assert run_cleanup(j,b,n)['confirmed']
assert j.observe(b)['state']=='unknown'
@pytest.mark.parametrize('target',[2,5])
def test_actual_readback_drift_never_verifies(owner,target):
n,j,b,c,compiler,*_=owner
with j.locked():
start(n,j,b)
for i in range(target):n.metadata_call(i)
call=compiler.before_call(n.plan,target-1)
body=json.loads(call.payload)
if target==2:body['policy']+='\npath "sibling/*" { capabilities = ["read"] }'
else:body['policies']+=',unexpected-policy'
assert c('PUT',call.path,body)[0] in {200,204}
with pytest.raises(Refused,match='baseline or readback'):n.metadata_call(target)
with pytest.raises(Refused):n.verify_metadata()
assert run_cleanup(j,b,n)['confirmed']
assert j.observe(b)['state']=='unknown'
@pytest.mark.parametrize('status,response',[(403,{'errors':[]}), (404,{}),(404,{'errors':['permission denied']}), (200,{'data':None})])
def test_only_exact_backend_absence_counts(status,response):
with pytest.raises(Refused):NativeFixtureAdapter._metadata_snapshot('auth/kubernetes/role/x',status,response)
def test_json_types_and_unknown_role_fields_remain_significant(owner):
n,j,b,c,compiler,*_=owner
original=n._http
def changed(method,path,token,body=b''):
status,response=original(method,path,token,body)
if method=='GET' and path==compiler.before_call(n.plan,5).path and status==200:
response['data']['token_num_uses']=False # False == 0 in Python; never canonical JSON.
return status,response
n._http=changed
with j.locked():
start(n,j,b)
for i in range(5):n.metadata_call(i)
with pytest.raises(Refused):n.metadata_call(5)
assert run_cleanup(j,b,n)['confirmed']
assert j.observe(b)['state']=='unknown'
def test_reopened_sqlite_cannot_resume_or_recover_memory_handles(owner):
n,j,b,c,compiler,*_=owner
with j.locked():
start(n,j,b)
n.metadata_call(0);n.metadata_call(1)
# Model loss of the live admission; handles deliberately are not reconstructed.
# The still-running disposable server proves native effect survives reopening.
location=j.directory
assert c('GET',compiler.before_call(n.plan,1).path)[0]==200
j.db.close()
# Owner journal retains the exact DB path; this is a real reopened SQLite.
reopened=Journal(location)
try:
with reopened.locked():
assert reopened.observe(b)['dispatch_allowed'] is False
with pytest.raises(Refused):reopened.reserve(b,n.cleanup)
with pytest.raises(Refused):reopened.next_call_ordinal(b)
finally:reopened.close()
# No recovery/cleanup claim: only same-process fixture teardown retains handles.
def test_verified_readback_with_real_cleanup_denial_is_not_complete(owner):
n,j,b,c,compiler,*_=owner
with j.locked():
start(n,j,b)
for i in range(6):n.metadata_call(i)
n.verify_metadata()
# Actual backend removal denies the task's self-revoke capability.
assert c('DELETE','sys/policies/acl/'+n.policy_name)[0] in {200,204}
result=run_cleanup(j,b,n)
assert result['confirmed'] is False
assert j.observe(b)['state']=='verified'
with pytest.raises(Refused):j.transition(b,'verified','cleanup-confirmed','0'*64)
assert c('POST','auth/token/lookup-accessor',{'accessor':n.task.accessor})[0]==200
def test_actual_denied_baseline_cannot_be_caught_then_write_retried(owner):
n,j,b,c,compiler,*_=owner
with j.locked():
start(n,j,b)
# Task still has update but loses read: real 403 is not absent baseline.
denied=n.hcl.replace('["read", "create", "update"]','["create", "update"]')
assert c('PUT','sys/policies/acl/'+n.policy_name,{'policy':denied})[0] in {200,204}
with pytest.raises(Refused):n.metadata_call(0)
assert j.observe(b)['state']=='unknown'
with pytest.raises(Refused):n.metadata_call(1)
assert c('GET',compiler.before_call(n.plan,1).path)[0]==404
assert run_cleanup(j,b,n)['confirmed']

View file

@ -970,6 +970,19 @@ and defaults stay unchanged. T02 still owns login/claim/TTL/use/MASON acceptance
Secrets T06 separately owns dispatcher adoption and steady-state default migration.
2026-10-16 T12 wave 5 source preparation: disposable adapter now binds owner
canonical before/after metadata snapshots to existing descriptor hashes, rejects
per-target baseline/readback mismatch, and records readback/verified evidence.
Mediator reports cleanup-confirmed only after independent fixed cleanup receipts.
Complete role metadata and policy name/HCL/version/cas are compared; validated
modification timestamp alone is excluded. This is sequential fixture acceptance,
not aggregate CAS, concurrent-writer exclusion or native production completion.
Role conflict may follow a policy write. Restart refuses dispatch and cannot
recover memory-only token/accessor cleanup handles. T12 remains progress; actual
fresh CCR/catalog/grant, source/trust/caller/policy admission, isolated owner custody,
scoped recovery and attended native acceptance remain explicit prerequisites.
2026-10-11 T11 refinement (same source-review trial): the exact Secrets Engine
v3 manifest `6bb05bac74e4939c3e42db2ae95c5f9fb4616bb20cc285811a315c26669ebce1`
and all named bytes/proof were verified and independently re-reviewed. The three