779 lines
46 KiB
Diff
779 lines
46 KiB
Diff
diff --git a/docs/task-execution-mediator-seam.md b/docs/task-execution-mediator-seam.md
|
|
new file mode 100644
|
|
index 0000000..964688b
|
|
--- /dev/null
|
|
+++ b/docs/task-execution-mediator-seam.md
|
|
@@ -0,0 +1,107 @@
|
|
+# Strict task handoff to independent consumption confirmation
|
|
+
|
|
+RPF-WP-0035-T12 source preparation, 2026-10-16. This seam calls only the existing
|
|
+fixed disposable native adapter. It installs no production origin, identity,
|
|
+permission, real grant, generic broker or backend default.
|
|
+
|
|
+## Actual starting gaps and reused owner interfaces
|
|
+
|
|
+At Platform source base d042d468 and Secrets c80a944 the native fixture used a
|
|
+synthetic direct `consume-confirmed` journal transition. No actual strict Secrets
|
|
+task handoff existed. Generic Secrets consume accepted a substituted approval ID,
|
|
+coerced idempotent values and did not require the original exact decision. Those
|
|
+are Secrets-owned defects; Platform did not copy its authorization policy or edit
|
|
+that repository. The Secrets owner prepared a separate strict module under
|
|
+SECRETS-WP-0013, referenced by exact hashes in this package's manifest.
|
|
+
|
|
+Approval Engine already provides `ExecutionConfirmationClient.consume_exact` and
|
|
+`confirm_consumption`. The latter performs an independently authenticated GET of
|
|
+the durable original approval object and filters it to the original consumption
|
|
+triplet/time. It is historical PIP evidence, not authorization. It neither validates
|
|
+an Access decision nor makes a supplied receipt authoritative. Platform's adapter
|
|
+previously assumed its first native call was journal ordinal zero, conflicting
|
|
+with a preceding durable CAS/confirmation sequence.
|
|
+
|
|
+`scripts/task_execution_mediator.py` now performs the narrow owner join:
|
|
+
|
|
+1. Require the actual `secrets_engine.task_execution_handoff` dependency; absence
|
|
+ refuses before provider credentials, journal reservation or native effects.
|
|
+ The mediator itself calls `authorize_task_execution`. It accepts neither an
|
|
+ inbound authorization object nor a caller-selected authorizer/transport callback.
|
|
+2. Independently compile the exact local catalogue/descriptor/grant and compare the
|
|
+ strict owner's detached handoff descriptor byte-for-byte. Only authorization_id
|
|
+ may be added to approval reference metadata; purpose and source semantics cannot
|
|
+ be substituted through an inbound catalogue object. Secrets owns signed Check,
|
|
+ claim/native-act/full and approval-free digest joins, caller/subject correspondence
|
|
+ and the strict prepared object. Platform does not render or duplicate that decision.
|
|
+3. Bind the actual returned full canonical digest into the owner Plan. Reserve the
|
|
+ approval/request/operation/Plan tuple and the precommitted cleanup obligations
|
|
+ once while holding the same invocation lock through cleanup.
|
|
+4. Commit a CAS call intent, invoke Secrets `consume_task_execution` once and require
|
|
+ its actual typed result with exact original approval/request/decision/time and
|
|
+ `idempotent is False`. A historical idempotent response cannot open native work.
|
|
+5. Commit a separate read intent and invoke the actual Approval owner's default
|
|
+ isolated transport, with independently installed read credential and exact same
|
|
+ owner-admitted origin. Require `confirm_consumption` to match the native act,
|
|
+ approval-free digest and original exact triplet/time. Raw approval entries and
|
|
+ provider credentials never enter journal evidence or receipts.
|
|
+6. Recheck original approval/decision/operation bounds using the existing Secrets
|
|
+ time adapter, then persist `consume-confirmed`. Any CAS/read failure, timeout,
|
|
+ denied read or mismatch records unknown and causes zero native execution. A
|
|
+ subsequent invocation cannot retry the consumed approval or persisted admission.
|
|
+7. Only then enter the fixed native grant/metadata adapter. Its next business-call
|
|
+ ordinal comes from the same live journal, so CAS and GET occupy ordinals0/1 and
|
|
+ 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.
|
|
+
|
|
+The owner configuration selects provider route/read credential and the existing
|
|
+Secrets configuration. These are independently installed runtime inputs, never
|
|
+part of an HTTP body or authority derived from a caller string. Both providers use
|
|
+real existing transports. Python import/file checks do not establish runtime
|
|
+credential isolation; production custody/enforcement remains an external gate.
|
|
+
|
|
+## Whole disposable source-server proof
|
|
+
|
|
+Tests run the actual Access `serve` binary built from the owner source, with its
|
|
+real Ed25519 signing implementation and enforce-mode TokenReview client. A local
|
|
+synthetic reviewer independently admits only the explicitly fixed service token,
|
|
+principal/audience and expiry. This is not actual Kubernetes identity proof.
|
|
+Signing keys are generated in private temporary files; the public key and real
|
|
+policy digest are independently installed in the Secrets trust file. The policy
|
|
+digest comes from an owner-local actual source CLI before any network response.
|
|
+
|
|
+The positive policy is a separately named local synthetic exact-descriptor Rego
|
|
+fixture with a real deny fixture and Rego test. It never edits or enables the
|
|
+actual task policy. The actual proposed service task package is exercised unchanged
|
|
+and returns genuine signed DENY. There is no fabricated DecisionEnvelope or
|
|
+model-only approval. Real Approval WSGI/SQLite plus distinct independently admitted
|
|
+synthetic service/human identities create, approve, consume and serve the object.
|
|
+The strict Secrets module uses its actual mandatory producer-type signature verifier,
|
|
+retained request/claim/envelope and one-shot strict CAS, followed by the independent
|
|
+Approval GET and real disposable OpenBao grant/token/metadata/cleanup requests.
|
|
+
|
|
+Tests also prove actual independent read-scope denial after successful consume:
|
|
+the durable approval is consumed, Platform remains unknown with pending read intent,
|
|
+no target native objects exist and reinvocation cannot consume again. The native
|
|
+journal is checked for known synthetic token leakage. A source purpose substitution
|
|
+refuses before authorization/CAS, and a missing strict module refuses before any
|
|
+credentials. Actual-policy denial leaves the approval approved and journal empty.
|
|
+All identity, policy/registry admission, source-bundle/baseline digests and grant
|
|
+approval in these tests are synthetic fixture inputs. These tests do not admit an
|
|
+estate caller, real grant, production transport or actual protected custody.
|
|
+
|
|
+## Remaining production work
|
|
+
|
|
+Adopt/publish the exact strict Secrets dependency and this owner seam together;
|
|
+admit the existing task-execution-grant source projection and actual identity,
|
|
+policy package, provider origin/custody and complete source revisions. The current
|
|
+actual policy remains hard-closed. Finish target CCR immutable semantics, total
|
|
+native expected-state normalization/conflict control, CCR38 provenance, actual
|
|
+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.
|
|
diff --git a/scripts/task_execution_journal.py b/scripts/task_execution_journal.py
|
|
index a16291b..8f1b9a2 100644
|
|
--- a/scripts/task_execution_journal.py
|
|
+++ b/scripts/task_execution_journal.py
|
|
@@ -231,6 +231,21 @@ class Journal:
|
|
return {"state": row[3], "evidence_digest": row[4], "dispatch_allowed": False,
|
|
"external_outcome_unknown": bool(pending)}
|
|
|
|
+ def next_call_ordinal(self, binding: Binding):
|
|
+ """Continue the same admitted invocation after owner auth/consume calls.
|
|
+
|
|
+ This never grants dispatch or resumes a persisted admission. The actual
|
|
+ before_call transaction remains the final serialization/intent guard.
|
|
+ """
|
|
+ if not self._held or self._admission != binding:
|
|
+ raise Refused("next call requires same live admission")
|
|
+ count,pending=self.db.execute("SELECT COUNT(*),COALESCE(SUM(outcome='pending'),0) FROM calls WHERE operation=?",(binding.operation_id,)).fetchone()
|
|
+ cleanup=self.db.execute("SELECT COUNT(*) FROM cleanup WHERE operation=? AND outcome!='owed'",(binding.operation_id,)).fetchone()[0]
|
|
+ state=self.db.execute("SELECT state FROM operations WHERE operation=?",(binding.operation_id,)).fetchone()[0]
|
|
+ if pending or cleanup or state in {'failed','unknown','cleanup-confirmed'} or count>63:
|
|
+ raise Refused("call sequence is uncertain, closed or exhausted")
|
|
+ return count
|
|
+
|
|
def before_call(self, binding: Binding, ordinal: int, call_digest: str):
|
|
"""Persist one exact plan-call intent before the mediator issues it.
|
|
|
|
diff --git a/scripts/task_execution_mediator.py b/scripts/task_execution_mediator.py
|
|
new file mode 100644
|
|
index 0000000..d42b55f
|
|
--- /dev/null
|
|
+++ b/scripts/task_execution_mediator.py
|
|
@@ -0,0 +1,180 @@
|
|
+"""Owner mediator seam for the existing strict Secrets handoff and Approval GET.
|
|
+
|
|
+Only the disposable NativeFixtureAdapter is callable here. No production backend,
|
|
+caller-supplied authorizer/transport, approval boolean or portable receipt exists.
|
|
+The strict Secrets module is an explicit external dependency: absence refuses
|
|
+before credential-bearing requests or native effects, never a permissive fallback.
|
|
+"""
|
|
+from __future__ import annotations
|
|
+from dataclasses import dataclass
|
|
+from datetime import datetime,timezone
|
|
+import importlib
|
|
+import json
|
|
+from pathlib import Path
|
|
+from urllib.parse import urlsplit
|
|
+
|
|
+from task_execution_cleanup import run_cleanup
|
|
+from task_execution_journal import Binding,Cleanup,Journal,Refused
|
|
+from task_execution_plan import Compiler,encoded,content_hash
|
|
+from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class ConfirmationConfig:
|
|
+ """Owner-installed independent read credential/route, never request fields."""
|
|
+ origin: str
|
|
+ token_file: Path
|
|
+ timeout_seconds: float = 3
|
|
+
|
|
+
|
|
+_HANDOFF_FIELDS={'execution_contract','approval_id','request_digest','decision_id',
|
|
+ 'native_binding','approval_binding_digest','submitted_request_digest',
|
|
+ 'decision_not_before','decision_expires_at','approval_expires_at'}
|
|
+
|
|
+
|
|
+def _strict_dependency():
|
|
+ try:
|
|
+ module=importlib.import_module('secrets_engine.task_execution_handoff')
|
|
+ for name in ['TaskExecutionAuthorization','TaskExecutionConsumption',
|
|
+ 'authorize_task_execution','consume_task_execution']:
|
|
+ if not hasattr(module,name):raise ImportError()
|
|
+ return module
|
|
+ except ImportError:
|
|
+ raise Refused('strict Secrets task execution handoff dependency unavailable') from None
|
|
+
|
|
+
|
|
+def _fresh(cfg,handoff,descriptor):
|
|
+ from secrets_engine.application_time import read_window,validity_bounds
|
|
+ lower,upper=validity_bounds(window=read_window(cfg))
|
|
+ try:
|
|
+ parse=lambda value:datetime.fromisoformat(value.replace('Z','+00:00'))
|
|
+ deadline=parse(descriptor['operation_deadline'])
|
|
+ approval=parse(handoff['approval_expires_at']);decision=parse(handoff['decision_expires_at'])
|
|
+ start=parse(handoff['decision_not_before']) if handoff['decision_not_before'] else None
|
|
+ if any(value.tzinfo is None for value in [deadline,approval,decision]) or (start is not None and start.tzinfo is None):raise ValueError()
|
|
+ if upper>=min(deadline,approval,decision) or deadline>min(approval,decision) or (start is not None and lower<start):raise ValueError()
|
|
+ except (KeyError,TypeError,ValueError,AttributeError):
|
|
+ raise Refused('original task authority deadline refused') from None
|
|
+
|
|
+
|
|
+def _intent(journal,binding,kind,fields):
|
|
+ digest=content_hash(encoded({'kind':kind,**fields}))
|
|
+ ordinal=journal.next_call_ordinal(binding)
|
|
+ journal.before_call(binding,ordinal,digest)
|
|
+ return ordinal,digest
|
|
+
|
|
+
|
|
+def _unknown(journal,binding):
|
|
+ state=journal.observe(binding)['state']
|
|
+ if state not in {'unknown','failed','cleanup-confirmed'}:
|
|
+ journal.transition(binding,state,'unknown')
|
|
+
|
|
+
|
|
+def run_native_fixture_task(*,compiler:Compiler,journal:Journal,plan_request:dict,
|
|
+ owner_descriptor:bytes,secrets_cfg,entry,
|
|
+ confirmation:ConfirmationConfig,
|
|
+ native_admission:FixtureAdmission):
|
|
+ """Authorize inside owner boundary, reserve, consume once, independently GET.
|
|
+
|
|
+ Strict authorization is executed here through the actual Secrets-owned module;
|
|
+ an inbound TaskExecutionAuthorization or arbitrary callable is never accepted.
|
|
+ This function returns closed metadata, not tokens or raw approval/decision data.
|
|
+ Positive execution requires the module's real signed PDP path; actual unadmitted
|
|
+ Access policy must still deny. Fixture policy/identity admission is independent
|
|
+ synthetic setup and cannot become production acceptance.
|
|
+ """
|
|
+ strict=_strict_dependency()
|
|
+ from approval_engine.execution_confirmation import ExecutionConfirmationClient
|
|
+ from secrets_engine.execution_contract import ExecutionContract
|
|
+ if type(confirmation) is not ConfirmationConfig or type(native_admission) is not FixtureAdmission:
|
|
+ raise Refused('independent owner provider configuration required')
|
|
+ try:
|
|
+ origin=urlsplit(confirmation.origin)
|
|
+ if origin.scheme not in {'http','https'} or origin.hostname!='127.0.0.1' or not origin.port or origin.username or origin.password or origin.path or origin.query or origin.fragment:
|
|
+ raise ValueError()
|
|
+ if confirmation.origin!=str(getattr(secrets_cfg,'approval_url','')).rstrip('/'):
|
|
+ raise ValueError()
|
|
+ descriptor=ExecutionContract.parse(json.loads(owner_descriptor)).as_dict()
|
|
+ if encoded(descriptor)!=owner_descriptor or native_admission.descriptor!=owner_descriptor:
|
|
+ raise ValueError()
|
|
+ except Exception:
|
|
+ raise Refused('exact owner descriptor or provider route refused') from None
|
|
+ # An independently pinned owner catalogue must match every engine field. Its
|
|
+ # approval IDs are source references, never runtime authority.
|
|
+ proposal=compiler.proposal()
|
|
+ target={key:getattr(entry,key) for key in proposal['catalog_target']}
|
|
+ if encoded(target)!=encoded(proposal['catalog_target']) or entry.id!=proposal['catalog_id'] or entry.stage!='prod':
|
|
+ raise Refused('owner catalogue does not match strict task request')
|
|
+ snapshot={path:compiler.read(Path(path)) for path in compiler.pins}
|
|
+ owner_entry=compiler._catalogue_entry(snapshot)
|
|
+ # Only the served approval pointer is added after issue. Purpose or other
|
|
+ # source approval semantics cannot be substituted through an inbound entry.
|
|
+ without_reference=lambda value:{key:item for key,item in value.items() if key!='authorization_id'}
|
|
+ if encoded(without_reference(entry.approval))!=encoded(without_reference(owner_entry.approval)):
|
|
+ raise Refused('owner approval metadata differs from source')
|
|
+ client=ExecutionConfirmationClient(confirmation.origin,confirmation.token_file,
|
|
+ timeout_seconds=confirmation.timeout_seconds)
|
|
+ with journal.locked():
|
|
+ authorization=strict.authorize_task_execution(secrets_cfg,entry,
|
|
+ execution_contract=ExecutionContract.parse(descriptor))
|
|
+ if type(authorization) is not strict.TaskExecutionAuthorization:
|
|
+ raise Refused('strict task authorization missing')
|
|
+ handoff=authorization.as_owner_handoff()
|
|
+ if type(handoff) is not dict or set(handoff)!=_HANDOFF_FIELDS or encoded(handoff['execution_contract'])!=owner_descriptor:
|
|
+ raise Refused('strict task handoff does not match owner descriptor')
|
|
+ _fresh(secrets_cfg,handoff,descriptor)
|
|
+ request=dict(plan_request,request_digest=handoff['request_digest'])
|
|
+ plan=compiler.bind_execution(compiler.compile(request),descriptor,owner_descriptor)
|
|
+ binding=Binding(handoff['approval_id'],handoff['request_digest'],descriptor['operation_id'],plan.content_digest)
|
|
+ # Constructor validates fixed native origin/fixture process/grant sources
|
|
+ # but performs no native HTTP request or credential acquisition.
|
|
+ native=NativeFixtureAdapter(compiler,plan,journal,binding,native_admission)
|
|
+ journal.reserve(binding,native.cleanup)
|
|
+ fields={key:handoff[key] for key in ['approval_id','request_digest','decision_id']}
|
|
+ try:
|
|
+ ordinal,digest=_intent(journal,binding,'strict-secrets-task-consume',fields)
|
|
+ consumed=strict.consume_task_execution(secrets_cfg,authorization)
|
|
+ if type(consumed) is not strict.TaskExecutionConsumption:
|
|
+ raise Refused('strict task consumption missing')
|
|
+ receipt={key:getattr(consumed,key) for key in ['approval_id','request_digest','decision_id','consumed_at','idempotent']}
|
|
+ if any(receipt[key]!=value for key,value in fields.items()) or receipt['idempotent'] is not False:
|
|
+ raise Refused('historical or mismatched task consumption refused')
|
|
+ stamp=datetime.fromisoformat(receipt['consumed_at'].replace('Z','+00:00'))
|
|
+ if stamp.tzinfo is None:raise Refused('consumption timestamp missing')
|
|
+ journal.after_call(binding,ordinal,digest,content_hash(encoded(receipt)))
|
|
+ _fresh(secrets_cfg,handoff,descriptor)
|
|
+ ordinal,digest=_intent(journal,binding,'independent-approval-consumption-read',fields)
|
|
+ confirmed=client.confirm_consumption(binding.approval_id,binding.request_digest,
|
|
+ handoff['decision_id'],expected_binding=handoff['native_binding'],
|
|
+ expected_pdp_digest=handoff['approval_binding_digest'])
|
|
+ if confirmed!={key:receipt[key] for key in ['approval_id','request_digest','decision_id','consumed_at']}:
|
|
+ raise Refused('independent consumption correspondence refused')
|
|
+ journal.after_call(binding,ordinal,digest,content_hash(encoded(confirmed)))
|
|
+ _fresh(secrets_cfg,handoff,descriptor)
|
|
+ journal.transition(binding,'reserved','consume-confirmed',content_hash(encoded(confirmed)))
|
|
+ except Exception:
|
|
+ _unknown(journal,binding)
|
|
+ raise Refused('task consumption or independent confirmation unconfirmed; no native execution') from None
|
|
+ result={'operation_id':binding.operation_id,'request_digest':binding.request_digest,
|
|
+ 'decision_id':handoff['decision_id'],'native_apply':'unverified',
|
|
+ 'native_calls_completed':0,'cleanup_confirmed':False}
|
|
+ try:
|
|
+ journal.transition(binding,'consume-confirmed','authenticating')
|
|
+ # Fixture identity is independently seeded synthetic setup, not CCR38
|
|
+ # provenance proof or a production service login.
|
|
+ journal.transition(binding,'authenticating','grant-issuing')
|
|
+ native.materialize_grant()
|
|
+ journal.transition(binding,'grant-issued','applying')
|
|
+ for ordinal in range(6):
|
|
+ _fresh(secrets_cfg,handoff,descriptor)
|
|
+ native.metadata_call(ordinal)
|
|
+ result['native_calls_completed']+=1
|
|
+ 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.
|
|
+ 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 29d168d..c9ee244 100644
|
|
--- a/scripts/task_execution_native_fixture.py
|
|
+++ b/scripts/task_execution_native_fixture.py
|
|
@@ -122,8 +122,8 @@ class NativeFixtureAdapter:
|
|
self.used+=1
|
|
evidence_payload = encoded({'token':'owner-retained-task-token'}) if path=='auth/token/lookup' else payload
|
|
digest=content_hash(encoded({'method':method,'path':path,'payload_sha256':'sha256:'+content_hash(evidence_payload),'credential_slot':'task' if task else 'owner'}))
|
|
- ordinal=self._ordinal
|
|
- self.journal.before_call(self.binding,ordinal,digest);self._ordinal+=1
|
|
+ ordinal=self.journal.next_call_ordinal(self.binding)
|
|
+ self.journal.before_call(self.binding,ordinal,digest);self._ordinal=ordinal+1
|
|
try: status,data=self._http(method,path,token,payload)
|
|
except Exception:
|
|
state=self.journal.observe(self.binding)['state']
|
|
diff --git a/tests/task_execution_source_servers.py b/tests/task_execution_source_servers.py
|
|
new file mode 100644
|
|
index 0000000..3570382
|
|
--- /dev/null
|
|
+++ b/tests/task_execution_source_servers.py
|
|
@@ -0,0 +1,206 @@
|
|
+"""Real Approval/Access servers; every admission/key/identity is synthetic local.
|
|
+
|
|
+The positive fixture policy is separately named and exact-descriptor scoped. It
|
|
+never edits either existing unadmitted production task policy or their false gate.
|
|
+"""
|
|
+import base64
|
|
+from contextlib import contextmanager
|
|
+from dataclasses import replace
|
|
+from datetime import datetime,timedelta,timezone
|
|
+import hashlib
|
|
+from http.client import HTTPConnection
|
|
+from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
|
|
+import json
|
|
+import os
|
|
+from pathlib import Path
|
|
+import shutil
|
|
+import socket
|
|
+import subprocess
|
|
+import threading
|
|
+import time
|
|
+from types import SimpleNamespace
|
|
+from wsgiref.simple_server import make_server,WSGIRequestHandler
|
|
+
|
|
+from cryptography import x509
|
|
+from cryptography.hazmat.primitives import serialization,hashes
|
|
+from cryptography.hazmat.primitives.asymmetric import ed25519,rsa
|
|
+from cryptography.x509.oid import NameOID
|
|
+
|
|
+ACCESS=Path(os.environ.get('ACCESS_ENGINE_SOURCE_ROOT','/home/worsch/access-engine'))
|
|
+APPROVAL=Path(os.environ.get('APPROVAL_ENGINE_SOURCE_ROOT','/home/worsch/approval-engine'))
|
|
+SECRETS=Path(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT','/home/worsch/secrets-engine'))
|
|
+FIXTURE_PACKAGE='platform.disposable-task-seam.fixture'
|
|
+FIXTURE_VERSION='v1'
|
|
+PRINCIPAL='system:serviceaccount:secrets-engine:secrets-engine'
|
|
+
|
|
+
|
|
+def private(path,text):path.write_text(text);path.chmod(0o600);return path
|
|
+
|
|
+def free_port():
|
|
+ with socket.socket() as sock:sock.bind(('127.0.0.1',0));return sock.getsockname()[1]
|
|
+
|
|
+
|
|
+def http(origin,method,path,body=None,token=None):
|
|
+ from urllib.parse import urlsplit
|
|
+ parsed=urlsplit(origin);conn=HTTPConnection(parsed.hostname,parsed.port,timeout=4)
|
|
+ try:
|
|
+ headers={'Content-Type':'application/json'}
|
|
+ if token is not None:headers['Authorization']='Bearer '+token
|
|
+ conn.request(method,path,body=json.dumps(body) if body is not None else None,headers=headers)
|
|
+ response=conn.getresponse();raw=response.read(1024*1024)
|
|
+ return response.status,json.loads(raw) if raw else {}
|
|
+ finally:conn.close()
|
|
+
|
|
+
|
|
+class QuietWSGI(WSGIRequestHandler):
|
|
+ def log_message(self,*args):pass
|
|
+
|
|
+
|
|
+@contextmanager
|
|
+def approval_server(directory):
|
|
+ from approval_engine.api import App
|
|
+ from approval_engine.auth import Identity,StaticTokenAuthenticator
|
|
+ from approval_engine.store import Engine
|
|
+ service='synthetic-source-service';human='synthetic-source-human';no_read='synthetic-source-no-read'
|
|
+ def identity(subject,kind,scopes):
|
|
+ return Identity(subject,'https://synthetic.invalid',('approval-engine',),kind,'tenant:platform',
|
|
+ frozenset(),frozenset(scopes),{'level':'aal2','methods':['synthetic']},
|
|
+ 'fixture:synthetic-identity',tenant_source='directory' if kind=='human' else 'registration')
|
|
+ identities={service:identity('secrets-engine','service',['approval:create','approval:read','approval:consume']),
|
|
+ human:identity('synthetic-founder','human',['approval:approve','approval:read']),
|
|
+ no_read:identity('platform-fixture-confirmation','service',['approval:consume'])}
|
|
+ engine=Engine(str(directory/'approval.sqlite'));app=App(engine,StaticTokenAuthenticator(identities))
|
|
+ calls=[]
|
|
+ def logged(environ,start_response):
|
|
+ calls.append((environ['REQUEST_METHOD'],environ['PATH_INFO']))
|
|
+ return app(environ,start_response)
|
|
+ server=make_server('127.0.0.1',0,logged,handler_class=QuietWSGI)
|
|
+ thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
|
+ try:
|
|
+ yield SimpleNamespace(origin='http://127.0.0.1:'+str(server.server_port),engine=engine,
|
|
+ service_token=service,human_token=human,no_read_token=no_read,calls=calls)
|
|
+ finally:server.shutdown();server.server_close();thread.join(5);engine.close()
|
|
+
|
|
+
|
|
+@contextmanager
|
|
+def reviewer_server(directory):
|
|
+ # Actual Access TokenReview client consumes this independently admitted
|
|
+ # synthetic reviewer. This is not a Kubernetes authentication proof.
|
|
+ exp=int(time.time())+900
|
|
+ payload=base64.urlsafe_b64encode(json.dumps({'exp':exp}).encode()).decode().rstrip('=')
|
|
+ token='synthetic.'+payload+'.synthetic';reviewer='synthetic-reviewer';calls=[]
|
|
+ class Handler(BaseHTTPRequestHandler):
|
|
+ def log_message(self,*args):pass
|
|
+ def do_POST(self):
|
|
+ size=int(self.headers.get('Content-Length','0'));data=json.loads(self.rfile.read(size))
|
|
+ valid=(self.path=='/apis/authentication.k8s.io/v1/tokenreviews'
|
|
+ and self.headers.get('Authorization')=='Bearer '+reviewer
|
|
+ and data.get('spec',{}).get('token')==token
|
|
+ and 'flex-auth' in data.get('spec',{}).get('audiences',[]))
|
|
+ calls.append(valid)
|
|
+ result={'apiVersion':'authentication.k8s.io/v1','kind':'TokenReview',
|
|
+ 'status':{'authenticated':valid,'audiences':['flex-auth'],
|
|
+ 'user':{'username':PRINCIPAL}}}
|
|
+ raw=json.dumps(result).encode();self.send_response(201)
|
|
+ self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(raw)));self.end_headers();self.wfile.write(raw)
|
|
+ server=ThreadingHTTPServer(('127.0.0.1',0),Handler);thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
|
+ # The actual reviewer constructor requires a valid CA even for its explicitly
|
|
+ # synthetic HTTP endpoint; this CA never trusts an estate endpoint.
|
|
+ key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
|
|
+ name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic-local-reviewer')])
|
|
+ now=datetime.now(timezone.utc)
|
|
+ cert=x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(days=1)).sign(key,hashes.SHA256())
|
|
+ ca=directory/'fixture-ca.pem';ca.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
|
+ reviewer_file=private(directory/'reviewer.token',reviewer)
|
|
+ try:yield SimpleNamespace(origin='http://127.0.0.1:'+str(server.server_port),caller_token=token,reviewer_file=reviewer_file,ca=ca,calls=calls)
|
|
+ finally:server.shutdown();server.server_close();thread.join(5)
|
|
+
|
|
+
|
|
+def fixture_policy(directory,descriptor,request):
|
|
+ policy=directory/'fixture-policy.md'
|
|
+ fixture=directory/'fixture-cases.json'
|
|
+ fixture.write_text(json.dumps([{'id':'synthetic-claim-free-deny','request':request,
|
|
+ 'expect':{'effect':'deny','reason':'fixture_unapproved'}}]))
|
|
+ document='''---
|
|
+id: platform.disposable-task-seam.fixture
|
|
+name: Disposable exact task seam fixture
|
|
+namespace: secrets-engine:secret-catalog-lane
|
|
+version: v1
|
|
+status: proposed
|
|
+package: fixture.platform.task_seam
|
|
+allow_ttl: 5m
|
|
+actions: [execute-ccr]
|
|
+owner: team:synthetic-fixture
|
|
+fixtures: [fixture-cases.json]
|
|
+caring: {profile: caring-0.4.0-rc2, enforce: false}
|
|
+activation: {mode: local}
|
|
+---
|
|
+Synthetic local policy only. No production task policy is edited or enabled.
|
|
+
|
|
+```rego
|
|
+default decision := {"effect":"deny","reason":"fixture_unapproved"}
|
|
+decision := {"effect":"allow","reason":"synthetic_exact_task_approved"} {
|
|
+ input.action == "execute-ccr"
|
|
+ input.tenant == "tenant:platform"
|
|
+ input.subject.id == "secrets-engine"
|
|
+ input.subject.type == "service"
|
|
+ input.resource.system == "secrets-engine"
|
|
+ input.context.human_control == true
|
|
+ input.context.approval.valid_now == true
|
|
+ input.context.approval.consumed == false
|
|
+ input.context.approval.binding.human_control == true
|
|
+ input.context.execution_contract == DESCRIPTOR
|
|
+}
|
|
+```
|
|
+''' + '''
|
|
+```rego test
|
|
+import data.fixture.platform.task_seam
|
|
+test_missing_approval_denies { task_seam.decision.effect == "deny" with input as {} }
|
|
+```
|
|
+'''
|
|
+ document=document.replace('DESCRIPTOR',json.dumps(descriptor,separators=(',',':')))
|
|
+ policy.write_text(document)
|
|
+ return policy
|
|
+
|
|
+
|
|
+@contextmanager
|
|
+def access_server(directory,descriptor,request,reviewer,*,actual_policy=False):
|
|
+ binary=Path(os.environ.get('PLATFORM_ACCESS_FIXTURE_BINARY','/tmp/netkingdom-platform-wave4-access'))
|
|
+ verifier=Path(os.environ.get('PLATFORM_PDP_FIXTURE_VERIFIER','/tmp/netkingdom-platform-wave4-verify'))
|
|
+ if not binary.is_file() or not verifier.is_file():raise RuntimeError('build source fixture binaries first')
|
|
+ registry=directory/'registry.json';registry.write_bytes((ACCESS/'examples/secrets-engine/registry_snapshot.json').read_bytes())
|
|
+ policy=ACCESS/'examples/secrets-engine-service-task-execution/policy_package.md' if actual_policy else fixture_policy(directory,descriptor,request)
|
|
+ requestfile=directory/'claim-free-request.json';requestfile.write_text(json.dumps(request))
|
|
+ # Trusted owner-local source CLI derives the real package pin independently
|
|
+ # from the network responder. Its DENY is used only as digest provenance.
|
|
+ offline=subprocess.run([str(binary),'check','--registry',str(registry),'--policy',str(policy),'--request',str(requestfile)],capture_output=True,text=True,timeout=15)
|
|
+ if offline.returncode!=0:raise RuntimeError('source policy fixture invalid: '+offline.stderr[:300])
|
|
+ produced=json.loads(offline.stdout);provenance=produced['provenance']
|
|
+ key=ed25519.Ed25519PrivateKey.generate()
|
|
+ seed=key.private_bytes(serialization.Encoding.Raw,serialization.PrivateFormat.Raw,serialization.NoEncryption())
|
|
+ public=key.public_key().public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw)
|
|
+ b64=lambda value:base64.urlsafe_b64encode(value).decode().rstrip('=')
|
|
+ seedfile=private(directory/'signing.seed',b64(seed))
|
|
+ trust={name:provenance[name] for name in ['evaluator','mode','policy_package','policy_version','policy_package_digest']}
|
|
+ trust.update(schema='secrets-engine.pdp-trust.v1',keys={'synthetic-owner-key':b64(public)})
|
|
+ trustfile=directory/'pdp-trust.json';trustfile.write_text(json.dumps(trust))
|
|
+ port=free_port();origin='http://127.0.0.1:'+str(port)
|
|
+ args=[str(binary),'serve','--addr','127.0.0.1:'+str(port),'--registry',str(registry),'--policy',str(policy),
|
|
+ '--envelope-signing-key-file',str(seedfile),'--envelope-signing-kid','synthetic-owner-key',
|
|
+ '--caller-auth-mode','enforce','--caller-audience','flex-auth','--caller-kubernetes-url',reviewer.origin,
|
|
+ '--caller-reviewer-token-file',str(reviewer.reviewer_file),'--caller-ca-file',str(reviewer.ca),
|
|
+ '--caller-binding','secrets-engine='+PRINCIPAL]
|
|
+ process=subprocess.Popen(args,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
+ for _ in range(100):
|
|
+ if process.poll() is not None:raise RuntimeError('source Access server exited')
|
|
+ try:
|
|
+ with socket.create_connection(('127.0.0.1',port),timeout=.1):break
|
|
+ except OSError:time.sleep(.05)
|
|
+ else:process.kill();process.wait();raise RuntimeError('source Access server failed to start')
|
|
+ try:yield SimpleNamespace(origin=origin,trustfile=trustfile,verifier=verifier,
|
|
+ package=provenance['policy_package'],version=provenance['policy_version'],
|
|
+ process=process,offline_envelope=produced)
|
|
+ finally:
|
|
+ process.terminate()
|
|
+ try:process.wait(timeout=5)
|
|
+ except subprocess.TimeoutExpired:process.kill();process.wait()
|
|
diff --git a/tests/test_task_execution_mediation_journal.py b/tests/test_task_execution_mediation_journal.py
|
|
new file mode 100644
|
|
index 0000000..d0f8e19
|
|
--- /dev/null
|
|
+++ b/tests/test_task_execution_mediation_journal.py
|
|
@@ -0,0 +1,32 @@
|
|
+"""Durable CAS/confirmation call prefix cannot overlap native call ordinals."""
|
|
+import os
|
|
+from pathlib import Path
|
|
+import sys
|
|
+import uuid
|
|
+import pytest
|
|
+sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
|
|
+from task_execution_journal import Binding,Cleanup,Journal,Refused
|
|
+
|
|
+
|
|
+def test_next_call_continues_auth_prefix_without_replay_or_resume(tmp_path):
|
|
+ os.chmod(tmp_path,0o700);journal=Journal(tmp_path)
|
|
+ binding=Binding('fixture-approval','sha256:'+'a'*64,str(uuid.uuid4()),'b'*64)
|
|
+ try:
|
|
+ with pytest.raises(Refused):journal.next_call_ordinal(binding)
|
|
+ with journal.locked():
|
|
+ journal.reserve(binding,(Cleanup('task-token-revoke','c'*64),))
|
|
+ assert journal.next_call_ordinal(binding)==0
|
|
+ for ordinal in [0,1]:
|
|
+ journal.before_call(binding,ordinal,'d'*64)
|
|
+ with pytest.raises(Refused):journal.next_call_ordinal(binding)
|
|
+ journal.after_call(binding,ordinal,'d'*64,'e'*64)
|
|
+ assert journal.next_call_ordinal(binding)==2
|
|
+ journal.transition(binding,'reserved','consume-confirmed')
|
|
+ journal.before_call(binding,2,'f'*64)
|
|
+ with pytest.raises(Refused):journal.before_call(binding,0,'f'*64)
|
|
+ journal.after_call(binding,2,'f'*64,'e'*64)
|
|
+ journal.before_cleanup(binding,0,Cleanup('task-token-revoke','c'*64))
|
|
+ with pytest.raises(Refused):journal.next_call_ordinal(binding)
|
|
+ with journal.locked():
|
|
+ with pytest.raises(Refused):journal.next_call_ordinal(binding)
|
|
+ finally:journal.close()
|
|
diff --git a/tests/test_task_execution_mediator.py b/tests/test_task_execution_mediator.py
|
|
new file mode 100644
|
|
index 0000000..8ad9045
|
|
--- /dev/null
|
|
+++ b/tests/test_task_execution_mediator.py
|
|
@@ -0,0 +1,160 @@
|
|
+"""Whole source-server/native fixture proof, never production owner acceptance."""
|
|
+from contextlib import contextmanager
|
|
+from dataclasses import replace
|
|
+import importlib
|
|
+import json
|
|
+from pathlib import Path
|
|
+import sys
|
|
+from types import SimpleNamespace
|
|
+import uuid
|
|
+
|
|
+import pytest
|
|
+sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
|
|
+from test_task_execution_native_fixture import owner,bao_dev,compiler
|
|
+from task_execution_mediator import ConfirmationConfig,run_native_fixture_task
|
|
+from task_execution_journal import Refused
|
|
+from task_execution_source_servers import approval_server,reviewer_server,access_server,http,private
|
|
+from secrets_engine.approval_consume import _request_purpose
|
|
+from secrets_engine.approval_claim import binding_from_check_request
|
|
+from secrets_engine.authorization import build_action_request
|
|
+from secrets_engine.decision_check import check_decision
|
|
+from secrets_engine.execution_contract import ExecutionContract
|
|
+
|
|
+
|
|
+@contextmanager
|
|
+def whole_fixture(owner,tmp_path,*,actual_policy=False,confirmation_read=True):
|
|
+ native,journal,binding,call,compiler,descriptor=owner
|
|
+ snapshot={path:compiler.read(Path(path)) for path in compiler.pins}
|
|
+ entry=compiler._catalogue_entry(snapshot)
|
|
+ approval_id=str(uuid.uuid4())
|
|
+ entry=replace(entry,approval={**entry.approval,'authorization_id':approval_id})
|
|
+ request=build_action_request(entry,'execute-ccr',subject_id='secrets-engine',subject_type='service',
|
|
+ purpose=_request_purpose(entry),execution_contract=ExecutionContract.parse(descriptor))
|
|
+ with reviewer_server(tmp_path) as reviewer, access_server(tmp_path,descriptor,request,reviewer,actual_policy=actual_policy) as access, approval_server(tmp_path) as approval:
|
|
+ issue=access.offline_envelope
|
|
+ body={'id':approval_id,'binding':binding_from_check_request(issue['binding']),
|
|
+ 'pdp_digest':issue['binding']['request_digest'],'pdp_path':True,'human_control':True,
|
|
+ 'validity':{'not_before':'2026-01-01T00:00:00+00:00',
|
|
+ 'expires_at':reviewer_expiry()}}
|
|
+ status,obj=http(approval.origin,'POST','/v1/approvals',body,approval.service_token)
|
|
+ assert status==201
|
|
+ assert http(approval.origin,'POST','/v1/approvals/'+approval_id+'/entries',{},approval.human_token)[0]==200
|
|
+ approval_token=private(tmp_path/'approval.token',approval.service_token)
|
|
+ pdp_token=private(tmp_path/'pdp.token',reviewer.caller_token)
|
|
+ confirmation_token=private(tmp_path/'confirmation.token',approval.service_token if confirmation_read else approval.no_read_token)
|
|
+ cfg=SimpleNamespace(approval_url=approval.origin,approval_token_file=approval_token,
|
|
+ approval_client_secret_file=None,pdp_url=access.origin,pdp_token_file=pdp_token,
|
|
+ pdp_trust_file=access.trustfile,pdp_verifier=access.verifier,
|
|
+ authorization_subject_id='secrets-engine',authorization_subject_type='service',
|
|
+ authorization_policy_package=access.package,authorization_policy_version=access.version,
|
|
+ clock_trust_file=None)
|
|
+ yield SimpleNamespace(native=native,journal=journal,call=call,compiler=compiler,
|
|
+ descriptor=descriptor,entry=entry,cfg=cfg,approval=approval,
|
|
+ confirmation=ConfirmationConfig(approval.origin,confirmation_token),
|
|
+ approval_id=approval_id,request=request,reviewer=reviewer)
|
|
+
|
|
+
|
|
+def reviewer_expiry():
|
|
+ from datetime import datetime,timedelta,timezone
|
|
+ return (datetime.now(timezone.utc)+timedelta(minutes=10)).isoformat()
|
|
+
|
|
+
|
|
+def invoke(fixture):
|
|
+ return run_native_fixture_task(compiler=fixture.compiler,journal=fixture.journal,
|
|
+ plan_request=fixture.native.meta['request'],owner_descriptor=fixture.native.admission.descriptor,
|
|
+ secrets_cfg=fixture.cfg,entry=fixture.entry,confirmation=fixture.confirmation,
|
|
+ native_admission=fixture.native.admission)
|
|
+
|
|
+
|
|
+def test_real_source_servers_signed_fixture_allow_and_actual_task_policy_deny(owner,tmp_path):
|
|
+ # These two Checks test real source servers and independently installed keys;
|
|
+ # they are not a fabricated DecisionEnvelope or activation of a real policy.
|
|
+ for actual_policy in [False,True]:
|
|
+ directory=tmp_path/('actual-policy' if actual_policy else 'fixture-policy');directory.mkdir()
|
|
+ with whole_fixture(owner,directory,actual_policy=actual_policy) as fixture:
|
|
+ status,claim=http(fixture.approval.origin,'GET','/v1/approvals/'+fixture.approval_id+'/claim',token=fixture.approval.service_token)
|
|
+ assert status==200
|
|
+ request=json.loads(json.dumps(fixture.request));request['context']['approval']=claim
|
|
+ envelope=check_decision(base_url=fixture.cfg.pdp_url,token_file=fixture.cfg.pdp_token_file,request=request,
|
|
+ trust_file=fixture.cfg.pdp_trust_file,verifier=fixture.cfg.pdp_verifier)
|
|
+ assert envelope['effect']==('deny' if actual_policy else 'allow')
|
|
+ assert envelope['provenance']['caller']['mode']=='enforce'
|
|
+ assert all(fixture.reviewer.calls)
|
|
+ assert fixture.approval.engine.get(fixture.approval_id).status=='approved'
|
|
+ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)[0]==404
|
|
+
|
|
+
|
|
+def test_missing_strict_dependency_never_reads_credentials_or_reserves(monkeypatch):
|
|
+ original=importlib.import_module
|
|
+ def missing(name,*args,**kwargs):
|
|
+ if name=='secrets_engine.task_execution_handoff':raise ImportError()
|
|
+ return original(name,*args,**kwargs)
|
|
+ monkeypatch.setattr(importlib,'import_module',missing)
|
|
+ with pytest.raises(Refused,match='dependency unavailable'):
|
|
+ run_native_fixture_task(compiler=None,journal=None,plan_request={},owner_descriptor=b'',
|
|
+ secrets_cfg=None,entry=None,confirmation=None,native_admission=None)
|
|
+
|
|
+
|
|
+def test_whole_strict_consume_confirmation_precedes_real_native_effect(owner,tmp_path):
|
|
+ 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
|
|
+ obj=fixture.approval.engine.get(fixture.approval_id)
|
|
+ assert obj.status=='consumed'
|
|
+ assert obj.consumed_digest==result['request_digest']
|
|
+ calls=fixture.journal.db.execute('SELECT ordinal,outcome FROM calls ORDER BY ordinal').fetchall()
|
|
+ assert calls[:2]==[(0,'complete'),(1,'complete')]
|
|
+ assert len(calls)==13 # CAS + GET + five native grant calls + six metadata calls
|
|
+ assert fixture.approval.calls.count(('POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1
|
|
+ assert fixture.approval.calls.count(('GET','/v1/approvals/'+fixture.approval_id))==1
|
|
+ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)[0]==404
|
|
+ raw=(fixture.journal.directory/'journal.sqlite').read_bytes()
|
|
+ for token in [fixture.native.admission.owner_token,fixture.native.admission.identity_session.token,
|
|
+ fixture.approval.service_token,fixture.reviewer.caller_token]:
|
|
+ assert token.encode() not in raw
|
|
+ with pytest.raises(Exception):invoke(fixture)
|
|
+ assert fixture.approval.calls.count(('POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1
|
|
+
|
|
+
|
|
+def test_actual_confirmation_read_scope_denial_keeps_native_untouched(owner,tmp_path):
|
|
+ with whole_fixture(owner,tmp_path,confirmation_read=False) as fixture:
|
|
+ with pytest.raises(Refused,match='confirmation unconfirmed'):invoke(fixture)
|
|
+ assert fixture.approval.engine.get(fixture.approval_id).status=='consumed'
|
|
+ state=fixture.journal.db.execute('SELECT state FROM operations').fetchone()[0]
|
|
+ assert state=='unknown'
|
|
+ assert fixture.journal.db.execute("SELECT COUNT(*) FROM calls WHERE outcome='pending'").fetchone()[0]==1
|
|
+ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)[0]==404
|
|
+ policy_path=fixture.compiler.before_call(fixture.native.plan,1).path
|
|
+ assert fixture.call('GET',policy_path)[0]==404
|
|
+ with pytest.raises(Exception):invoke(fixture)
|
|
+ assert fixture.approval.calls.count(('POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1
|
|
+
|
|
+
|
|
+def test_actual_unadmitted_policy_refuses_before_cas_and_native(owner,tmp_path):
|
|
+ with whole_fixture(owner,tmp_path,actual_policy=True) as fixture:
|
|
+ with pytest.raises(Exception):invoke(fixture)
|
|
+ assert fixture.approval.engine.get(fixture.approval_id).status=='approved'
|
|
+ assert not any(method=='POST' and path.endswith('/consume') for method,path in fixture.approval.calls)
|
|
+ assert fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]==0
|
|
+ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)[0]==404
|
|
+
|
|
+
|
|
+def test_entry_purpose_substitution_refuses_before_authorizer_or_cas(owner,tmp_path):
|
|
+ with whole_fixture(owner,tmp_path) as fixture:
|
|
+ fixture.entry=replace(fixture.entry,approval={**fixture.entry.approval,'purpose':'caller-substituted-purpose'})
|
|
+ before=list(fixture.approval.calls)
|
|
+ with pytest.raises(Refused,match='approval metadata differs'):invoke(fixture)
|
|
+ assert fixture.approval.calls==before
|
|
+ assert fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]==0
|
|
+ assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)[0]==404
|
|
+
|
|
+
|
|
+def test_catalogue_bool_integer_alias_refuses_before_authorizer(owner,tmp_path):
|
|
+ with whole_fixture(owner,tmp_path) as fixture:
|
|
+ assert fixture.entry.delivery_auth['secret_id_num_uses']==1
|
|
+ fixture.entry=replace(fixture.entry,delivery_auth={**fixture.entry.delivery_auth,'secret_id_num_uses':True})
|
|
+ before=list(fixture.approval.calls)
|
|
+ with pytest.raises(Refused,match='catalogue does not match'):invoke(fixture)
|
|
+ assert fixture.approval.calls==before
|
|
+ assert fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]==0
|
|
diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md
|
|
index f9af000..4ea8015 100644
|
|
--- a/workplans/RPF-WP-0035-credential-lane-implementation.md
|
|
+++ b/workplans/RPF-WP-0035-credential-lane-implementation.md
|
|
@@ -1262,3 +1262,18 @@ 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.
|
|
+
|
|
+2026-10-16 T12 fourth source package: connected the Secrets-owned strict task
|
|
+handoff to actual Approval ExecutionConfirmationClient independent GET before
|
|
+consume-confirmed/native fixture work. Platform accepts no supplied authorization
|
|
+boolean/object/callback and preserves its exact owner descriptor/grant/Plan join.
|
|
+Durable CAS/GET intents precede native ordinals through the repaired journal
|
|
+prefix API. Actual source-server fixture uses signed Access/enforced synthetic
|
|
+TokenReview, real Approval WSGI/SQLite with distinct synthetic human/service
|
|
+identities, actual strict Secrets signature/CAS validation and disposable native
|
|
+OpenBao. Untouched actual task policy remains signed DENY; separately named
|
|
+synthetic fixture policy is never production admission. Read-scope denial after
|
|
+CAS records consumed/unknown with zero native effects. Exact dependency/source
|
|
+hashes, tests and remaining gates are retained with the source package and in
|
|
+docs/task-execution-mediator-seam.md. No real grant/custody, policy default, owner
|
|
+sync or native production acceptance is performed; T12 stays progress.
|