435 lines
26 KiB
Diff
435 lines
26 KiB
Diff
|
|
diff --git a/docs/task-execution-mediator-seam.md b/docs/task-execution-mediator-seam.md
|
||
|
|
index 964688b..463c056 100644
|
||
|
|
--- a/docs/task-execution-mediator-seam.md
|
||
|
|
+++ b/docs/task-execution-mediator-seam.md
|
||
|
|
@@ -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).
|
||
|
|
diff --git a/docs/task-execution-native-fixture.md b/docs/task-execution-native-fixture.md
|
||
|
|
index 7096f1a..1dd6b91 100644
|
||
|
|
--- a/docs/task-execution-native-fixture.md
|
||
|
|
+++ b/docs/task-execution-native-fixture.md
|
||
|
|
@@ -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.
|
||
|
|
diff --git a/scripts/task_execution_mediator.py b/scripts/task_execution_mediator.py
|
||
|
|
index d42b55f..6614974 100644
|
||
|
|
--- a/scripts/task_execution_mediator.py
|
||
|
|
+++ b/scripts/task_execution_mediator.py
|
||
|
|
@@ -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
|
||
|
|
diff --git a/scripts/task_execution_native_fixture.py b/scripts/task_execution_native_fixture.py
|
||
|
|
index c9ee244..caa7da1 100644
|
||
|
|
--- a/scripts/task_execution_native_fixture.py
|
||
|
|
+++ b/scripts/task_execution_native_fixture.py
|
||
|
|
@@ -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)
|
||
|
|
diff --git a/tests/test_task_execution_mediator.py b/tests/test_task_execution_mediator.py
|
||
|
|
index 8ad9045..52024f1 100644
|
||
|
|
--- a/tests/test_task_execution_mediator.py
|
||
|
|
+++ b/tests/test_task_execution_mediator.py
|
||
|
|
@@ -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']
|
||
|
|
diff --git a/tests/test_task_execution_native_fixture.py b/tests/test_task_execution_native_fixture.py
|
||
|
|
index 2a8e419..4c54313 100644
|
||
|
|
--- a/tests/test_task_execution_native_fixture.py
|
||
|
|
+++ b/tests/test_task_execution_native_fixture.py
|
||
|
|
@@ -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
|
||
|
|
|
||
|
|
diff --git a/tests/test_task_execution_readback.py b/tests/test_task_execution_readback.py
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..5bc0b5b
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/tests/test_task_execution_readback.py
|
||
|
|
@@ -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']
|
||
|
|
diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md
|
||
|
|
index 4ea8015..8b09305 100644
|
||
|
|
--- a/workplans/RPF-WP-0035-credential-lane-implementation.md
|
||
|
|
+++ b/workplans/RPF-WP-0035-credential-lane-implementation.md
|
||
|
|
@@ -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
|