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