diff --git a/README.md b/README.md
index d4e7897..b1f0440 100644
--- a/README.md
+++ b/README.md
@@ -347,6 +347,17 @@ baton-mcp # start MCP server
Exposes resources (`baton://status`, `baton://topology`, `baton://node/{name}`), tools (`circuit_status`, `list_nodes`, `show_routes`, `show_metrics`, `show_signals`), and prompts (`circuit_overview`).
+## Delegated Provider Library
+
+Python integrations can opt into the delegated connector and credential custody
+APIs, including a single-node SQLite reference for durable dispatch, bounded
+provider attempts, recovery, audit events, and failure notifications. Callers
+must supply their own trusted verifier and custody-internal provider operation
+factory. No default CLI path enables provider delivery.
+
+See [the custody boundary](docs/credential-custody-boundary.md) for the exact
+contract, reference limitations, and production integration prerequisites.
+
## Service Manifests
Each service can self-describe with a `baton-service.yaml`:
diff --git a/docs/credential-custody-boundary.md b/docs/credential-custody-boundary.md
new file mode 100644
index 0000000..c9b4107
--- /dev/null
+++ b/docs/credential-custody-boundary.md
@@ -0,0 +1,235 @@
+# Provider Credential Custody Boundary
+
+## Purpose
+
+Baton owns the cloud-neutral egress control boundary for provider-backed
+operations. A business workflow may select a connector and provide opaque
+recipient and payload references, but it must never receive or invoke with an email,
+telephony, or other external-provider credential value.
+
+`src/baton/credential_custody.py` defines the reusable boundary:
+
+- `ProviderCredentialHandle` is an administrator-configured opaque handle,
+ not a secret-store URI or credential value.
+- `SignedWorkloadAuthorizationVerifier` supports standalone custody consumers.
+ The composed delegated runtime does not invoke it after dispatch admission.
+- `CredentialCustodyAuthorizer` either obtains a standalone trusted verifier
+ outcome or accepts the composed runtime's already verified shared outcome.
+ `authorize_provider_dispatch` binds it to the exact workload, initial
+ connector handle, channel, purpose, opaque recipient and payload references,
+ request fingerprint, and provider-attempt budget before consuming the
+ reservation.
+- `AuthorizationConsumptionLedger` is required for every provider operation and
+ is invoked inside authorization to atomically bind authorization, request
+ fingerprint, and idempotency key.
+- `CustodiedReferenceResolver` is the only permitted provider-operation
+ boundary. It resolves material internally and returns a sanitized outcome.
+- `CredentialCustodyInvokerFactory` is the adapter required by
+ `DelegatedConnectorExecutor`: after dispatch idempotency begins and only
+ immediately before an actual provider attempt, it validates route-to-handle
+ bindings and the complete configured failover scope, consumes one
+ authorization reservation, and returns an invoker that can call only
+ `CustodiedReferenceResolver`.
+- The resulting `AuthorizedProviderDispatch` carries one ledger reservation
+ across primary and backup attempts, but can select only connector handles
+ already included in its verified scope and is bounded by its verified
+ provider-attempt budget.
+- Every custody request and provider-attempt reservation is bound to the exact
+ active dispatch claim. An expired or reclaimed claim cannot reserve authority,
+ invoke a provider, renew itself, or complete the dispatch.
+
+## Runtime Composition
+
+`src/baton/delegated_runtime.py` supplies the reusable composition boundary:
+
+- `DelegatedRuntimeComponents` requires an explicit durable dispatch journal,
+ authorization ledger, dispatch signal sink, custody audit sink, failure
+ notifier, and the journal's real claim-lease duration.
+- `DelegatedConnectorRuntime.compose` validates route-to-handle bindings and
+ requires the claim lease to exceed every bounded provider-attempt timeout
+ before constructing an executor.
+- `SinglePurposeProviderOperationFactory` is the only operation factory used by
+ the concrete resolver. Provider material may exist inside its prepared
+ operation, but no Baton request, authorization, outcome, event, or runtime
+ state type contains that material.
+- `ConfiguredVerifierBundle` supplies one verifier with the exact audience,
+ workload, purpose, per-channel available-connector ceiling, issuer-policy
+ reference, and rotation-policy reference before dispatch admission. The
+ verifier must return one
+ `VerifiedDelegatedAuthorization`; Baton derives both dispatch and custody
+ views from that same immutable outcome.
+- `SignetDelegatedAuthorizationAdapter` sends the exact runtime context to a
+ trusted Signet delegated-provider verifier client, requires its verified
+ policy metadata to match, and maps the wire-shaped outcome into Baton's
+ shared authorization. It is key-free mapping evidence, not a cryptographic
+ verifier or trusted transport implementation.
+ The runtime derives the per-channel available-connector ceiling from its
+ enabled routes; a Signet outcome may narrow that set but cannot authorize a
+ connector outside it. Issuer and trust-policy configuration must come from
+ an audited high-administration control plane; the adapter constructor is not
+ that control plane.
+- `SqliteDelegatedRuntimeState` and
+ `DelegatedConnectorRuntime.build_sqlite_reference` are single-node reference
+ implementations for executable recovery evidence. They are not a
+ multi-replica production state backend.
+
+## Exact Delegated Authorization Contract
+
+The trusted Signet-compatible verifier receives the opaque authorization
+reference, the exact `DispatchRequest`, and a `DelegatedAuthorizationContext`.
+The context's per-channel available connector IDs are the runtime ceiling: the
+verified authorization may narrow to a non-empty subset but cannot authorize
+an unavailable connector.
+Its verified outcome must bind:
+
+- authorization ID and issuer;
+- Baton delegated-executor audience;
+- the exact issuer-policy and rotation-policy references applied by the
+ verifier;
+- workload ID/principal;
+- exactly one channel;
+- allowed connector IDs and exactly one purpose matching the runtime;
+- `not_before` and `not_after`;
+- `max_uses=1` and a provider-attempt budget; and
+- the exact canonical request fingerprint.
+
+The configured Baton verifier bundle must also supply a positive provider-attempt
+ceiling no greater than `3` and an authorization-lifetime ceiling no greater
+than `15` minutes. Baton sends those ceilings to the Signet verifier, rejects a
+Signet result that exceeds either one, and rechecks the bounded verified result
+before dispatch admission.
+
+The request fingerprint is a domain-separated SHA-256 digest over
+`dispatch_id`, workflow/operation ID, channel, opaque recipient reference,
+opaque payload reference, and idempotency key. `DispatchRequest` and
+`CredentialUseRequest` both reject a fingerprint that does not match those
+fields. Recipient and payload references therefore must identify immutable or
+versioned data; the digest does not make a mutable reference immutable.
+
+`dispatch_claim_id` is acquired only after verification and is intentionally
+outside the signed fingerprint. The durable journal and authorization ledger
+bind it to the active exact dispatch before reservation or provider use.
+
+The verified outcome contains no provider credential, credential handle,
+recipient value, payload value, or provider response. `authorization_id` is the
+sanitized correlation reference carried through custody audit events.
+Issuer-policy and rotation-policy references are verified trust metadata, not
+signed provider-operation claims; they must exactly match the configured
+verifier bundle.
+
+## Failure Semantics
+
+- Missing, expired, mismatched, or out-of-scope authority is denied before any
+ provider operation.
+- Only single-dispatch authority with an exact durable ledger reservation is
+ accepted. This prevents a provider send from accepting reusable authority or
+ a `one_time` claim without replay enforcement.
+- Sanitized outcomes carry provider identity, status, correlation identifiers,
+ audit reference, and bounded failure code only. They do not carry material,
+ provider response bodies, recipient data, or message data.
+- Delegated terminal outcomes carry connector identity as well as provider
+ identity, so audits and failure notifications can identify the configured
+ connector without revealing custody material.
+- A provider attempt is atomically reserved in the durable authorization ledger
+ before invocation. Its verified attempt budget remains consumed across
+ process crashes, dispatch aborts, and stale-claim recovery.
+- A successful provider operation followed by custody outcome or audit
+ persistence failure returns non-retryable `custody_state_unavailable`; the
+ executor does not automatically send again.
+- A provider operation followed by dispatch-journal completion failure raises
+ `DispatchStateUnavailable` and leaves the claim for explicit recovery. It
+ does not erase or abort the post-provider claim.
+- After a dispatch claim is acquired, exceptional execution paths do not
+ automatically abort it. Because the executor may not know whether a provider
+ operation occurred, the lease remains in place until controlled recovery.
+
+## Recovery Windows
+
+The runtime deliberately fails closed around these crash windows:
+
+1. Before provider-attempt reservation, an expired claim may be reclaimed. The
+ old claim can no longer reserve or invoke.
+2. After provider-attempt reservation but before provider invocation, the
+ durable budget remains consumed. Recovery does not blindly repeat that
+ attempt.
+3. After provider invocation but before custody outcome or dispatch-journal
+ completion, delivery is uncertain. The custody-internal provider operation
+ must enforce the supplied idempotency key; without that provider-side
+ guarantee, at-least-once recovery can duplicate delivery.
+4. After dispatch-journal completion but before terminal signal persistence, a
+ replay returns the completed sanitized outcome and retries the idempotent
+ signal without invoking the provider again.
+
+The SQLite reference hash chain detects mutation when verified against its
+current anchor, but it is tamper-evident rather than tamper-proof. A privileged
+actor could rewrite the database and recompute the chain. Its failure queue is
+durable and acknowledgeable, but does not prove that an external operator was
+paged.
+
+## Standalone Library Boundary
+
+The delegated modules are opt-in Python library APIs. No CLI command, default
+configuration, or adapter invokes them automatically. Consumers supply a
+trusted verifier and a custody-internal operation factory explicitly; the
+SQLite constructor supplies a durable single-node reference implementation.
+The included tests exercise the contracts with credential-free doubles and
+SQLite recovery, without claiming live signature verification or provider use.
+
+Each provider attempt rechecks the verified validity window before and after
+waiting for its durable budget reservation. An expired reservation cannot be
+used to invoke a provider, and a budget consumed while waiting stays consumed.
+Journal renewal and completion evaluate lease time only after acquiring the
+SQLite write lock, so contention cannot revive an expired claim.
+
+## Production Integration Prerequisites
+
+These modules are a connected runtime and executable reference, not a
+configured production custody deployment. The delegated executor no longer
+accepts a direct provider invoker. Production construction must use explicit
+durable components and an audited custody implementation; the protocol seams
+exist for approved cloud-neutral backends.
+
+A production integrator must supply and qualify the following before enabling
+provider delivery. These deployment requirements do not imply that the library
+includes those external services:
+
+1. A concrete trusted Signet delegated-provider verifier client and transport
+ with issuer and rotation enforcement. The key-free Baton adapter does not
+ establish that trust.
+2. A shared, highly available multi-replica implementation of the durable
+ component contracts, including atomic claim, reservation, attempt-budget,
+ completion, replay, and recovery behavior.
+3. A custody-internal resolver implementation, potentially backed by OpenBao
+ after license, deployment, assurance, and operational review.
+4. An externally anchored audit pipeline and an operated failure-notification
+ consumer with tested acknowledgement and escalation.
+5. Provider-operation idempotency and operator reconciliation for uncertain
+ post-invocation outcomes.
+6. A shared circuit-breaker implementation or an explicit decision to accept
+ process-local circuit state. The current executor's circuit state is local
+ to one process.
+7. Key-free executable evidence for each boundary and the provider executor.
+
+No provider credential values or signing keys are used or packaged by this
+contract or its tests.
+
+## OpenBao Candidate Evidence
+
+OpenBao is a candidate implementation backend for the internal resolver, not
+an adopted dependency in this change. Official project documentation checked
+on 2026-06-04 records:
+
+- Source licensing as MPL-2.0 with OSI and FSF recognition:
+
+- Versioned arbitrary secret storage and ACL separation through KV v2:
+
+- Cryptographic processing without retaining submitted data through Transit:
+
+- Request audit-device behavior:
+
+- JWT/OIDC authentication capability:
+
+
+These documents establish candidate fit and commercial open-source eligibility;
+they do not establish certification, deployment security, high availability,
+unseal policy, vulnerability disposition, or approval for MEA production use.
diff --git a/src/baton/credential_custody.py b/src/baton/credential_custody.py
new file mode 100644
index 0000000..2fd21a8
--- /dev/null
+++ b/src/baton/credential_custody.py
@@ -0,0 +1,954 @@
+"""Cloud-neutral provider credential custody contracts.
+
+This module defines the boundary between a business workflow and a
+single-purpose provider executor. No public type carries credential values.
+A concrete resolver may access provider material only while executing an
+already authorized operation inside the custody boundary.
+"""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Callable, Mapping
+from dataclasses import dataclass
+from datetime import UTC, datetime
+from enum import StrEnum
+from typing import Protocol
+
+from .delegated_connector import (
+ AuthorizationDenied as DelegatedAuthorizationDenied,
+)
+from .delegated_connector import (
+ CapabilityReference,
+ Channel,
+ ConnectorRoute,
+ CustodiedProviderInvoker,
+ DeliveryStatus,
+ DispatchClaim,
+ DispatchRequest,
+ ProviderAttemptOutcome,
+ VerifiedDispatchGrant,
+ dispatch_request_fingerprint,
+)
+
+_FINGERPRINT_RE = re.compile(r"^[a-f0-9]{64}$")
+_CODE_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
+
+
+class ProviderChannel(StrEnum):
+ SMS = "sms"
+ EMAIL = "email"
+
+
+class CustodyResultStatus(StrEnum):
+ ACCEPTED = "accepted"
+ DELIVERED = "delivered"
+ FAILED = "failed"
+
+
+class CustodyAuditKind(StrEnum):
+ AUTHORIZATION_DENIED = "authorization_denied"
+ AUTHORIZATION_ACCEPTED = "authorization_accepted"
+ RESERVATION_REQUIRED = "reservation_required"
+ REPLAY_REJECTED = "replay_rejected"
+ INVOCATION_COMPLETED = "invocation_completed"
+ INVOCATION_FAILED = "invocation_failed"
+
+
+class CustodyError(Exception):
+ """Base error for the non-material custody boundary."""
+
+
+class CustodyAuthorizationDenied(CustodyError):
+ """A verified outcome cannot authorize this provider operation."""
+
+
+class ConsumptionReservationRequired(CustodyAuthorizationDenied):
+ """Bounded-use authority lacks an atomic consumption reservation."""
+
+
+class CustodyMonitoringUnavailable(CustodyError):
+ """Durable sanitized custody audit or failure notification is unavailable."""
+
+
+@dataclass(frozen=True)
+class ProviderCredentialHandle:
+ """Opaque lookup handle configured by an administrator.
+
+ ``handle_id`` is not a provider credential, secret-store URI, or ciphertext.
+ The custody implementation owns the mapping to a concrete backend record.
+ """
+
+ handle_id: str
+ connector_id: str
+ provider_key: str
+ channel: ProviderChannel
+ version_ref: str
+
+ def __post_init__(self) -> None:
+ for name in ("handle_id", "connector_id", "provider_key", "version_ref"):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+
+
+@dataclass(frozen=True)
+class WorkloadAuthorizationReference:
+ """Opaque reference to a signed authorization checked by a trusted verifier."""
+
+ reference: str
+
+ def __post_init__(self) -> None:
+ if not self.reference:
+ raise ValueError("authorization reference is required")
+
+
+@dataclass(frozen=True)
+class CredentialUseRequest:
+ """Immutable provider-use scope supplied to the custody boundary."""
+
+ operation_id: str
+ dispatch_id: str
+ workload_id: str
+ connector_id: str
+ channel: ProviderChannel
+ purpose: str
+ recipient_ref: str
+ payload_ref: str
+ idempotency_key: str
+ dispatch_claim_id: str
+ request_fingerprint: str
+
+ def __post_init__(self) -> None:
+ for name in (
+ "operation_id",
+ "dispatch_id",
+ "workload_id",
+ "connector_id",
+ "purpose",
+ "recipient_ref",
+ "payload_ref",
+ "idempotency_key",
+ "dispatch_claim_id",
+ ):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+ expected_fingerprint = dispatch_request_fingerprint(
+ dispatch_id=self.dispatch_id,
+ workflow_id=self.operation_id,
+ channel=Channel(self.channel.value),
+ recipient_ref=self.recipient_ref,
+ payload_ref=self.payload_ref,
+ idempotency_key=self.idempotency_key,
+ )
+ if self.request_fingerprint != expected_fingerprint:
+ raise ValueError("request_fingerprint does not bind the immutable dispatch request")
+
+
+@dataclass(frozen=True)
+class VerifiedWorkloadAuthorization:
+ """Credential-free outcome returned after trusted signature verification."""
+
+ authorization_id: str
+ workload_id: str
+ allowed_connector_ids: frozenset[str]
+ allowed_channels: frozenset[ProviderChannel]
+ allowed_purposes: frozenset[str]
+ request_fingerprint: str
+ not_before: datetime
+ not_after: datetime
+ max_uses: int | None
+ max_provider_attempts: int
+
+ def __post_init__(self) -> None:
+ if not self.authorization_id or not self.workload_id:
+ raise ValueError("authorization_id and workload_id are required")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+ if self.not_before.tzinfo is None or self.not_after.tzinfo is None:
+ raise ValueError("authorization timestamps must be timezone-aware")
+ if self.not_after <= self.not_before:
+ raise ValueError("authorization expiry must be after not_before")
+ if self.max_uses is not None and (type(self.max_uses) is not int or self.max_uses < 1):
+ raise ValueError("max_uses must be positive when bounded")
+ if type(self.max_provider_attempts) is not int or self.max_provider_attempts < 1:
+ raise ValueError("max_provider_attempts must be positive")
+
+
+@dataclass(frozen=True)
+class VerifiedDelegatedAuthorization(VerifiedDispatchGrant):
+ """One verified outcome shared by dispatch admission and credential custody."""
+
+ authorization_id: str
+ issuer: str
+ audience: str
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+ allowed_purposes: frozenset[str]
+ not_before: datetime
+ max_uses: int
+
+ def __post_init__(self) -> None:
+ super().__post_init__()
+ for name in (
+ "authorization_id",
+ "issuer",
+ "audience",
+ "issuer_policy_ref",
+ "rotation_policy_ref",
+ ):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if (
+ not self.allowed_connectors
+ or not self.allowed_purposes
+ or not all(self.allowed_connectors)
+ or not all(self.allowed_purposes)
+ ):
+ raise ValueError("allowed connectors and purposes are required")
+ if self.not_before.tzinfo is None:
+ raise ValueError("not_before must be timezone-aware")
+ if self.not_after <= self.not_before:
+ raise ValueError("authorization expiry must be after not_before")
+ if type(self.max_uses) is not int or self.max_uses != 1:
+ raise ValueError("delegated provider authorization must be single-use")
+
+ def as_workload_authorization(self) -> VerifiedWorkloadAuthorization:
+ """Derive the custody view without repeating signature verification."""
+
+ return VerifiedWorkloadAuthorization(
+ authorization_id=self.authorization_id,
+ workload_id=self.principal,
+ allowed_connector_ids=self.allowed_connectors,
+ allowed_channels=frozenset({ProviderChannel(self.channel.value)}),
+ allowed_purposes=self.allowed_purposes,
+ request_fingerprint=self.request_fingerprint,
+ not_before=self.not_before,
+ not_after=self.not_after,
+ max_uses=self.max_uses,
+ max_provider_attempts=self.max_attempts,
+ )
+
+
+@dataclass(frozen=True)
+class ConsumptionReservation:
+ """Atomic bounded-use reservation produced by a durable ledger."""
+
+ reservation_id: str
+ authorization_id: str
+ request_fingerprint: str
+ idempotency_key: str
+
+ def __post_init__(self) -> None:
+ if not self.reservation_id or not self.authorization_id or not self.idempotency_key:
+ raise ValueError("reservation identifiers are required")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+
+
+@dataclass(frozen=True)
+class AuthorizedCredentialUse:
+ """Non-material authorization passed to a custody-internal invoker."""
+
+ authorization_id: str
+ handle_id: str
+ connector_id: str
+ provider_key: str
+ operation_id: str
+ dispatch_id: str
+ recipient_ref: str
+ payload_ref: str
+ idempotency_key: str
+ request_fingerprint: str
+ dispatch_claim_id: str
+ reservation_id: str = ""
+
+
+@dataclass(frozen=True)
+class AuthorizedProviderDispatch:
+ """Reserved provider dispatch that may select only scoped connector handles."""
+
+ authorization_id: str
+ workload_id: str
+ allowed_connector_ids: frozenset[str]
+ channel: ProviderChannel
+ purpose: str
+ operation_id: str
+ dispatch_id: str
+ recipient_ref: str
+ payload_ref: str
+ idempotency_key: str
+ request_fingerprint: str
+ dispatch_claim_id: str
+ reservation_id: str
+ max_provider_attempts: int
+ not_before: datetime
+ not_after: datetime
+
+ def reservation(self) -> ConsumptionReservation:
+ return ConsumptionReservation(
+ reservation_id=self.reservation_id,
+ authorization_id=self.authorization_id,
+ request_fingerprint=self.request_fingerprint,
+ idempotency_key=self.idempotency_key,
+ )
+
+ def authorize_handle(self, handle: ProviderCredentialHandle) -> AuthorizedCredentialUse:
+ if handle.connector_id not in self.allowed_connector_ids:
+ raise CustodyAuthorizationDenied("connector is outside authorization scope")
+ if handle.channel is not self.channel:
+ raise CustodyAuthorizationDenied("channel is outside authorization scope")
+ return AuthorizedCredentialUse(
+ authorization_id=self.authorization_id,
+ handle_id=handle.handle_id,
+ connector_id=handle.connector_id,
+ provider_key=handle.provider_key,
+ operation_id=self.operation_id,
+ dispatch_id=self.dispatch_id,
+ recipient_ref=self.recipient_ref,
+ payload_ref=self.payload_ref,
+ idempotency_key=self.idempotency_key,
+ request_fingerprint=self.request_fingerprint,
+ dispatch_claim_id=self.dispatch_claim_id,
+ reservation_id=self.reservation_id,
+ )
+
+
+@dataclass(frozen=True)
+class SanitizedCustodyOutcome:
+ """Result safe to emit outside the provider custody boundary."""
+
+ operation_id: str
+ dispatch_id: str
+ provider_key: str
+ status: CustodyResultStatus
+ audit_ref: str
+ failure_code: str = ""
+ retryable: bool = False
+ failover_allowed: bool = False
+ counts_toward_circuit: bool = False
+
+ def __post_init__(self) -> None:
+ if not self.operation_id or not self.dispatch_id or not self.provider_key:
+ raise ValueError("outcome identity fields are required")
+ if not self.audit_ref:
+ raise ValueError("audit_ref is required")
+ if self.failure_code and not _CODE_RE.fullmatch(self.failure_code):
+ raise ValueError("failure_code must be a sanitized identifier")
+ if self.status is CustodyResultStatus.FAILED and not self.failure_code:
+ raise ValueError("failed outcomes require a failure_code")
+ if self.status is not CustodyResultStatus.FAILED and self.failure_code:
+ raise ValueError("successful outcomes cannot contain a failure_code")
+ if self.status is not CustodyResultStatus.FAILED and (
+ self.retryable or self.failover_allowed or self.counts_toward_circuit
+ ):
+ raise ValueError("successful outcomes cannot contain failure policy flags")
+
+
+@dataclass(frozen=True)
+class CustodyAuditEvent:
+ """Sanitized accountability event, suitable for alerting and audit sinks."""
+
+ kind: CustodyAuditKind
+ operation_id: str
+ dispatch_id: str
+ workload_id: str
+ connector_id: str
+ provider_key: str
+ authorization_id: str = ""
+ failure_code: str = ""
+
+ def __post_init__(self) -> None:
+ for name in (
+ "operation_id",
+ "dispatch_id",
+ "workload_id",
+ "connector_id",
+ "provider_key",
+ ):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if self.failure_code and not _CODE_RE.fullmatch(self.failure_code):
+ raise ValueError("failure_code must be a sanitized identifier")
+
+
+class SignedWorkloadAuthorizationVerifier(Protocol):
+ """Validate origin and scope of a signed workload authorization."""
+
+ async def verify(
+ self,
+ reference: WorkloadAuthorizationReference,
+ request: CredentialUseRequest,
+ ) -> VerifiedWorkloadAuthorization:
+ ...
+
+
+class AuthorizationConsumptionLedger(Protocol):
+ """Reserve bounded authority atomically and persist terminal outcomes.
+
+ A concrete implementation must reject reuse with a different request
+ fingerprint. The delegated executor journal is responsible for returning
+ an already completed sanitized delivery outcome rather than invoking twice.
+ """
+
+ async def reserve_once(
+ self,
+ authorization: VerifiedWorkloadAuthorization,
+ request: CredentialUseRequest,
+ ) -> ConsumptionReservation:
+ ...
+
+ async def complete(
+ self,
+ reservation: ConsumptionReservation,
+ outcome: SanitizedCustodyOutcome,
+ ) -> None:
+ ...
+
+ async def reserve_attempt(
+ self,
+ reservation: ConsumptionReservation,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> int:
+ """Atomically reserve and return the next provider-attempt number."""
+ ...
+
+
+class CustodiedReferenceResolver(Protocol):
+ """Invoke a provider operation while keeping resolved material internal."""
+
+ async def invoke(
+ self,
+ handle: ProviderCredentialHandle,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> SanitizedCustodyOutcome:
+ ...
+
+
+class CustodyAuditSink(Protocol):
+ """Persist sanitized custody transitions for accountability."""
+
+ async def emit(self, event: CustodyAuditEvent) -> None:
+ ...
+
+
+class CustodyFailureNotifier(Protocol):
+ """Raise sanitized operational notifications for custody failures."""
+
+ async def notify(self, event: CustodyAuditEvent) -> None:
+ ...
+
+
+async def authorize_provider_dispatch(
+ initial_handle: ProviderCredentialHandle,
+ request: CredentialUseRequest,
+ authorization: VerifiedWorkloadAuthorization,
+ *,
+ ledger: AuthorizationConsumptionLedger,
+ provider_attempt_budget: int | None = None,
+ required_connector_ids: frozenset[str] | None = None,
+ now: datetime | None = None,
+) -> AuthorizedProviderDispatch:
+ """Validate and reserve a credential-free verifier outcome for one dispatch.
+
+ Provider-use authority must describe exactly one dispatch and cannot cross
+ this boundary unless a ledger has atomically reserved the exact
+ authorization, request fingerprint, and idempotency key.
+ """
+
+ current_time = now or datetime.now(UTC)
+ if current_time < authorization.not_before or current_time >= authorization.not_after:
+ raise CustodyAuthorizationDenied("workload authorization is outside its validity window")
+ if authorization.workload_id != request.workload_id:
+ raise CustodyAuthorizationDenied("workload identity is outside authorization scope")
+ if authorization.request_fingerprint != request.request_fingerprint:
+ raise CustodyAuthorizationDenied("request fingerprint is outside authorization scope")
+ if initial_handle.connector_id != request.connector_id:
+ raise CustodyAuthorizationDenied(
+ "initial credential handle does not match requested connector"
+ )
+ if request.connector_id not in authorization.allowed_connector_ids:
+ raise CustodyAuthorizationDenied("initial connector is outside authorization scope")
+ if required_connector_ids and not required_connector_ids.issubset(
+ authorization.allowed_connector_ids
+ ):
+ raise CustodyAuthorizationDenied("route policy is outside authorization scope")
+ if (
+ initial_handle.channel is not request.channel
+ or request.channel not in authorization.allowed_channels
+ ):
+ raise CustodyAuthorizationDenied("channel is outside authorization scope")
+ if request.purpose not in authorization.allowed_purposes:
+ raise CustodyAuthorizationDenied("purpose is outside authorization scope")
+ if (
+ provider_attempt_budget is not None
+ and provider_attempt_budget > authorization.max_provider_attempts
+ ):
+ raise CustodyAuthorizationDenied(
+ "dispatch attempt budget exceeds credential custody scope"
+ )
+
+ if authorization.max_uses != 1:
+ raise CustodyAuthorizationDenied(
+ "provider invocation requires single-dispatch authorization"
+ )
+ reservation = await ledger.reserve_once(authorization, request)
+ if (
+ reservation.authorization_id != authorization.authorization_id
+ or reservation.request_fingerprint != request.request_fingerprint
+ or reservation.idempotency_key != request.idempotency_key
+ ):
+ raise ConsumptionReservationRequired(
+ "ledger reservation does not bind this authorization request"
+ )
+
+ return AuthorizedProviderDispatch(
+ authorization_id=authorization.authorization_id,
+ workload_id=request.workload_id,
+ allowed_connector_ids=authorization.allowed_connector_ids,
+ channel=request.channel,
+ purpose=request.purpose,
+ operation_id=request.operation_id,
+ dispatch_id=request.dispatch_id,
+ recipient_ref=request.recipient_ref,
+ payload_ref=request.payload_ref,
+ idempotency_key=request.idempotency_key,
+ request_fingerprint=request.request_fingerprint,
+ dispatch_claim_id=request.dispatch_claim_id,
+ reservation_id=reservation.reservation_id,
+ max_provider_attempts=authorization.max_provider_attempts,
+ not_before=authorization.not_before,
+ not_after=authorization.not_after,
+ )
+
+
+class CredentialCustodyAuthorizer:
+ """Verify or accept one trusted outcome, then reserve its exact operation."""
+
+ def __init__(
+ self,
+ verifier: SignedWorkloadAuthorizationVerifier | None,
+ ledger: AuthorizationConsumptionLedger,
+ *,
+ audit_sink: CustodyAuditSink | None = None,
+ failure_notifier: CustodyFailureNotifier | None = None,
+ clock: Callable[[], datetime] | None = None,
+ ):
+ self._verifier = verifier
+ self._ledger = ledger
+ self._audit_sink = audit_sink
+ self._failure_notifier = failure_notifier
+ self._clock = clock or (lambda: datetime.now(UTC))
+
+ @classmethod
+ def for_verified_outcomes(
+ cls,
+ ledger: AuthorizationConsumptionLedger,
+ *,
+ audit_sink: CustodyAuditSink | None = None,
+ failure_notifier: CustodyFailureNotifier | None = None,
+ clock: Callable[[], datetime] | None = None,
+ ) -> CredentialCustodyAuthorizer:
+ """Construct the reservation path used after one trusted verification."""
+
+ return cls(
+ None,
+ ledger,
+ audit_sink=audit_sink,
+ failure_notifier=failure_notifier,
+ clock=clock,
+ )
+
+ async def authorize(
+ self,
+ reference: WorkloadAuthorizationReference,
+ handle: ProviderCredentialHandle,
+ request: CredentialUseRequest,
+ *,
+ provider_attempt_budget: int | None = None,
+ required_connector_ids: frozenset[str] | None = None,
+ ) -> AuthorizedProviderDispatch:
+ authorization_id = ""
+ try:
+ if self._verifier is None:
+ raise CustodyAuthorizationDenied(
+ "signed workload authorization verifier is not configured"
+ )
+ authorization = await self._verifier.verify(reference, request)
+ authorization_id = authorization.authorization_id
+ except CustodyAuthorizationDenied:
+ await self._record_failure(
+ request,
+ handle,
+ authorization_id=authorization_id,
+ kind=CustodyAuditKind.AUTHORIZATION_DENIED,
+ failure_code="authorization_denied",
+ )
+ raise
+ except Exception as exc:
+ await self._record_failure(
+ request,
+ handle,
+ authorization_id=authorization_id,
+ kind=CustodyAuditKind.AUTHORIZATION_DENIED,
+ failure_code="verification_unavailable",
+ )
+ raise CustodyAuthorizationDenied("workload authorization verification failed") from exc
+
+ return await self.authorize_verified(
+ authorization,
+ handle,
+ request,
+ provider_attempt_budget=provider_attempt_budget,
+ required_connector_ids=required_connector_ids,
+ )
+
+ async def authorize_verified(
+ self,
+ authorization: VerifiedWorkloadAuthorization,
+ handle: ProviderCredentialHandle,
+ request: CredentialUseRequest,
+ *,
+ provider_attempt_budget: int | None = None,
+ required_connector_ids: frozenset[str] | None = None,
+ ) -> AuthorizedProviderDispatch:
+ """Reserve a previously verified outcome without invoking another verifier."""
+
+ try:
+ reserved_dispatch = await authorize_provider_dispatch(
+ handle,
+ request,
+ authorization,
+ ledger=self._ledger,
+ provider_attempt_budget=provider_attempt_budget,
+ required_connector_ids=required_connector_ids,
+ now=self._clock(),
+ )
+ except ConsumptionReservationRequired:
+ await self._record_failure(
+ request,
+ handle,
+ authorization_id=authorization.authorization_id,
+ kind=CustodyAuditKind.RESERVATION_REQUIRED,
+ failure_code="reservation_required",
+ )
+ raise
+ except CustodyAuthorizationDenied:
+ await self._record_failure(
+ request,
+ handle,
+ authorization_id=authorization.authorization_id,
+ kind=CustodyAuditKind.AUTHORIZATION_DENIED,
+ failure_code="authorization_denied",
+ )
+ raise
+ except Exception as exc:
+ await self._record_failure(
+ request,
+ handle,
+ authorization_id=authorization.authorization_id,
+ kind=CustodyAuditKind.AUTHORIZATION_DENIED,
+ failure_code="authorization_unavailable",
+ )
+ raise CustodyAuthorizationDenied("workload authorization reservation failed") from exc
+
+ await self._emit_audit(
+ CustodyAuditEvent(
+ kind=CustodyAuditKind.AUTHORIZATION_ACCEPTED,
+ operation_id=request.operation_id,
+ dispatch_id=request.dispatch_id,
+ workload_id=request.workload_id,
+ connector_id=handle.connector_id,
+ provider_key=handle.provider_key,
+ authorization_id=reserved_dispatch.authorization_id,
+ )
+ )
+ return reserved_dispatch
+
+ async def record_outcome(
+ self,
+ dispatch: AuthorizedProviderDispatch,
+ handle: ProviderCredentialHandle,
+ outcome: SanitizedCustodyOutcome,
+ ) -> None:
+ await self._ledger.complete(dispatch.reservation(), outcome)
+ kind = (
+ CustodyAuditKind.INVOCATION_FAILED
+ if outcome.status is CustodyResultStatus.FAILED
+ else CustodyAuditKind.INVOCATION_COMPLETED
+ )
+ event = CustodyAuditEvent(
+ kind=kind,
+ operation_id=dispatch.operation_id,
+ dispatch_id=dispatch.dispatch_id,
+ workload_id=dispatch.workload_id,
+ connector_id=handle.connector_id,
+ provider_key=handle.provider_key,
+ authorization_id=dispatch.authorization_id,
+ failure_code=outcome.failure_code,
+ )
+ if outcome.status is CustodyResultStatus.FAILED:
+ await self._emit_and_notify_failure(event)
+ else:
+ await self._emit_audit(event)
+
+ async def reserve_attempt(
+ self,
+ dispatch: AuthorizedProviderDispatch,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> int:
+ self._validate_attempt_window(dispatch)
+ attempt = await self._ledger.reserve_attempt(dispatch.reservation(), authorized_use)
+ # Reserving can wait for durable state. Expiry while waiting still denies
+ # provider use; the consumed budget remains consumed.
+ self._validate_attempt_window(dispatch)
+ return attempt
+
+ def _validate_attempt_window(self, dispatch: AuthorizedProviderDispatch) -> None:
+ if not dispatch.not_before <= self._clock() < dispatch.not_after:
+ raise CustodyAuthorizationDenied("provider authorization is outside its validity window")
+
+ async def record_invocation_failure(
+ self,
+ dispatch: AuthorizedProviderDispatch,
+ handle: ProviderCredentialHandle,
+ failure_code: str,
+ ) -> None:
+ event = CustodyAuditEvent(
+ kind=CustodyAuditKind.INVOCATION_FAILED,
+ operation_id=dispatch.operation_id,
+ dispatch_id=dispatch.dispatch_id,
+ workload_id=dispatch.workload_id,
+ connector_id=handle.connector_id,
+ provider_key=handle.provider_key,
+ authorization_id=dispatch.authorization_id,
+ failure_code=failure_code,
+ )
+ await self._emit_and_notify_failure(event)
+
+ async def _record_failure(
+ self,
+ request: CredentialUseRequest,
+ handle: ProviderCredentialHandle,
+ *,
+ authorization_id: str,
+ kind: CustodyAuditKind,
+ failure_code: str,
+ ) -> None:
+ event = CustodyAuditEvent(
+ kind=kind,
+ operation_id=request.operation_id,
+ dispatch_id=request.dispatch_id,
+ workload_id=request.workload_id,
+ connector_id=handle.connector_id,
+ provider_key=handle.provider_key,
+ authorization_id=authorization_id,
+ failure_code=failure_code,
+ )
+ await self._emit_and_notify_failure(event)
+
+ async def _emit_and_notify_failure(self, event: CustodyAuditEvent) -> None:
+ audit_error: CustodyMonitoringUnavailable | None = None
+ try:
+ await self._emit_audit(event)
+ except CustodyMonitoringUnavailable as exc:
+ audit_error = exc
+ try:
+ await self._notify_failure(event)
+ except CustodyMonitoringUnavailable:
+ if audit_error is not None:
+ raise CustodyMonitoringUnavailable(
+ "custody audit and failure notification persistence failed"
+ ) from audit_error
+ raise
+ if audit_error is not None:
+ raise audit_error
+
+ async def _emit_audit(self, event: CustodyAuditEvent) -> None:
+ if self._audit_sink is None:
+ return
+ try:
+ await self._audit_sink.emit(event)
+ except Exception as exc:
+ raise CustodyMonitoringUnavailable("custody audit persistence failed") from exc
+
+ async def _notify_failure(self, event: CustodyAuditEvent) -> None:
+ if self._failure_notifier is None:
+ return
+ try:
+ await self._failure_notifier.notify(event)
+ except Exception as exc:
+ raise CustodyMonitoringUnavailable("custody failure notification persistence failed") from exc
+
+
+class CredentialCustodyInvokerFactory:
+ """Connect delegated dispatch to a single reserved custody execution path."""
+
+ def __init__(
+ self,
+ authorizer: CredentialCustodyAuthorizer,
+ resolver: CustodiedReferenceResolver,
+ handles: Mapping[str, ProviderCredentialHandle],
+ *,
+ workload_id: str,
+ purpose: str,
+ ):
+ if not workload_id or not purpose:
+ raise ValueError("workload_id and purpose are required")
+ self._authorizer = authorizer
+ self._resolver = resolver
+ self._handles = dict(handles)
+ self._workload_id = workload_id
+ self._purpose = purpose
+
+ def _handle_for_route(self, route: ConnectorRoute) -> ProviderCredentialHandle:
+ handle = self._handles.get(route.connector_id)
+ expected_channel = ProviderChannel(route.channel.value)
+ if (
+ handle is None
+ or handle.connector_id != route.connector_id
+ or handle.handle_id != route.credential_handle
+ or handle.provider_key != route.provider_key
+ or handle.channel is not expected_channel
+ ):
+ raise DelegatedAuthorizationDenied("connector custody binding is invalid")
+ return handle
+
+ async def prepare(
+ self,
+ _capability: CapabilityReference,
+ request: DispatchRequest,
+ grant: VerifiedDispatchGrant,
+ claim: DispatchClaim,
+ initial_route: ConnectorRoute,
+ authorized_routes: tuple[ConnectorRoute, ...],
+ ) -> CustodiedProviderInvoker:
+ if not isinstance(grant, VerifiedDelegatedAuthorization):
+ raise DelegatedAuthorizationDenied(
+ "credential custody requires one shared verified authorization outcome"
+ )
+ if initial_route not in authorized_routes:
+ raise DelegatedAuthorizationDenied("initial route is outside dispatch policy")
+ initial_handle = self._handle_for_route(initial_route)
+ for route in authorized_routes:
+ self._handle_for_route(route)
+ custody_request = CredentialUseRequest(
+ operation_id=request.workflow_id,
+ dispatch_id=request.dispatch_id,
+ workload_id=self._workload_id,
+ connector_id=initial_route.connector_id,
+ channel=ProviderChannel(request.channel.value),
+ purpose=self._purpose,
+ recipient_ref=request.recipient_ref,
+ payload_ref=request.payload_ref,
+ idempotency_key=request.idempotency_key,
+ dispatch_claim_id=claim.claim_id,
+ request_fingerprint=request.request_fingerprint,
+ )
+ try:
+ reserved_dispatch = await self._authorizer.authorize_verified(
+ grant.as_workload_authorization(),
+ initial_handle,
+ custody_request,
+ provider_attempt_budget=grant.max_attempts,
+ required_connector_ids=frozenset(
+ route.connector_id for route in authorized_routes
+ ),
+ )
+ except CustodyAuthorizationDenied as exc:
+ raise DelegatedAuthorizationDenied("credential custody authorization denied") from exc
+ return _ReservedCustodiedProviderInvoker(
+ authorizer=self._authorizer,
+ resolver=self._resolver,
+ handles=self._handles,
+ dispatch=reserved_dispatch,
+ request=request,
+ )
+
+
+class _ReservedCustodiedProviderInvoker:
+ def __init__(
+ self,
+ *,
+ authorizer: CredentialCustodyAuthorizer,
+ resolver: CustodiedReferenceResolver,
+ handles: Mapping[str, ProviderCredentialHandle],
+ dispatch: AuthorizedProviderDispatch,
+ request: DispatchRequest,
+ ):
+ self._authorizer = authorizer
+ self._resolver = resolver
+ self._handles = dict(handles)
+ self._dispatch = dispatch
+ self._request = request
+
+ async def invoke(
+ self, route: ConnectorRoute, request: DispatchRequest
+ ) -> ProviderAttemptOutcome:
+ if (
+ request.dispatch_id != self._request.dispatch_id
+ or request.request_fingerprint != self._request.request_fingerprint
+ or request.idempotency_key != self._request.idempotency_key
+ ):
+ raise DelegatedAuthorizationDenied("prepared custody dispatch was rebound")
+ handle = self._handles.get(route.connector_id)
+ if (
+ handle is None
+ or handle.connector_id != route.connector_id
+ or handle.handle_id != route.credential_handle
+ or handle.provider_key != route.provider_key
+ or handle.channel.value != route.channel.value
+ ):
+ raise DelegatedAuthorizationDenied("connector custody binding is invalid")
+ try:
+ authorized_use = self._dispatch.authorize_handle(handle)
+ await self._authorizer.reserve_attempt(self._dispatch, authorized_use)
+ outcome = await self._resolver.invoke(handle, authorized_use)
+ except CustodyAuthorizationDenied as exc:
+ await self._authorizer.record_invocation_failure(
+ self._dispatch,
+ handle,
+ "provider_custody_denied",
+ )
+ raise DelegatedAuthorizationDenied("provider custody invocation denied") from exc
+ except Exception:
+ await self._authorizer.record_invocation_failure(
+ self._dispatch,
+ handle,
+ "resolver_unavailable",
+ )
+ raise
+ if (
+ outcome.operation_id != self._dispatch.operation_id
+ or outcome.dispatch_id != request.dispatch_id
+ or outcome.provider_key != route.provider_key
+ ):
+ raise DelegatedAuthorizationDenied("custody outcome binding is invalid")
+ try:
+ await self._authorizer.record_outcome(self._dispatch, handle, outcome)
+ except Exception: # noqa: BLE001 - contain provider/backend failures without leaking material.
+ try:
+ await self._authorizer.record_invocation_failure(
+ self._dispatch,
+ handle,
+ "custody_state_unavailable",
+ )
+ except CustodyMonitoringUnavailable:
+ pass
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus.FAILED,
+ audit_ref=outcome.audit_ref,
+ failure_code="custody_state_unavailable",
+ retryable=False,
+ failover_allowed=False,
+ counts_toward_circuit=False,
+ )
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus(outcome.status.value),
+ audit_ref=outcome.audit_ref,
+ failure_code=outcome.failure_code,
+ retryable=outcome.retryable,
+ failover_allowed=outcome.failover_allowed,
+ counts_toward_circuit=outcome.counts_toward_circuit,
+ )
diff --git a/src/baton/delegated_connector.py b/src/baton/delegated_connector.py
new file mode 100644
index 0000000..353eed4
--- /dev/null
+++ b/src/baton/delegated_connector.py
@@ -0,0 +1,717 @@
+"""Cloud-neutral delegated external-provider dispatch orchestration.
+
+This module is the Baton-side runtime boundary for provider-backed delivery.
+It accepts opaque authorization and connector references, orchestrates retries,
+failover, and circuit breakers, and returns sanitized outcomes only. The
+``CustodiedProviderInvokerFactory`` prepares a custody-authorized invoker
+immediately before a provider attempt. Its prepared invoker is the
+single-purpose boundary allowed to resolve and use provider credential
+material internally.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import hashlib
+import json
+import re
+import time
+from collections.abc import Awaitable, Callable, Sequence
+from dataclasses import dataclass
+from datetime import UTC, datetime
+from enum import StrEnum
+from typing import Protocol
+
+_SAFE_CODE_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
+_FINGERPRINT_RE = re.compile(r"^[a-f0-9]{64}$")
+
+
+class Channel(StrEnum):
+ SMS = "sms"
+ EMAIL = "email"
+
+
+def dispatch_request_fingerprint(
+ *,
+ dispatch_id: str,
+ workflow_id: str,
+ channel: Channel,
+ recipient_ref: str,
+ payload_ref: str,
+ idempotency_key: str,
+) -> str:
+ """Return the domain-separated digest for one immutable dispatch request."""
+
+ payload = {
+ "channel": channel.value,
+ "dispatch_id": dispatch_id,
+ "idempotency_key": idempotency_key,
+ "payload_ref": payload_ref,
+ "recipient_ref": recipient_ref,
+ "schema": "baton.delegated-dispatch.v1",
+ "workflow_id": workflow_id,
+ }
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
+ return hashlib.sha256(encoded).hexdigest()
+
+
+class DeliveryStatus(StrEnum):
+ ACCEPTED = "accepted"
+ DELIVERED = "delivered"
+ FAILED = "failed"
+ EXHAUSTED = "exhausted"
+
+
+class DispatchSignalKind(StrEnum):
+ AUTHORIZATION_DENIED = "authorization_denied"
+ IDEMPOTENCY_CONFLICT = "idempotency_conflict"
+ ATTEMPT_FAILED = "attempt_failed"
+ ATTEMPT_SUCCEEDED = "attempt_succeeded"
+ CIRCUIT_OPEN = "circuit_open"
+ FAILOVER_USED = "failover_used"
+ DELIVERY_EXHAUSTED = "delivery_exhausted"
+
+
+class DelegatedConnectorError(Exception):
+ """Base error for dispatch decisions that never expose provider material."""
+
+
+class AuthorizationDenied(DelegatedConnectorError):
+ """A capability cannot authorize this dispatch."""
+
+
+class DispatchInProgress(DelegatedConnectorError):
+ """The idempotency key is already executing elsewhere."""
+
+
+class DispatchBindingConflict(DelegatedConnectorError):
+ """An idempotency key was reused for different immutable request content."""
+
+
+class DispatchStateUnavailable(DelegatedConnectorError):
+ """Durable dispatch state could not record a post-invocation transition."""
+
+
+class MonitoringUnavailable(DelegatedConnectorError):
+ """A required sanitized signal could not be persisted."""
+
+
+class InvalidConnectorPolicy(ValueError):
+ """Connector routes do not form a valid ordered provider policy."""
+
+
+@dataclass(frozen=True)
+class CapabilityReference:
+ """Opaque reference to an authorization proof verified inside the stack."""
+
+ reference: str
+
+ def __post_init__(self) -> None:
+ if not self.reference:
+ raise ValueError("capability reference is required")
+
+
+@dataclass(frozen=True)
+class DispatchRequest:
+ """A provider dispatch instruction containing references, never material."""
+
+ dispatch_id: str
+ workflow_id: str
+ channel: Channel
+ recipient_ref: str
+ payload_ref: str
+ idempotency_key: str
+ request_fingerprint: str
+
+ def __post_init__(self) -> None:
+ for name in (
+ "dispatch_id",
+ "workflow_id",
+ "recipient_ref",
+ "payload_ref",
+ "idempotency_key",
+ ):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+ expected_fingerprint = dispatch_request_fingerprint(
+ dispatch_id=self.dispatch_id,
+ workflow_id=self.workflow_id,
+ channel=self.channel,
+ recipient_ref=self.recipient_ref,
+ payload_ref=self.payload_ref,
+ idempotency_key=self.idempotency_key,
+ )
+ if self.request_fingerprint != expected_fingerprint:
+ raise ValueError("request_fingerprint does not bind the immutable dispatch request")
+
+
+@dataclass(frozen=True)
+class VerifiedDispatchGrant:
+ """Verified authorization scope returned by a trusted verifier."""
+
+ principal: str
+ channel: Channel
+ allowed_connectors: frozenset[str]
+ not_after: datetime
+ max_attempts: int
+ request_fingerprint: str
+
+ def __post_init__(self) -> None:
+ if not self.principal:
+ raise ValueError("principal is required")
+ if self.not_after.tzinfo is None:
+ raise ValueError("not_after must be timezone-aware")
+ if type(self.max_attempts) is not int or self.max_attempts < 1:
+ raise ValueError("max_attempts must be positive")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+
+
+@dataclass(frozen=True)
+class ConnectorRoute:
+ """Enabled provider metadata and opaque custody binding."""
+
+ connector_id: str
+ provider_key: str
+ channel: Channel
+ credential_handle: str
+ priority: int
+ enabled: bool = True
+ timeout_ms: int = 5000
+ max_attempts: int = 1
+ retry_backoff_ms: int = 100
+ circuit_breaker_threshold: int = 3
+ circuit_reset_seconds: float = 60.0
+
+ def __post_init__(self) -> None:
+ for name in ("connector_id", "provider_key", "credential_handle"):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if self.priority < 0:
+ raise ValueError("priority cannot be negative")
+ if self.timeout_ms < 1:
+ raise ValueError("timeout_ms must be positive")
+ if type(self.max_attempts) is not int or self.max_attempts < 1:
+ raise ValueError("max_attempts must be positive")
+ if self.retry_backoff_ms < 0:
+ raise ValueError("retry_backoff_ms cannot be negative")
+ if self.circuit_breaker_threshold < 0:
+ raise ValueError("circuit_breaker_threshold cannot be negative")
+ if self.circuit_reset_seconds < 0:
+ raise ValueError("circuit_reset_seconds cannot be negative")
+
+
+@dataclass(frozen=True)
+class ProviderAttemptOutcome:
+ """Sanitized result returned by the custody-internal provider invoker."""
+
+ status: DeliveryStatus
+ audit_ref: str
+ failure_code: str = ""
+ retryable: bool = False
+ failover_allowed: bool = False
+ counts_toward_circuit: bool = False
+
+ def __post_init__(self) -> None:
+ if self.status not in (DeliveryStatus.ACCEPTED, DeliveryStatus.DELIVERED, DeliveryStatus.FAILED):
+ raise ValueError("attempt status must be accepted, delivered, or failed")
+ if not self.audit_ref:
+ raise ValueError("audit_ref is required")
+ if self.failure_code and not _SAFE_CODE_RE.fullmatch(self.failure_code):
+ raise ValueError("failure_code must contain sanitized identifier characters only")
+ if self.status is DeliveryStatus.FAILED and not self.failure_code:
+ raise ValueError("failed attempts require a sanitized failure_code")
+ if self.status is not DeliveryStatus.FAILED and self.failure_code:
+ raise ValueError("successful attempts cannot include a failure_code")
+
+
+@dataclass(frozen=True)
+class DeliveryOutcome:
+ """Sanitized result safe to return to a caller such as MEA comms."""
+
+ dispatch_id: str
+ workflow_id: str
+ channel: Channel
+ connector_id: str
+ provider_key: str
+ status: DeliveryStatus
+ attempt_count: int
+ failover_used: bool
+ audit_ref: str
+ failure_code: str = ""
+
+
+@dataclass(frozen=True)
+class DispatchBinding:
+ """Immutable journal binding for one idempotent dispatch instruction."""
+
+ idempotency_key: str
+ request_fingerprint: str
+
+ def __post_init__(self) -> None:
+ if not self.idempotency_key:
+ raise ValueError("idempotency_key is required")
+ if not _FINGERPRINT_RE.fullmatch(self.request_fingerprint):
+ raise ValueError("request_fingerprint must be a lowercase SHA-256 digest")
+
+
+@dataclass(frozen=True)
+class DispatchClaim:
+ """Lease-bearing ownership token for one durable dispatch attempt."""
+
+ claim_id: str
+ binding: DispatchBinding
+ lease_expires_at: datetime
+
+ def __post_init__(self) -> None:
+ if not self.claim_id:
+ raise ValueError("claim_id is required")
+ if self.lease_expires_at.tzinfo is None:
+ raise ValueError("lease_expires_at must be timezone-aware")
+
+
+@dataclass(frozen=True)
+class DispatchSignal:
+ """Non-sensitive operational event for audit and alert pipelines.
+
+ Every signal carries ``dispatch_id`` so failures, failover, and terminal
+ delivery can be correlated without exposing provider material.
+ """
+
+ kind: DispatchSignalKind
+ workflow_id: str
+ channel: Channel
+ dispatch_id: str
+ connector_id: str = ""
+ provider_key: str = ""
+ attempt_count: int = 0
+ failure_code: str = ""
+
+
+class ScopedAuthorizationVerifier(Protocol):
+ """Verifies capability origin and dispatch scope against trusted policy."""
+
+ async def verify(
+ self, capability: CapabilityReference, request: DispatchRequest
+ ) -> VerifiedDispatchGrant:
+ ...
+
+
+class CustodiedProviderInvoker(Protocol):
+ """Executes one provider operation inside the credential custody boundary."""
+
+ async def invoke(
+ self, route: ConnectorRoute, request: DispatchRequest
+ ) -> ProviderAttemptOutcome:
+ """Use only the matching handle internally and return sanitized metadata."""
+ ...
+
+
+class CustodiedProviderInvokerFactory(Protocol):
+ """Prepare a custody-bound invoker after idempotency begins, before sending."""
+
+ async def prepare(
+ self,
+ capability: CapabilityReference,
+ request: DispatchRequest,
+ grant: VerifiedDispatchGrant,
+ claim: DispatchClaim,
+ initial_route: ConnectorRoute,
+ authorized_routes: tuple[ConnectorRoute, ...],
+ ) -> CustodiedProviderInvoker:
+ """Reserve authority for the attempted route policy without using material."""
+ ...
+
+
+class DispatchJournal(Protocol):
+ """Atomic idempotency journal required before any provider invocation.
+
+ Implementations must raise ``DispatchBindingConflict`` when an
+ idempotency key was previously bound to a different request fingerprint.
+ """
+
+ async def completed(self, binding: DispatchBinding) -> DeliveryOutcome | None:
+ ...
+
+ async def begin(self, binding: DispatchBinding) -> DispatchClaim | None:
+ ...
+
+ async def renew(self, claim: DispatchClaim) -> None:
+ ...
+
+ async def complete(self, claim: DispatchClaim, outcome: DeliveryOutcome) -> None:
+ ...
+
+ async def abort(self, claim: DispatchClaim) -> None:
+ ...
+
+
+class DispatchSignalSink(Protocol):
+ """Durably accepts sanitized events used for audit and alerting.
+
+ Terminal events must be idempotent by ``(kind, dispatch_id)`` because
+ completed dispatches retry notification delivery without resending.
+ """
+
+ async def emit(self, signal: DispatchSignal) -> None:
+ ...
+
+
+@dataclass
+class _CircuitState:
+ failures: int = 0
+ opened_at: float | None = None
+
+
+class DelegatedConnectorExecutor:
+ """Dispatches through authorized ordered connectors with fail-closed controls."""
+
+ def __init__(
+ self,
+ routes: Sequence[ConnectorRoute],
+ verifier: ScopedAuthorizationVerifier,
+ invoker_factory: CustodiedProviderInvokerFactory,
+ journal: DispatchJournal,
+ signal_sink: DispatchSignalSink,
+ *,
+ clock: Callable[[], datetime] | None = None,
+ monotonic: Callable[[], float] | None = None,
+ sleep: Callable[[float], Awaitable[None]] | None = None,
+ ):
+ self._routes = self._validate_routes(routes)
+ self._verifier = verifier
+ self._invoker_factory = invoker_factory
+ self._journal = journal
+ self._signal_sink = signal_sink
+ self._clock = clock or (lambda: datetime.now(UTC))
+ self._monotonic = monotonic or time.monotonic
+ self._sleep = sleep or asyncio.sleep
+ self._circuits: dict[str, _CircuitState] = {}
+
+ @staticmethod
+ def _validate_routes(routes: Sequence[ConnectorRoute]) -> tuple[ConnectorRoute, ...]:
+ ids: set[str] = set()
+ priorities: set[tuple[Channel, int]] = set()
+ for route in routes:
+ if route.connector_id in ids:
+ raise InvalidConnectorPolicy(f"duplicate connector id: {route.connector_id}")
+ ids.add(route.connector_id)
+ if route.enabled:
+ priority_key = (route.channel, route.priority)
+ if priority_key in priorities:
+ raise InvalidConnectorPolicy(
+ f"duplicate active priority for channel: {route.channel.value}/{route.priority}"
+ )
+ priorities.add(priority_key)
+ return tuple(sorted(routes, key=lambda route: (route.channel.value, route.priority)))
+
+ async def dispatch(
+ self, capability: CapabilityReference, request: DispatchRequest
+ ) -> DeliveryOutcome:
+ """Dispatch once per key and immutable request fingerprint after verification."""
+ try:
+ grant = await self._verifier.verify(capability, request)
+ self._validate_grant(grant, request)
+ except Exception as exc:
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.AUTHORIZATION_DENIED,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ failure_code="authorization_denied",
+ )
+ )
+ if isinstance(exc, AuthorizationDenied):
+ raise
+ raise AuthorizationDenied("dispatch authorization denied") from exc
+
+ binding = DispatchBinding(request.idempotency_key, request.request_fingerprint)
+ try:
+ completed = await self._journal.completed(binding)
+ except DispatchBindingConflict:
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.IDEMPOTENCY_CONFLICT,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ failure_code="idempotency_binding_conflict",
+ )
+ )
+ raise
+ if completed is not None:
+ await self._emit_terminal(completed)
+ return completed
+ try:
+ claim = await self._journal.begin(binding)
+ except DispatchBindingConflict:
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.IDEMPOTENCY_CONFLICT,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ failure_code="idempotency_binding_conflict",
+ )
+ )
+ raise
+ if claim is None:
+ completed = await self._journal.completed(binding)
+ if completed is not None:
+ await self._emit_terminal(completed)
+ return completed
+ raise DispatchInProgress("dispatch with this idempotency key is already in progress")
+
+ try:
+ outcome = await self._dispatch_authorized(capability, grant, claim, request)
+ except AuthorizationDenied:
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.AUTHORIZATION_DENIED,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ failure_code="authorization_denied",
+ )
+ )
+ raise
+
+ try:
+ await self._journal.complete(claim, outcome)
+ except Exception as exc:
+ raise DispatchStateUnavailable(
+ "durable dispatch outcome persistence failed after provider execution"
+ ) from exc
+
+ await self._emit_terminal(outcome)
+ return outcome
+
+ def _validate_grant(self, grant: VerifiedDispatchGrant, request: DispatchRequest) -> None:
+ if grant.request_fingerprint != request.request_fingerprint:
+ raise AuthorizationDenied("dispatch fingerprint is outside authorization scope")
+ if grant.channel is not request.channel:
+ raise AuthorizationDenied("dispatch channel is outside authorization scope")
+ if self._clock() >= grant.not_after:
+ raise AuthorizationDenied("dispatch authorization has expired")
+ enabled = {
+ route.connector_id
+ for route in self._routes
+ if route.channel is request.channel and route.enabled
+ }
+ if not enabled.intersection(grant.allowed_connectors):
+ raise AuthorizationDenied("no authorized active connector for dispatch")
+
+ async def _dispatch_authorized(
+ self,
+ capability: CapabilityReference,
+ grant: VerifiedDispatchGrant,
+ claim: DispatchClaim,
+ request: DispatchRequest,
+ ) -> DeliveryOutcome:
+ routes = tuple(
+ route
+ for route in self._routes
+ if route.channel is request.channel
+ and route.enabled
+ and route.connector_id in grant.allowed_connectors
+ )
+ attempt_count = 0
+ last_connector = ""
+ last_provider = ""
+ last_audit_ref = ""
+ last_failure = "provider_unavailable"
+ failover_used = False
+ attempted_or_skipped_route = False
+ allow_next_route = True
+ invoker: CustodiedProviderInvoker | None = None
+
+ for route in routes:
+ if not allow_next_route:
+ break
+ if self._circuit_is_open(route):
+ last_connector = route.connector_id
+ last_provider = route.provider_key
+ last_audit_ref = f"circuit:{request.dispatch_id}:{route.connector_id}"
+ last_failure = "circuit_open"
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.CIRCUIT_OPEN,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ connector_id=route.connector_id,
+ provider_key=route.provider_key,
+ attempt_count=attempt_count,
+ failure_code="circuit_open",
+ )
+ )
+ attempted_or_skipped_route = True
+ continue
+ if attempted_or_skipped_route:
+ failover_used = True
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.FAILOVER_USED,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ connector_id=route.connector_id,
+ provider_key=route.provider_key,
+ attempt_count=attempt_count,
+ )
+ )
+ attempted_or_skipped_route = True
+ allow_next_route = False
+ if invoker is None:
+ await self._journal.renew(claim)
+ invoker = await self._invoker_factory.prepare(
+ capability,
+ request,
+ grant,
+ claim,
+ route,
+ routes,
+ )
+
+ for local_attempt in range(route.max_attempts):
+ if attempt_count >= grant.max_attempts:
+ break
+ attempt_count += 1
+ last_connector = route.connector_id
+ last_provider = route.provider_key
+ await self._journal.renew(claim)
+ attempt = await self._invoke(invoker, route, request)
+ last_audit_ref = attempt.audit_ref
+ if attempt.status in (DeliveryStatus.ACCEPTED, DeliveryStatus.DELIVERED):
+ self._record_success(route)
+ return DeliveryOutcome(
+ dispatch_id=request.dispatch_id,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ connector_id=route.connector_id,
+ provider_key=route.provider_key,
+ status=attempt.status,
+ attempt_count=attempt_count,
+ failover_used=failover_used,
+ audit_ref=attempt.audit_ref,
+ )
+
+ last_failure = attempt.failure_code
+ allow_next_route = attempt.failover_allowed
+ if attempt.counts_toward_circuit:
+ self._record_failure(route)
+ await self._emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.ATTEMPT_FAILED,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ dispatch_id=request.dispatch_id,
+ connector_id=route.connector_id,
+ provider_key=route.provider_key,
+ attempt_count=attempt_count,
+ failure_code=attempt.failure_code,
+ )
+ )
+ if not attempt.retryable:
+ break
+ if local_attempt < route.max_attempts - 1 and attempt_count < grant.max_attempts:
+ await self._sleep(route.retry_backoff_ms / 1000)
+
+ if attempt_count >= grant.max_attempts:
+ break
+
+ return DeliveryOutcome(
+ dispatch_id=request.dispatch_id,
+ workflow_id=request.workflow_id,
+ channel=request.channel,
+ connector_id=last_connector,
+ provider_key=last_provider,
+ status=DeliveryStatus.EXHAUSTED,
+ attempt_count=attempt_count,
+ failover_used=failover_used,
+ audit_ref=last_audit_ref,
+ failure_code=last_failure,
+ )
+
+ async def _invoke(
+ self,
+ invoker: CustodiedProviderInvoker,
+ route: ConnectorRoute,
+ request: DispatchRequest,
+ ) -> ProviderAttemptOutcome:
+ try:
+ return await asyncio.wait_for(
+ invoker.invoke(route, request),
+ timeout=route.timeout_ms / 1000,
+ )
+ except AuthorizationDenied:
+ raise
+ except TimeoutError:
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus.FAILED,
+ audit_ref=f"timeout:{request.dispatch_id}:{route.connector_id}",
+ failure_code="provider_timeout",
+ retryable=True,
+ failover_allowed=True,
+ counts_toward_circuit=True,
+ )
+ except Exception: # noqa: BLE001 - contain provider/backend failures without leaking material.
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus.FAILED,
+ audit_ref=f"error:{request.dispatch_id}:{route.connector_id}",
+ failure_code="provider_error",
+ retryable=True,
+ failover_allowed=True,
+ counts_toward_circuit=True,
+ )
+
+ def _circuit_is_open(self, route: ConnectorRoute) -> bool:
+ if route.circuit_breaker_threshold == 0:
+ return False
+ state = self._circuits.get(route.connector_id)
+ if state is None or state.opened_at is None:
+ return False
+ if self._monotonic() - state.opened_at >= route.circuit_reset_seconds:
+ state.opened_at = None
+ return False
+ return True
+
+ def _record_success(self, route: ConnectorRoute) -> None:
+ self._circuits[route.connector_id] = _CircuitState()
+
+ def _record_failure(self, route: ConnectorRoute) -> None:
+ state = self._circuits.setdefault(route.connector_id, _CircuitState())
+ state.failures += 1
+ if (
+ route.circuit_breaker_threshold > 0
+ and state.failures >= route.circuit_breaker_threshold
+ ):
+ state.opened_at = self._monotonic()
+
+ async def _emit(self, signal: DispatchSignal) -> None:
+ try:
+ await self._signal_sink.emit(signal)
+ except Exception as exc:
+ raise MonitoringUnavailable("sanitized dispatch signal persistence failed") from exc
+
+ async def _emit_terminal(self, outcome: DeliveryOutcome) -> None:
+ kind = (
+ DispatchSignalKind.DELIVERY_EXHAUSTED
+ if outcome.status is DeliveryStatus.EXHAUSTED
+ else DispatchSignalKind.ATTEMPT_SUCCEEDED
+ )
+ await self._emit(
+ DispatchSignal(
+ kind=kind,
+ dispatch_id=outcome.dispatch_id,
+ workflow_id=outcome.workflow_id,
+ channel=outcome.channel,
+ connector_id=outcome.connector_id,
+ provider_key=outcome.provider_key,
+ attempt_count=outcome.attempt_count,
+ failure_code=outcome.failure_code,
+ )
+ )
diff --git a/src/baton/delegated_runtime.py b/src/baton/delegated_runtime.py
new file mode 100644
index 0000000..98fb5d6
--- /dev/null
+++ b/src/baton/delegated_runtime.py
@@ -0,0 +1,1177 @@
+"""Durable composition for delegated provider connector execution.
+
+The runtime persists only opaque identifiers, sanitized outcomes, and
+hash-chained operational evidence. Provider credential material remains behind
+the single-purpose provider operation factory.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import hashlib
+import json
+import os
+import sqlite3
+import uuid
+from collections.abc import Callable, Mapping, Sequence
+from contextlib import closing
+from dataclasses import asdict, dataclass, is_dataclass
+from datetime import UTC, datetime, timedelta
+from enum import Enum
+from pathlib import Path
+from typing import Any, Protocol
+
+from baton.credential_custody import (
+ AuthorizationConsumptionLedger,
+ AuthorizedCredentialUse,
+ ConsumptionReservation,
+ ConsumptionReservationRequired,
+ CredentialCustodyAuthorizer,
+ CredentialCustodyInvokerFactory,
+ CredentialUseRequest,
+ CustodiedReferenceResolver,
+ CustodyAuditEvent,
+ CustodyAuditSink,
+ CustodyAuthorizationDenied,
+ CustodyFailureNotifier,
+ ProviderCredentialHandle,
+ SanitizedCustodyOutcome,
+ VerifiedDelegatedAuthorization,
+ VerifiedWorkloadAuthorization,
+)
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ ConnectorRoute,
+ DelegatedConnectorExecutor,
+ DeliveryOutcome,
+ DeliveryStatus,
+ DispatchBinding,
+ DispatchBindingConflict,
+ DispatchClaim,
+ DispatchJournal,
+ DispatchRequest,
+ DispatchSignal,
+ DispatchSignalSink,
+ DispatchStateUnavailable,
+ ScopedAuthorizationVerifier,
+)
+
+_ZERO_HASH = "0" * 64
+MAX_DELEGATED_PROVIDER_ATTEMPTS = 3
+MAX_DELEGATED_PROVIDER_LIFETIME_SECONDS = 15 * 60
+
+
+def _utc_now() -> datetime:
+ return datetime.now(UTC)
+
+
+def _iso(value: datetime) -> str:
+ if value.tzinfo is None:
+ raise ValueError("timestamp must be timezone-aware")
+ return value.astimezone(UTC).isoformat()
+
+
+def _parse_time(value: str) -> datetime:
+ parsed = datetime.fromisoformat(value)
+ if parsed.tzinfo is None:
+ raise ValueError("persisted timestamp must be timezone-aware")
+ return parsed
+
+
+def _primitive(value: Any) -> Any:
+ if isinstance(value, Enum):
+ return value.value
+ if isinstance(value, datetime):
+ return _iso(value)
+ if is_dataclass(value):
+ return _primitive(asdict(value))
+ if isinstance(value, Mapping):
+ return {str(key): _primitive(item) for key, item in value.items()}
+ if isinstance(value, (tuple, list)):
+ return [_primitive(item) for item in value]
+ if isinstance(value, (set, frozenset)):
+ return sorted(_primitive(item) for item in value)
+ return value
+
+
+def _canonical_json(value: Any) -> str:
+ return json.dumps(_primitive(value), sort_keys=True, separators=(",", ":"))
+
+
+def _delivery_outcome_payload(outcome: DeliveryOutcome) -> dict[str, Any]:
+ return _primitive(outcome)
+
+
+def _delivery_outcome_from_payload(payload: str) -> DeliveryOutcome:
+ data = json.loads(payload)
+ return DeliveryOutcome(
+ dispatch_id=data["dispatch_id"],
+ workflow_id=data["workflow_id"],
+ channel=Channel(data["channel"]),
+ connector_id=data["connector_id"],
+ provider_key=data["provider_key"],
+ status=DeliveryStatus(data["status"]),
+ attempt_count=data["attempt_count"],
+ failover_used=data["failover_used"],
+ audit_ref=data["audit_ref"],
+ failure_code=data["failure_code"],
+ )
+
+
+@dataclass(frozen=True)
+class DelegatedAuthorizationContext:
+ """Exact runtime and trust-policy context supplied to a trusted verifier."""
+
+ audience: str
+ workload_id: str
+ purpose: str
+ available_connector_ids: frozenset[str]
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+ provider_attempt_ceiling: int
+ authorization_lifetime_ceiling_seconds: int
+
+ def __post_init__(self) -> None:
+ for name in (
+ "audience",
+ "workload_id",
+ "purpose",
+ "issuer_policy_ref",
+ "rotation_policy_ref",
+ ):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if not self.available_connector_ids or not all(self.available_connector_ids):
+ raise ValueError("available_connector_ids are required")
+ if (
+ type(self.provider_attempt_ceiling) is not int
+ or not 1 <= self.provider_attempt_ceiling <= MAX_DELEGATED_PROVIDER_ATTEMPTS
+ ):
+ raise ValueError("provider_attempt_ceiling is outside the hard delegated-provider limit")
+ if not (
+ type(self.authorization_lifetime_ceiling_seconds) is int
+ and 1
+ <= self.authorization_lifetime_ceiling_seconds
+ <= MAX_DELEGATED_PROVIDER_LIFETIME_SECONDS
+ ):
+ raise ValueError(
+ "authorization_lifetime_ceiling_seconds is outside the hard delegated-provider limit"
+ )
+
+
+class DelegatedAuthorizationVerifier(Protocol):
+ """Verify one delegated-provider authorization against exact runtime policy."""
+
+ async def verify(
+ self,
+ capability: CapabilityReference,
+ request: DispatchRequest,
+ context: DelegatedAuthorizationContext,
+ ) -> VerifiedDelegatedAuthorization:
+ ...
+
+
+@dataclass(frozen=True)
+class _ContextBoundDelegatedAuthorizationVerifier:
+ verifier: DelegatedAuthorizationVerifier
+ audience: str
+ workload_id: str
+ purpose: str
+ available_connector_ids_by_channel: Mapping[Channel, frozenset[str]]
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+ provider_attempt_ceiling: int
+ authorization_lifetime_ceiling_seconds: int
+ clock: Callable[[], datetime]
+
+ async def verify(
+ self,
+ capability: CapabilityReference,
+ request: DispatchRequest,
+ ) -> VerifiedDelegatedAuthorization:
+ available_connector_ids = self.available_connector_ids_by_channel.get(request.channel)
+ if not available_connector_ids:
+ raise AuthorizationDenied("no available connector policy exists for dispatch channel")
+ context = DelegatedAuthorizationContext(
+ audience=self.audience,
+ workload_id=self.workload_id,
+ purpose=self.purpose,
+ available_connector_ids=available_connector_ids,
+ issuer_policy_ref=self.issuer_policy_ref,
+ rotation_policy_ref=self.rotation_policy_ref,
+ provider_attempt_ceiling=self.provider_attempt_ceiling,
+ authorization_lifetime_ceiling_seconds=self.authorization_lifetime_ceiling_seconds,
+ )
+ outcome = await self.verifier.verify(capability, request, context)
+ if not isinstance(outcome, VerifiedDelegatedAuthorization):
+ raise AuthorizationDenied(
+ "delegated verifier did not return the required shared authorization outcome"
+ )
+ if outcome.audience != context.audience:
+ raise AuthorizationDenied("authorization audience is outside runtime scope")
+ if outcome.issuer_policy_ref != context.issuer_policy_ref:
+ raise AuthorizationDenied("authorization issuer policy is outside runtime scope")
+ if outcome.rotation_policy_ref != context.rotation_policy_ref:
+ raise AuthorizationDenied("authorization rotation policy is outside runtime scope")
+ if outcome.principal != context.workload_id:
+ raise AuthorizationDenied("authorization principal is outside runtime scope")
+ if outcome.allowed_purposes != frozenset({context.purpose}):
+ raise AuthorizationDenied("authorization purpose is outside runtime scope")
+ if not outcome.allowed_connectors.issubset(context.available_connector_ids):
+ raise AuthorizationDenied("authorization connector scope exceeds runtime policy")
+ if outcome.max_attempts > context.provider_attempt_ceiling:
+ raise AuthorizationDenied("authorization attempt budget exceeds runtime policy")
+ if outcome.not_after - outcome.not_before > timedelta(
+ seconds=context.authorization_lifetime_ceiling_seconds
+ ):
+ raise AuthorizationDenied("authorization validity window exceeds runtime policy")
+ current_time = self.clock()
+ if current_time < outcome.not_before or current_time >= outcome.not_after:
+ raise AuthorizationDenied("authorization is outside its validity window")
+ return outcome
+
+
+@dataclass(frozen=True)
+class ConfiguredVerifierBundle:
+ """One verifier plus the exact externally managed trust-policy context."""
+
+ verifier: DelegatedAuthorizationVerifier
+ audience: str
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+ provider_attempt_ceiling: int
+ authorization_lifetime_ceiling_seconds: int
+
+ def __post_init__(self) -> None:
+ for name in ("audience", "issuer_policy_ref", "rotation_policy_ref"):
+ if not getattr(self, name):
+ raise ValueError(f"{name} is required")
+ if (
+ type(self.provider_attempt_ceiling) is not int
+ or not 1 <= self.provider_attempt_ceiling <= MAX_DELEGATED_PROVIDER_ATTEMPTS
+ ):
+ raise ValueError("provider_attempt_ceiling is outside the hard delegated-provider limit")
+ if not (
+ type(self.authorization_lifetime_ceiling_seconds) is int
+ and 1
+ <= self.authorization_lifetime_ceiling_seconds
+ <= MAX_DELEGATED_PROVIDER_LIFETIME_SECONDS
+ ):
+ raise ValueError(
+ "authorization_lifetime_ceiling_seconds is outside the hard delegated-provider limit"
+ )
+
+ def bind(
+ self,
+ *,
+ workload_id: str,
+ purpose: str,
+ available_connector_ids_by_channel: Mapping[Channel, frozenset[str]],
+ clock: Callable[[], datetime] | None = None,
+ ) -> ScopedAuthorizationVerifier:
+ """Bind trust policy and runtime scope before any dispatch admission."""
+
+ return _ContextBoundDelegatedAuthorizationVerifier(
+ self.verifier,
+ self.audience,
+ workload_id,
+ purpose,
+ dict(available_connector_ids_by_channel),
+ self.issuer_policy_ref,
+ self.rotation_policy_ref,
+ self.provider_attempt_ceiling,
+ self.authorization_lifetime_ceiling_seconds,
+ clock or _utc_now,
+ )
+
+
+class SinglePurposeProviderOperation(Protocol):
+ """Custody-internal operation prepared for one exact authorized use."""
+
+ async def invoke(self) -> SanitizedCustodyOutcome:
+ ...
+
+
+class SinglePurposeProviderOperationFactory(Protocol):
+ """Prepare one operation while keeping any resolved material internal."""
+
+ async def prepare(
+ self,
+ handle: ProviderCredentialHandle,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> SinglePurposeProviderOperation:
+ ...
+
+
+class SinglePurposeCustodiedResolver(CustodiedReferenceResolver):
+ """Concrete resolver that exposes only a one-operation invocation object."""
+
+ def __init__(self, operation_factory: SinglePurposeProviderOperationFactory):
+ self._operation_factory = operation_factory
+
+ async def invoke(
+ self,
+ handle: ProviderCredentialHandle,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> SanitizedCustodyOutcome:
+ operation = await self._operation_factory.prepare(handle, authorized_use)
+ return await operation.invoke()
+
+
+@dataclass(frozen=True)
+class HashChainVerification:
+ ok: bool
+ event_count: int
+ anchor: str
+ broken_sequence: int | None = None
+
+
+class SqliteDelegatedRuntimeState:
+ """Single-node durable reference state for dispatch, custody, audit, and alerts."""
+
+ def __init__(
+ self,
+ path: str | Path,
+ *,
+ claim_lease_seconds: int = 60,
+ clock: Callable[[], datetime] | None = None,
+ ):
+ self.path = Path(path)
+ if str(self.path) == ":memory:":
+ raise ValueError("durable delegated runtime state cannot use :memory:")
+ if claim_lease_seconds < 1:
+ raise ValueError("claim_lease_seconds must be positive")
+ self._claim_lease_seconds = claim_lease_seconds
+ self._clock = clock or _utc_now
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+ self._initialize()
+ self.journal = SqliteDispatchJournal(self)
+ self.ledger = SqliteAuthorizationLedger(self)
+ self.sink = SqliteOperationalSink(self)
+
+ def _connect(self) -> sqlite3.Connection:
+ connection = sqlite3.connect(self.path, timeout=10, isolation_level=None)
+ connection.row_factory = sqlite3.Row
+ connection.execute("PRAGMA foreign_keys = ON")
+ connection.execute("PRAGMA journal_mode = WAL")
+ connection.execute("PRAGMA synchronous = FULL")
+ return connection
+
+ def _initialize(self) -> None:
+ with closing(self._connect()) as connection:
+ connection.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS dispatch_journal (
+ idempotency_key TEXT PRIMARY KEY,
+ request_fingerprint TEXT NOT NULL,
+ state TEXT NOT NULL CHECK (state IN ('running', 'completed', 'aborted')),
+ claim_id TEXT,
+ lease_expires_at TEXT,
+ outcome_json TEXT,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS authorization_reservations (
+ authorization_id TEXT PRIMARY KEY,
+ reservation_id TEXT NOT NULL UNIQUE,
+ request_fingerprint TEXT NOT NULL,
+ idempotency_key TEXT NOT NULL,
+ dispatch_id TEXT NOT NULL,
+ workload_id TEXT NOT NULL,
+ max_provider_attempts INTEGER NOT NULL,
+ attempt_count INTEGER NOT NULL DEFAULT 0,
+ outcome_json TEXT,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS operational_events (
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
+ stream TEXT NOT NULL,
+ event_key TEXT NOT NULL UNIQUE,
+ payload_json TEXT NOT NULL,
+ previous_hash TEXT NOT NULL,
+ event_hash TEXT NOT NULL UNIQUE,
+ created_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS failure_notifications (
+ notification_key TEXT PRIMARY KEY,
+ payload_json TEXT NOT NULL,
+ status TEXT NOT NULL CHECK (status IN ('pending', 'acknowledged')),
+ created_at TEXT NOT NULL,
+ acknowledged_at TEXT
+ );
+ """
+ )
+ try:
+ os.chmod(self.path, 0o600)
+ except OSError:
+ pass
+
+ async def verify_event_chain(self) -> HashChainVerification:
+ return await asyncio.to_thread(self._verify_event_chain)
+
+ def _verify_event_chain(self) -> HashChainVerification:
+ previous_hash = _ZERO_HASH
+ event_count = 0
+ with closing(self._connect()) as connection:
+ rows = connection.execute(
+ """
+ SELECT sequence, stream, event_key, payload_json, previous_hash, event_hash
+ FROM operational_events
+ ORDER BY sequence
+ """
+ ).fetchall()
+ for row in rows:
+ event_count += 1
+ expected = self._event_hash(
+ row["stream"],
+ row["event_key"],
+ row["payload_json"],
+ previous_hash,
+ )
+ if row["previous_hash"] != previous_hash or row["event_hash"] != expected:
+ return HashChainVerification(
+ ok=False,
+ event_count=event_count,
+ anchor=previous_hash,
+ broken_sequence=row["sequence"],
+ )
+ previous_hash = row["event_hash"]
+ return HashChainVerification(ok=True, event_count=event_count, anchor=previous_hash)
+
+ async def pending_failure_notifications(self) -> list[dict[str, Any]]:
+ return await asyncio.to_thread(self._pending_failure_notifications)
+
+ def _pending_failure_notifications(self) -> list[dict[str, Any]]:
+ with closing(self._connect()) as connection:
+ rows = connection.execute(
+ """
+ SELECT notification_key, payload_json, created_at
+ FROM failure_notifications
+ WHERE status = 'pending'
+ ORDER BY created_at, notification_key
+ """
+ ).fetchall()
+ return [
+ {
+ "notification_key": row["notification_key"],
+ "payload": json.loads(row["payload_json"]),
+ "created_at": row["created_at"],
+ }
+ for row in rows
+ ]
+
+ async def acknowledge_failure_notification(self, notification_key: str) -> bool:
+ if not notification_key:
+ raise ValueError("notification_key is required")
+ return await asyncio.to_thread(
+ self._acknowledge_failure_notification,
+ notification_key,
+ )
+
+ def _acknowledge_failure_notification(self, notification_key: str) -> bool:
+ with closing(self._connect()) as connection:
+ result = connection.execute(
+ """
+ UPDATE failure_notifications
+ SET status = 'acknowledged', acknowledged_at = ?
+ WHERE notification_key = ? AND status = 'pending'
+ """,
+ (_iso(self._clock()), notification_key),
+ )
+ return result.rowcount == 1
+
+ @staticmethod
+ def _event_hash(stream: str, event_key: str, payload_json: str, previous_hash: str) -> str:
+ canonical = _canonical_json(
+ {
+ "stream": stream,
+ "event_key": event_key,
+ "payload_json": payload_json,
+ "previous_hash": previous_hash,
+ }
+ )
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+
+ def _append_event(
+ self,
+ connection: sqlite3.Connection,
+ *,
+ stream: str,
+ event_key: str,
+ payload: Any,
+ ) -> None:
+ if connection.execute(
+ "SELECT 1 FROM operational_events WHERE event_key = ?",
+ (event_key,),
+ ).fetchone():
+ return
+ row = connection.execute(
+ "SELECT event_hash FROM operational_events ORDER BY sequence DESC LIMIT 1"
+ ).fetchone()
+ previous_hash = row["event_hash"] if row is not None else _ZERO_HASH
+ payload_json = _canonical_json(payload)
+ event_hash = self._event_hash(stream, event_key, payload_json, previous_hash)
+ connection.execute(
+ """
+ INSERT INTO operational_events (
+ stream, event_key, payload_json, previous_hash, event_hash, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ stream,
+ event_key,
+ payload_json,
+ previous_hash,
+ event_hash,
+ _iso(self._clock()),
+ ),
+ )
+
+
+class SqliteDispatchJournal(DispatchJournal):
+ def __init__(self, state: SqliteDelegatedRuntimeState):
+ self._state = state
+
+ async def completed(self, binding: DispatchBinding) -> DeliveryOutcome | None:
+ return await asyncio.to_thread(self._completed, binding)
+
+ def _completed(self, binding: DispatchBinding) -> DeliveryOutcome | None:
+ with closing(self._state._connect()) as connection:
+ row = connection.execute(
+ "SELECT request_fingerprint, state, outcome_json FROM dispatch_journal "
+ "WHERE idempotency_key = ?",
+ (binding.idempotency_key,),
+ ).fetchone()
+ if row is None:
+ return None
+ self._assert_fingerprint(row["request_fingerprint"], binding)
+ if row["state"] != "completed":
+ return None
+ if not row["outcome_json"]:
+ raise DispatchStateUnavailable("completed dispatch is missing its outcome")
+ return _delivery_outcome_from_payload(row["outcome_json"])
+
+ async def begin(self, binding: DispatchBinding) -> DispatchClaim | None:
+ return await asyncio.to_thread(self._begin, binding)
+
+ def _begin(self, binding: DispatchBinding) -> DispatchClaim | None:
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ now = self._state._clock()
+ lease_expires_at = now + timedelta(seconds=self._state._claim_lease_seconds)
+ claim = DispatchClaim(
+ claim_id=str(uuid.uuid4()),
+ binding=binding,
+ lease_expires_at=lease_expires_at,
+ )
+ row = connection.execute(
+ """
+ SELECT request_fingerprint, state, lease_expires_at
+ FROM dispatch_journal
+ WHERE idempotency_key = ?
+ """,
+ (binding.idempotency_key,),
+ ).fetchone()
+ if row is None:
+ connection.execute(
+ """
+ INSERT INTO dispatch_journal (
+ idempotency_key, request_fingerprint, state, claim_id,
+ lease_expires_at, updated_at
+ ) VALUES (?, ?, 'running', ?, ?, ?)
+ """,
+ (
+ binding.idempotency_key,
+ binding.request_fingerprint,
+ claim.claim_id,
+ _iso(claim.lease_expires_at),
+ _iso(now),
+ ),
+ )
+ connection.commit()
+ return claim
+
+ self._assert_fingerprint(row["request_fingerprint"], binding)
+ if row["state"] == "completed":
+ connection.commit()
+ return None
+ if (
+ row["state"] == "running"
+ and row["lease_expires_at"]
+ and _parse_time(row["lease_expires_at"]) > now
+ ):
+ connection.commit()
+ return None
+ connection.execute(
+ """
+ UPDATE dispatch_journal
+ SET state = 'running', claim_id = ?, lease_expires_at = ?,
+ outcome_json = NULL, updated_at = ?
+ WHERE idempotency_key = ?
+ """,
+ (
+ claim.claim_id,
+ _iso(claim.lease_expires_at),
+ _iso(now),
+ binding.idempotency_key,
+ ),
+ )
+ connection.commit()
+ return claim
+ except Exception:
+ connection.rollback()
+ raise
+
+ async def complete(self, claim: DispatchClaim, outcome: DeliveryOutcome) -> None:
+ await asyncio.to_thread(self._complete, claim, outcome)
+
+ async def renew(self, claim: DispatchClaim) -> None:
+ await asyncio.to_thread(self._renew, claim)
+
+ def _renew(self, claim: DispatchClaim) -> None:
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ now = self._state._clock()
+ lease_expires_at = now + timedelta(seconds=self._state._claim_lease_seconds)
+ result = connection.execute(
+ """
+ UPDATE dispatch_journal
+ SET lease_expires_at = ?, updated_at = ?
+ WHERE idempotency_key = ? AND request_fingerprint = ?
+ AND state = 'running' AND claim_id = ? AND lease_expires_at > ?
+ """,
+ (
+ _iso(lease_expires_at),
+ _iso(now),
+ claim.binding.idempotency_key,
+ claim.binding.request_fingerprint,
+ claim.claim_id,
+ _iso(now),
+ ),
+ )
+ if result.rowcount != 1:
+ raise DispatchStateUnavailable("dispatch claim is no longer active")
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ def _complete(self, claim: DispatchClaim, outcome: DeliveryOutcome) -> None:
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ now = self._state._clock()
+ row = connection.execute(
+ """
+ SELECT request_fingerprint, state, claim_id, lease_expires_at
+ FROM dispatch_journal
+ WHERE idempotency_key = ?
+ """,
+ (claim.binding.idempotency_key,),
+ ).fetchone()
+ if row is None:
+ raise DispatchStateUnavailable("dispatch claim is missing")
+ self._assert_fingerprint(row["request_fingerprint"], claim.binding)
+ if (
+ row["state"] != "running"
+ or row["claim_id"] != claim.claim_id
+ or not row["lease_expires_at"]
+ or _parse_time(row["lease_expires_at"]) <= now
+ ):
+ raise DispatchStateUnavailable("dispatch claim is no longer active")
+ connection.execute(
+ """
+ UPDATE dispatch_journal
+ SET state = 'completed', claim_id = NULL, lease_expires_at = NULL,
+ outcome_json = ?, updated_at = ?
+ WHERE idempotency_key = ?
+ """,
+ (
+ _canonical_json(_delivery_outcome_payload(outcome)),
+ _iso(now),
+ claim.binding.idempotency_key,
+ ),
+ )
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ async def abort(self, claim: DispatchClaim) -> None:
+ await asyncio.to_thread(self._abort, claim)
+
+ def _abort(self, claim: DispatchClaim) -> None:
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ row = connection.execute(
+ """
+ SELECT request_fingerprint, state, claim_id
+ FROM dispatch_journal
+ WHERE idempotency_key = ?
+ """,
+ (claim.binding.idempotency_key,),
+ ).fetchone()
+ if row is None:
+ connection.commit()
+ return
+ self._assert_fingerprint(row["request_fingerprint"], claim.binding)
+ if row["state"] == "running" and row["claim_id"] == claim.claim_id:
+ connection.execute(
+ """
+ UPDATE dispatch_journal
+ SET state = 'aborted', claim_id = NULL, lease_expires_at = NULL,
+ updated_at = ?
+ WHERE idempotency_key = ?
+ """,
+ (_iso(self._state._clock()), claim.binding.idempotency_key),
+ )
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ @staticmethod
+ def _assert_fingerprint(persisted: str, binding: DispatchBinding) -> None:
+ if persisted != binding.request_fingerprint:
+ raise DispatchBindingConflict("idempotency key is bound to another request")
+
+
+class SqliteAuthorizationLedger(AuthorizationConsumptionLedger):
+ def __init__(self, state: SqliteDelegatedRuntimeState):
+ self._state = state
+
+ async def reserve_once(
+ self,
+ authorization: VerifiedWorkloadAuthorization,
+ request: CredentialUseRequest,
+ ) -> ConsumptionReservation:
+ return await asyncio.to_thread(self._reserve_once, authorization, request)
+
+ def _reserve_once(
+ self,
+ authorization: VerifiedWorkloadAuthorization,
+ request: CredentialUseRequest,
+ ) -> ConsumptionReservation:
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ dispatch = connection.execute(
+ """
+ SELECT request_fingerprint, state, claim_id, lease_expires_at
+ FROM dispatch_journal
+ WHERE idempotency_key = ?
+ """,
+ (request.idempotency_key,),
+ ).fetchone()
+ if (
+ dispatch is None
+ or dispatch["request_fingerprint"] != request.request_fingerprint
+ or dispatch["state"] != "running"
+ or dispatch["claim_id"] != request.dispatch_claim_id
+ or not dispatch["lease_expires_at"]
+ or _parse_time(dispatch["lease_expires_at"]) <= self._state._clock()
+ ):
+ raise ConsumptionReservationRequired(
+ "active exact dispatch journal claim is required"
+ )
+
+ row = connection.execute(
+ """
+ SELECT reservation_id, request_fingerprint, idempotency_key,
+ dispatch_id, workload_id, max_provider_attempts
+ FROM authorization_reservations
+ WHERE authorization_id = ?
+ """,
+ (authorization.authorization_id,),
+ ).fetchone()
+ if row is not None:
+ if (
+ row["request_fingerprint"] != request.request_fingerprint
+ or row["idempotency_key"] != request.idempotency_key
+ or row["dispatch_id"] != request.dispatch_id
+ or row["workload_id"] != request.workload_id
+ or row["max_provider_attempts"] != authorization.max_provider_attempts
+ ):
+ raise CustodyAuthorizationDenied(
+ "authorization is already reserved for another dispatch"
+ )
+ connection.commit()
+ return ConsumptionReservation(
+ reservation_id=row["reservation_id"],
+ authorization_id=authorization.authorization_id,
+ request_fingerprint=request.request_fingerprint,
+ idempotency_key=request.idempotency_key,
+ )
+
+ reservation = ConsumptionReservation(
+ reservation_id=str(uuid.uuid4()),
+ authorization_id=authorization.authorization_id,
+ request_fingerprint=request.request_fingerprint,
+ idempotency_key=request.idempotency_key,
+ )
+ connection.execute(
+ """
+ INSERT INTO authorization_reservations (
+ authorization_id, reservation_id, request_fingerprint,
+ idempotency_key, dispatch_id, workload_id,
+ max_provider_attempts, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ authorization.authorization_id,
+ reservation.reservation_id,
+ request.request_fingerprint,
+ request.idempotency_key,
+ request.dispatch_id,
+ request.workload_id,
+ authorization.max_provider_attempts,
+ _iso(self._state._clock()),
+ ),
+ )
+ connection.commit()
+ return reservation
+ except Exception:
+ connection.rollback()
+ raise
+
+ async def complete(
+ self,
+ reservation: ConsumptionReservation,
+ outcome: SanitizedCustodyOutcome,
+ ) -> None:
+ await asyncio.to_thread(self._complete, reservation, outcome)
+
+ def _complete(
+ self,
+ reservation: ConsumptionReservation,
+ outcome: SanitizedCustodyOutcome,
+ ) -> None:
+ with closing(self._state._connect()) as connection:
+ result = connection.execute(
+ """
+ UPDATE authorization_reservations
+ SET outcome_json = ?, updated_at = ?
+ WHERE authorization_id = ? AND reservation_id = ?
+ AND request_fingerprint = ? AND idempotency_key = ?
+ AND dispatch_id = ?
+ """,
+ (
+ _canonical_json(outcome),
+ _iso(self._state._clock()),
+ reservation.authorization_id,
+ reservation.reservation_id,
+ reservation.request_fingerprint,
+ reservation.idempotency_key,
+ outcome.dispatch_id,
+ ),
+ )
+ if result.rowcount != 1:
+ raise ConsumptionReservationRequired("authorization reservation is missing")
+
+ async def reserve_attempt(
+ self,
+ reservation: ConsumptionReservation,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> int:
+ return await asyncio.to_thread(self._reserve_attempt, reservation, authorized_use)
+
+ def _reserve_attempt(
+ self,
+ reservation: ConsumptionReservation,
+ authorized_use: AuthorizedCredentialUse,
+ ) -> int:
+ if (
+ authorized_use.authorization_id != reservation.authorization_id
+ or authorized_use.reservation_id != reservation.reservation_id
+ or authorized_use.request_fingerprint != reservation.request_fingerprint
+ or authorized_use.idempotency_key != reservation.idempotency_key
+ ):
+ raise ConsumptionReservationRequired(
+ "authorized use does not bind the authorization reservation"
+ )
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ dispatch = connection.execute(
+ """
+ SELECT request_fingerprint, state, claim_id, lease_expires_at
+ FROM dispatch_journal
+ WHERE idempotency_key = ?
+ """,
+ (authorized_use.idempotency_key,),
+ ).fetchone()
+ if (
+ dispatch is None
+ or dispatch["request_fingerprint"] != authorized_use.request_fingerprint
+ or dispatch["state"] != "running"
+ or dispatch["claim_id"] != authorized_use.dispatch_claim_id
+ or not dispatch["lease_expires_at"]
+ or _parse_time(dispatch["lease_expires_at"]) <= self._state._clock()
+ ):
+ raise ConsumptionReservationRequired(
+ "active exact dispatch journal claim is required for provider attempt"
+ )
+ row = connection.execute(
+ """
+ SELECT attempt_count, max_provider_attempts
+ FROM authorization_reservations
+ WHERE authorization_id = ? AND reservation_id = ?
+ AND request_fingerprint = ? AND idempotency_key = ?
+ AND dispatch_id = ?
+ """,
+ (
+ reservation.authorization_id,
+ reservation.reservation_id,
+ reservation.request_fingerprint,
+ reservation.idempotency_key,
+ authorized_use.dispatch_id,
+ ),
+ ).fetchone()
+ if row is None:
+ raise ConsumptionReservationRequired("authorization reservation is missing")
+ if row["attempt_count"] >= row["max_provider_attempts"]:
+ raise CustodyAuthorizationDenied("provider attempt budget is exhausted")
+ attempt_count = row["attempt_count"] + 1
+ connection.execute(
+ """
+ UPDATE authorization_reservations
+ SET attempt_count = ?, updated_at = ?
+ WHERE authorization_id = ?
+ """,
+ (
+ attempt_count,
+ _iso(self._state._clock()),
+ reservation.authorization_id,
+ ),
+ )
+ connection.commit()
+ return attempt_count
+ except Exception:
+ connection.rollback()
+ raise
+
+
+class SqliteOperationalSink(DispatchSignalSink, CustodyAuditSink, CustodyFailureNotifier):
+ def __init__(self, state: SqliteDelegatedRuntimeState):
+ self._state = state
+
+ async def emit(self, event: DispatchSignal | CustodyAuditEvent) -> None:
+ await asyncio.to_thread(self._emit, event)
+
+ def _emit(self, event: DispatchSignal | CustodyAuditEvent) -> None:
+ stream, event_key = self._identity(event)
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ self._state._append_event(
+ connection,
+ stream=stream,
+ event_key=event_key,
+ payload=event,
+ )
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ async def notify(self, event: CustodyAuditEvent) -> None:
+ await asyncio.to_thread(self._notify, event)
+
+ def _notify(self, event: CustodyAuditEvent) -> None:
+ _, base_key = self._identity(event)
+ notification_key = f"failure:{base_key}"
+ payload_json = _canonical_json(event)
+ with closing(self._state._connect()) as connection:
+ connection.execute("BEGIN IMMEDIATE")
+ try:
+ connection.execute(
+ """
+ INSERT OR IGNORE INTO failure_notifications (
+ notification_key, payload_json, status, created_at
+ ) VALUES (?, ?, 'pending', ?)
+ """,
+ (notification_key, payload_json, _iso(self._state._clock())),
+ )
+ self._state._append_event(
+ connection,
+ stream="failure_notification",
+ event_key=notification_key,
+ payload=event,
+ )
+ connection.commit()
+ except Exception:
+ connection.rollback()
+ raise
+
+ @staticmethod
+ def _identity(event: DispatchSignal | CustodyAuditEvent) -> tuple[str, str]:
+ if isinstance(event, DispatchSignal):
+ return (
+ "dispatch_signal",
+ (
+ f"dispatch:{event.kind.value}:{event.dispatch_id}:"
+ f"{event.connector_id}:{event.attempt_count}"
+ ),
+ )
+ if isinstance(event, CustodyAuditEvent):
+ return (
+ "custody_audit",
+ (
+ f"custody:{event.kind.value}:{event.dispatch_id}:"
+ f"{event.connector_id}:{event.authorization_id}:{event.failure_code}"
+ ),
+ )
+ raise TypeError("unsupported operational event")
+
+
+@dataclass(frozen=True)
+class DelegatedRuntimeComponents:
+ """Explicit durable components required to compose a delegated runtime."""
+
+ journal: DispatchJournal
+ ledger: AuthorizationConsumptionLedger
+ signal_sink: DispatchSignalSink
+ audit_sink: CustodyAuditSink
+ failure_notifier: CustodyFailureNotifier
+ claim_lease_seconds: int
+
+ def __post_init__(self) -> None:
+ if any(
+ component is None
+ for component in (
+ self.journal,
+ self.ledger,
+ self.signal_sink,
+ self.audit_sink,
+ self.failure_notifier,
+ )
+ ):
+ raise ValueError("all delegated runtime components are required")
+ if self.claim_lease_seconds < 1:
+ raise ValueError("claim_lease_seconds must be positive")
+
+
+@dataclass(frozen=True)
+class DelegatedConnectorRuntime:
+ """Composed delegated connector runtime with explicit durable controls."""
+
+ executor: DelegatedConnectorExecutor
+ verifier_policy: ConfiguredVerifierBundle
+ reference_state: SqliteDelegatedRuntimeState | None = None
+
+ @classmethod
+ def compose(
+ cls,
+ *,
+ routes: Sequence[ConnectorRoute],
+ handles: Mapping[str, ProviderCredentialHandle],
+ verifiers: ConfiguredVerifierBundle,
+ operation_factory: SinglePurposeProviderOperationFactory,
+ components: DelegatedRuntimeComponents,
+ workload_id: str,
+ purpose: str,
+ clock: Callable[[], datetime] | None = None,
+ ) -> DelegatedConnectorRuntime:
+ if not workload_id or not purpose:
+ raise ValueError("workload_id and purpose are required")
+ enabled_routes = [route for route in routes if route.enabled]
+ if enabled_routes and components.claim_lease_seconds * 1000 <= max(
+ route.timeout_ms for route in enabled_routes
+ ):
+ raise ValueError("claim lease must exceed every provider attempt timeout")
+ for route in enabled_routes:
+ handle = handles.get(route.connector_id)
+ if (
+ handle is None
+ or handle.connector_id != route.connector_id
+ or handle.handle_id != route.credential_handle
+ or handle.provider_key != route.provider_key
+ or handle.channel.value != route.channel.value
+ ):
+ raise ValueError(f"missing exact custody handle for route {route.connector_id}")
+
+ authorizer = CredentialCustodyAuthorizer.for_verified_outcomes(
+ components.ledger,
+ audit_sink=components.audit_sink,
+ failure_notifier=components.failure_notifier,
+ clock=clock,
+ )
+ invoker_factory = CredentialCustodyInvokerFactory(
+ authorizer,
+ SinglePurposeCustodiedResolver(operation_factory),
+ handles,
+ workload_id=workload_id,
+ purpose=purpose,
+ )
+ executor = DelegatedConnectorExecutor(
+ routes,
+ verifiers.bind(
+ workload_id=workload_id,
+ purpose=purpose,
+ available_connector_ids_by_channel={
+ channel: frozenset(
+ route.connector_id
+ for route in enabled_routes
+ if route.channel is channel
+ )
+ for channel in {route.channel for route in enabled_routes}
+ },
+ clock=clock,
+ ),
+ invoker_factory,
+ components.journal,
+ components.signal_sink,
+ clock=clock,
+ )
+ return cls(executor=executor, verifier_policy=verifiers)
+
+ @classmethod
+ def build_sqlite_reference(
+ cls,
+ *,
+ routes: Sequence[ConnectorRoute],
+ handles: Mapping[str, ProviderCredentialHandle],
+ verifiers: ConfiguredVerifierBundle,
+ operation_factory: SinglePurposeProviderOperationFactory,
+ state_path: str | Path,
+ workload_id: str,
+ purpose: str,
+ claim_lease_seconds: int = 60,
+ clock: Callable[[], datetime] | None = None,
+ ) -> DelegatedConnectorRuntime:
+ """Build the single-node SQLite reference, not a multi-replica backend."""
+
+ state = SqliteDelegatedRuntimeState(
+ state_path,
+ claim_lease_seconds=claim_lease_seconds,
+ clock=clock,
+ )
+ runtime = cls.compose(
+ routes=routes,
+ handles=handles,
+ verifiers=verifiers,
+ operation_factory=operation_factory,
+ components=DelegatedRuntimeComponents(
+ journal=state.journal,
+ ledger=state.ledger,
+ signal_sink=state.sink,
+ audit_sink=state.sink,
+ failure_notifier=state.sink,
+ claim_lease_seconds=claim_lease_seconds,
+ ),
+ workload_id=workload_id,
+ purpose=purpose,
+ clock=clock,
+ )
+ return cls(
+ executor=runtime.executor,
+ verifier_policy=runtime.verifier_policy,
+ reference_state=state,
+ )
diff --git a/src/baton/signet_delegated_authorization.py b/src/baton/signet_delegated_authorization.py
new file mode 100644
index 0000000..060735b
--- /dev/null
+++ b/src/baton/signet_delegated_authorization.py
@@ -0,0 +1,212 @@
+"""Key-free adapter from a trusted Signet verifier outcome into Baton scope."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import dataclass
+from datetime import UTC, datetime, timedelta
+from typing import Protocol
+
+from baton.credential_custody import VerifiedDelegatedAuthorization
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ DispatchRequest,
+)
+from baton.delegated_runtime import DelegatedAuthorizationContext
+
+
+@dataclass(frozen=True)
+class SignetDelegatedProviderVerificationContext:
+ """Exact context sent to a trusted Signet delegated-provider verifier."""
+
+ issuer: str
+ audience: str
+ workload_id: str
+ channel: str
+ available_connector_ids: tuple[str, ...]
+ purpose: str
+ request_fingerprint: str
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+ provider_attempt_ceiling: int
+ authorization_lifetime_ceiling_seconds: int
+
+
+@dataclass(frozen=True)
+class VerifiedSignetDelegatedProviderAuthorization:
+ """Wire-shaped result returned only after trusted Signet verification."""
+
+ authorization_id: str
+ issuer: str
+ audience: str
+ workload_id: str
+ issued_at: str
+ not_before: str
+ not_after: str
+ channel: str
+ allowed_connector_ids: tuple[str, ...]
+ allowed_purposes: tuple[str, ...]
+ request_fingerprint: str
+ max_uses: int
+ max_provider_attempts: int
+ issuer_policy_ref: str
+ rotation_policy_ref: str
+
+
+class TrustedSignetDelegatedProviderClient(Protocol):
+ """Trusted transport that verifies a Signet envelope before returning scope."""
+
+ async def verify(
+ self,
+ reference: str,
+ context: SignetDelegatedProviderVerificationContext,
+ ) -> VerifiedSignetDelegatedProviderAuthorization:
+ ...
+
+
+class SignetDelegatedAuthorizationAdapter:
+ """Map one trusted Signet result into Baton's shared verified outcome."""
+
+ def __init__(
+ self,
+ client: TrustedSignetDelegatedProviderClient,
+ *,
+ issuer: str,
+ clock: Callable[[], datetime] | None = None,
+ ):
+ if not issuer:
+ raise ValueError("issuer is required")
+ self._client = client
+ self._issuer = issuer
+ self._clock = clock or (lambda: datetime.now(UTC))
+
+ async def verify(
+ self,
+ capability: CapabilityReference,
+ request: DispatchRequest,
+ context: DelegatedAuthorizationContext,
+ ) -> VerifiedDelegatedAuthorization:
+ signet_context = SignetDelegatedProviderVerificationContext(
+ issuer=self._issuer,
+ audience=context.audience,
+ workload_id=context.workload_id,
+ channel=request.channel.value,
+ available_connector_ids=tuple(sorted(context.available_connector_ids)),
+ purpose=context.purpose,
+ request_fingerprint=request.request_fingerprint,
+ issuer_policy_ref=context.issuer_policy_ref,
+ rotation_policy_ref=context.rotation_policy_ref,
+ provider_attempt_ceiling=context.provider_attempt_ceiling,
+ authorization_lifetime_ceiling_seconds=context.authorization_lifetime_ceiling_seconds,
+ )
+ outcome = await self._client.verify(capability.reference, signet_context)
+ return self._map_verified_outcome(outcome, signet_context)
+
+ def _map_verified_outcome(
+ self,
+ outcome: VerifiedSignetDelegatedProviderAuthorization,
+ context: SignetDelegatedProviderVerificationContext,
+ ) -> VerifiedDelegatedAuthorization:
+ self._validate_exact_context(outcome, context)
+ issued_at = _parse_time(outcome.issued_at, "issued_at")
+ not_before = _parse_time(outcome.not_before, "not_before")
+ not_after = _parse_time(outcome.not_after, "not_after")
+ current_time = self._clock()
+ if current_time.tzinfo is None:
+ raise AuthorizationDenied("Signet adapter clock must be timezone-aware")
+ if issued_at > current_time:
+ raise AuthorizationDenied("Signet authorization was issued in the future")
+ if not_before < issued_at or not_after <= not_before:
+ raise AuthorizationDenied("Signet authorization has an inconsistent time window")
+ if not_after - issued_at > timedelta(
+ seconds=context.authorization_lifetime_ceiling_seconds
+ ):
+ raise AuthorizationDenied("Signet authorization lifetime exceeds runtime policy")
+ if current_time < not_before or current_time >= not_after:
+ raise AuthorizationDenied("Signet authorization is outside its validity window")
+
+ try:
+ channel = Channel(outcome.channel)
+ return VerifiedDelegatedAuthorization(
+ principal=outcome.workload_id,
+ channel=channel,
+ allowed_connectors=frozenset(outcome.allowed_connector_ids),
+ not_after=not_after,
+ max_attempts=outcome.max_provider_attempts,
+ request_fingerprint=outcome.request_fingerprint,
+ authorization_id=outcome.authorization_id,
+ issuer=outcome.issuer,
+ audience=outcome.audience,
+ issuer_policy_ref=outcome.issuer_policy_ref,
+ rotation_policy_ref=outcome.rotation_policy_ref,
+ allowed_purposes=frozenset(outcome.allowed_purposes),
+ not_before=not_before,
+ max_uses=outcome.max_uses,
+ )
+ except (TypeError, ValueError) as exc:
+ raise AuthorizationDenied("Signet authorization outcome is invalid") from exc
+
+ @staticmethod
+ def _validate_exact_context(
+ outcome: VerifiedSignetDelegatedProviderAuthorization,
+ context: SignetDelegatedProviderVerificationContext,
+ ) -> None:
+ for name in (
+ "authorization_id",
+ "issuer",
+ "audience",
+ "workload_id",
+ "issued_at",
+ "not_before",
+ "not_after",
+ "channel",
+ "request_fingerprint",
+ "issuer_policy_ref",
+ "rotation_policy_ref",
+ ):
+ if not getattr(outcome, name):
+ raise AuthorizationDenied(f"Signet authorization is missing {name}")
+ if outcome.issuer != context.issuer:
+ raise AuthorizationDenied("Signet authorization issuer is outside configured scope")
+ if outcome.audience != context.audience:
+ raise AuthorizationDenied("Signet authorization audience is outside configured scope")
+ if outcome.workload_id != context.workload_id:
+ raise AuthorizationDenied("Signet authorization workload is outside configured scope")
+ if outcome.channel != context.channel:
+ raise AuthorizationDenied("Signet authorization channel is outside configured scope")
+ if outcome.request_fingerprint != context.request_fingerprint:
+ raise AuthorizationDenied("Signet authorization fingerprint is outside configured scope")
+ if outcome.issuer_policy_ref != context.issuer_policy_ref:
+ raise AuthorizationDenied("Signet issuer policy is outside configured scope")
+ if outcome.rotation_policy_ref != context.rotation_policy_ref:
+ raise AuthorizationDenied("Signet rotation policy is outside configured scope")
+ if type(outcome.max_uses) is not int or outcome.max_uses != 1:
+ raise AuthorizationDenied("Signet authorization must be single-use")
+ if type(outcome.max_provider_attempts) is not int or outcome.max_provider_attempts < 1:
+ raise AuthorizationDenied("Signet provider-attempt budget must be positive")
+ if outcome.max_provider_attempts > context.provider_attempt_ceiling:
+ raise AuthorizationDenied("Signet provider-attempt budget exceeds runtime policy")
+ if not outcome.allowed_connector_ids or len(set(outcome.allowed_connector_ids)) != len(
+ outcome.allowed_connector_ids
+ ):
+ raise AuthorizationDenied("Signet connector scope is empty or contains duplicates")
+ if not outcome.allowed_purposes or len(set(outcome.allowed_purposes)) != len(
+ outcome.allowed_purposes
+ ):
+ raise AuthorizationDenied("Signet purpose scope is empty or contains duplicates")
+ if not set(outcome.allowed_connector_ids).issubset(context.available_connector_ids):
+ raise AuthorizationDenied("Signet connector scope exceeds runtime policy")
+ if set(outcome.allowed_purposes) != {context.purpose}:
+ raise AuthorizationDenied("Signet purpose scope does not exactly match runtime policy")
+
+
+def _parse_time(value: str, name: str) -> datetime:
+ try:
+ parsed = datetime.fromisoformat(value)
+ except ValueError as exc:
+ raise AuthorizationDenied(f"Signet authorization has invalid {name}") from exc
+ if parsed.tzinfo is None:
+ raise AuthorizationDenied(f"Signet authorization {name} must be timezone-aware")
+ return parsed.astimezone(UTC)
diff --git a/tests/test_credential_custody.py b/tests/test_credential_custody.py
new file mode 100644
index 0000000..fc28929
--- /dev/null
+++ b/tests/test_credential_custody.py
@@ -0,0 +1,573 @@
+"""Credential-free contract checks for the provider custody boundary."""
+
+from datetime import UTC, datetime, timedelta
+
+import pytest
+
+from baton.credential_custody import (
+ ConsumptionReservation,
+ ConsumptionReservationRequired,
+ CredentialCustodyAuthorizer,
+ CredentialCustodyInvokerFactory,
+ CredentialUseRequest,
+ CustodyAuthorizationDenied,
+ CustodyResultStatus,
+ ProviderChannel,
+ ProviderCredentialHandle,
+ SanitizedCustodyOutcome,
+ VerifiedDelegatedAuthorization,
+ VerifiedWorkloadAuthorization,
+ WorkloadAuthorizationReference,
+ authorize_provider_dispatch,
+)
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ ConnectorRoute,
+ DeliveryStatus,
+ DispatchBinding,
+ DispatchClaim,
+ DispatchRequest,
+ VerifiedDispatchGrant,
+ dispatch_request_fingerprint,
+)
+
+NOW = datetime(2026, 6, 4, tzinfo=UTC)
+FINGERPRINT = dispatch_request_fingerprint(
+ dispatch_id="dispatch-1",
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key="dispatch-once-1",
+)
+
+
+def _handle(
+ connector_id: str = "sms-primary",
+ provider_key: str = "provider-primary",
+) -> ProviderCredentialHandle:
+ return ProviderCredentialHandle(
+ handle_id=f"handle-{connector_id}-v3",
+ connector_id=connector_id,
+ provider_key=provider_key,
+ channel=ProviderChannel.SMS,
+ version_ref="active-v3",
+ )
+
+
+def _request(**overrides) -> CredentialUseRequest:
+ fields = {
+ "operation_id": "operation-1",
+ "dispatch_id": "dispatch-1",
+ "workload_id": "mea-comms",
+ "connector_id": "sms-primary",
+ "channel": ProviderChannel.SMS,
+ "purpose": "case_notification",
+ "recipient_ref": "recipient-ref-1",
+ "payload_ref": "payload-ref-1",
+ "idempotency_key": "dispatch-once-1",
+ "dispatch_claim_id": "claim-1",
+ }
+ fields.update(overrides)
+ fields.setdefault(
+ "request_fingerprint",
+ dispatch_request_fingerprint(
+ dispatch_id=fields["dispatch_id"],
+ workflow_id=fields["operation_id"],
+ channel=Channel(fields["channel"].value),
+ recipient_ref=fields["recipient_ref"],
+ payload_ref=fields["payload_ref"],
+ idempotency_key=fields["idempotency_key"],
+ ),
+ )
+ return CredentialUseRequest(**fields)
+
+
+def _authorization(**overrides) -> VerifiedWorkloadAuthorization:
+ fields = {
+ "authorization_id": "authorization-1",
+ "workload_id": "mea-comms",
+ "allowed_connector_ids": frozenset({"sms-primary", "sms-backup"}),
+ "allowed_channels": frozenset({ProviderChannel.SMS}),
+ "allowed_purposes": frozenset({"case_notification"}),
+ "request_fingerprint": FINGERPRINT,
+ "not_before": NOW - timedelta(minutes=1),
+ "not_after": NOW + timedelta(minutes=5),
+ "max_uses": None,
+ "max_provider_attempts": 3,
+ }
+ fields.update(overrides)
+ return VerifiedWorkloadAuthorization(**fields)
+
+
+def _reservation(**overrides) -> ConsumptionReservation:
+ fields = {
+ "reservation_id": "reservation-1",
+ "authorization_id": "authorization-1",
+ "request_fingerprint": FINGERPRINT,
+ "idempotency_key": "dispatch-once-1",
+ }
+ fields.update(overrides)
+ return ConsumptionReservation(**fields)
+
+
+class OutcomeLedger:
+ def __init__(self, reservation: ConsumptionReservation | None = None):
+ self.reservation = reservation or _reservation()
+ self.requests: list[CredentialUseRequest] = []
+ self.outcomes: list[SanitizedCustodyOutcome] = []
+ self.attempt_count = 0
+ self.max_provider_attempts = 0
+
+ async def reserve_once(
+ self,
+ _authorization: VerifiedWorkloadAuthorization,
+ request: CredentialUseRequest,
+ ) -> ConsumptionReservation:
+ self.requests.append(request)
+ self.max_provider_attempts = _authorization.max_provider_attempts
+ return self.reservation
+
+ async def reserve_attempt(self, _reservation, _authorized_use) -> int:
+ if self.attempt_count >= self.max_provider_attempts:
+ raise CustodyAuthorizationDenied("provider attempt budget is exhausted")
+ self.attempt_count += 1
+ return self.attempt_count
+
+ async def complete(
+ self,
+ _reservation: ConsumptionReservation,
+ outcome: SanitizedCustodyOutcome,
+ ) -> None:
+ self.outcomes.append(outcome)
+
+
+class OutcomeVerifier:
+ def __init__(self, authorization: VerifiedWorkloadAuthorization):
+ self.authorization = authorization
+
+ async def verify(
+ self,
+ _reference: WorkloadAuthorizationReference,
+ _request: CredentialUseRequest,
+ ) -> VerifiedWorkloadAuthorization:
+ return self.authorization
+
+
+class OutcomeResolver:
+ def __init__(self, outcome: SanitizedCustodyOutcome):
+ self.outcome = outcome
+ self.calls = []
+
+ async def invoke(self, handle, authorized_use) -> SanitizedCustodyOutcome:
+ self.calls.append((handle, authorized_use))
+ return self.outcome
+
+
+def _delegated_request() -> DispatchRequest:
+ return DispatchRequest(
+ dispatch_id="dispatch-1",
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key="dispatch-once-1",
+ request_fingerprint=FINGERPRINT,
+ )
+
+
+def _delegated_route() -> ConnectorRoute:
+ return ConnectorRoute(
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ channel=Channel.SMS,
+ credential_handle="handle-sms-primary-v3",
+ priority=1,
+ )
+
+
+def _delegated_backup_route() -> ConnectorRoute:
+ return ConnectorRoute(
+ connector_id="sms-backup",
+ provider_key="provider-backup",
+ channel=Channel.SMS,
+ credential_handle="handle-sms-backup-v3",
+ priority=2,
+ )
+
+
+def _delegated_grant(
+ max_attempts: int = 1,
+ *,
+ allowed_connectors: frozenset[str] | None = None,
+) -> VerifiedDelegatedAuthorization:
+ return VerifiedDelegatedAuthorization(
+ principal="mea-comms",
+ channel=Channel.SMS,
+ allowed_connectors=allowed_connectors
+ or frozenset({"sms-primary", "sms-backup"}),
+ not_after=NOW + timedelta(minutes=5),
+ max_attempts=max_attempts,
+ request_fingerprint=FINGERPRINT,
+ authorization_id="authorization-1",
+ issuer="signet://issuer/mea",
+ audience="baton://delegated-provider-executor",
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ allowed_purposes=frozenset({"case_notification"}),
+ not_before=NOW - timedelta(minutes=1),
+ max_uses=1,
+ )
+
+
+def _dispatch_claim() -> DispatchClaim:
+ return DispatchClaim(
+ claim_id="claim-1",
+ binding=DispatchBinding("dispatch-once-1", FINGERPRINT),
+ lease_expires_at=NOW + timedelta(minutes=1),
+ )
+
+
+async def test_single_dispatch_reservation_authorizes_only_opaque_handle_use():
+ dispatch = await authorize_provider_dispatch(
+ _handle(),
+ _request(),
+ _authorization(max_uses=1),
+ ledger=OutcomeLedger(),
+ now=NOW,
+ )
+ result = dispatch.authorize_handle(_handle())
+
+ assert result.handle_id == "handle-sms-primary-v3"
+ assert result.dispatch_id == "dispatch-1"
+ assert result.recipient_ref == "recipient-ref-1"
+ assert result.payload_ref == "payload-ref-1"
+ assert result.reservation_id == "reservation-1"
+ assert dispatch.max_provider_attempts == 3
+ assert not hasattr(result, "material")
+
+
+async def test_request_fingerprint_change_is_denied():
+ changed_request = _request(
+ dispatch_id="dispatch-2",
+ idempotency_key="dispatch-once-2",
+ )
+ with pytest.raises(CustodyAuthorizationDenied):
+ await authorize_provider_dispatch(
+ _handle(),
+ changed_request,
+ _authorization(max_uses=1),
+ ledger=OutcomeLedger(),
+ now=NOW,
+ )
+
+
+def test_credential_request_rejects_noncanonical_fingerprint():
+ with pytest.raises(ValueError, match="does not bind"):
+ _request(request_fingerprint="b" * 64)
+
+
+async def test_connector_or_purpose_outside_scope_is_denied():
+ with pytest.raises(CustodyAuthorizationDenied):
+ await authorize_provider_dispatch(
+ _handle(),
+ _request(purpose="unapproved_operation"),
+ _authorization(max_uses=1),
+ ledger=OutcomeLedger(),
+ now=NOW,
+ )
+
+
+async def test_expired_authorization_is_denied():
+ with pytest.raises(CustodyAuthorizationDenied):
+ await authorize_provider_dispatch(
+ _handle(),
+ _request(),
+ _authorization(max_uses=1, not_after=NOW),
+ ledger=OutcomeLedger(),
+ now=NOW,
+ )
+
+
+async def test_unbounded_authorization_is_denied_before_ledger_reservation():
+ ledger = OutcomeLedger()
+ with pytest.raises(CustodyAuthorizationDenied):
+ await authorize_provider_dispatch(
+ _handle(), _request(), _authorization(), ledger=ledger, now=NOW
+ )
+ assert ledger.requests == []
+
+
+async def test_bounded_authorization_reserves_through_ledger():
+ ledger = OutcomeLedger()
+ result = await authorize_provider_dispatch(
+ _handle(),
+ _request(),
+ _authorization(max_uses=1),
+ ledger=ledger,
+ now=NOW,
+ )
+ assert result.reservation_id == "reservation-1"
+ assert ledger.requests == [_request()]
+
+
+async def test_bounded_authorization_rejects_reservation_for_other_request():
+ with pytest.raises(ConsumptionReservationRequired):
+ await authorize_provider_dispatch(
+ _handle(),
+ _request(),
+ _authorization(max_uses=1),
+ ledger=OutcomeLedger(_reservation(request_fingerprint="b" * 64)),
+ now=NOW,
+ )
+
+
+async def test_authorizer_obtains_verified_outcome_then_reserves_it():
+ authorizer = CredentialCustodyAuthorizer(
+ OutcomeVerifier(_authorization(max_uses=1)),
+ OutcomeLedger(),
+ clock=lambda: NOW,
+ )
+ result = await authorizer.authorize(
+ WorkloadAuthorizationReference("authorization-ref-1"),
+ _handle(),
+ _request(),
+ )
+ assert result.authorization_id == "authorization-1"
+
+
+async def test_invalid_initial_handle_is_denied_before_ledger_reservation():
+ ledger = OutcomeLedger()
+ with pytest.raises(CustodyAuthorizationDenied):
+ await authorize_provider_dispatch(
+ _handle("sms-backup", "provider-backup"),
+ _request(),
+ _authorization(max_uses=1),
+ ledger=ledger,
+ now=NOW,
+ )
+ assert ledger.requests == []
+
+
+async def test_one_reserved_dispatch_allows_scoped_primary_to_backup_selection():
+ ledger = OutcomeLedger()
+ dispatch = await authorize_provider_dispatch(
+ _handle(),
+ _request(),
+ _authorization(max_uses=1),
+ ledger=ledger,
+ now=NOW,
+ )
+
+ primary = dispatch.authorize_handle(_handle())
+ backup = dispatch.authorize_handle(_handle("sms-backup", "provider-backup"))
+ assert primary.reservation_id == backup.reservation_id == "reservation-1"
+ assert len(ledger.requests) == 1
+
+ with pytest.raises(CustodyAuthorizationDenied):
+ dispatch.authorize_handle(_handle("sms-unapproved", "provider-unapproved"))
+
+
+async def test_delegated_factory_reserves_before_custodied_provider_invocation():
+ ledger = OutcomeLedger()
+ authorizer = CredentialCustodyAuthorizer(
+ OutcomeVerifier(_authorization(max_uses=1)),
+ ledger,
+ clock=lambda: NOW,
+ )
+ resolver = OutcomeResolver(
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.FAILED,
+ audit_ref="audit-1",
+ failure_code="provider_timeout",
+ retryable=True,
+ failover_allowed=True,
+ counts_toward_circuit=True,
+ )
+ )
+ factory = CredentialCustodyInvokerFactory(
+ authorizer,
+ resolver,
+ {"sms-primary": _handle()},
+ workload_id="mea-comms",
+ purpose="case_notification",
+ )
+ route = _delegated_route()
+ invoker = await factory.prepare(
+ CapabilityReference("authorization-ref-1"),
+ _delegated_request(),
+ _delegated_grant(),
+ _dispatch_claim(),
+ route,
+ (route,),
+ )
+ result = await invoker.invoke(route, _delegated_request())
+
+ assert ledger.requests[0].dispatch_id == "dispatch-1"
+ assert ledger.requests[0].connector_id == "sms-primary"
+ assert resolver.calls[0][1].reservation_id == "reservation-1"
+ assert result.status is DeliveryStatus.FAILED
+ assert result.retryable is True
+ assert result.failover_allowed is True
+ assert result.counts_toward_circuit is True
+
+
+async def test_delegated_factory_rejects_independent_dispatch_grant():
+ ledger = OutcomeLedger()
+ factory = CredentialCustodyInvokerFactory(
+ CredentialCustodyAuthorizer(
+ OutcomeVerifier(_authorization(max_uses=1, max_provider_attempts=1)),
+ ledger,
+ clock=lambda: NOW,
+ ),
+ OutcomeResolver(
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.ACCEPTED,
+ audit_ref="audit-1",
+ )
+ ),
+ {"sms-primary": _handle()},
+ workload_id="mea-comms",
+ purpose="case_notification",
+ )
+ route = _delegated_route()
+ with pytest.raises(AuthorizationDenied):
+ await factory.prepare(
+ CapabilityReference("authorization-ref-1"),
+ _delegated_request(),
+ VerifiedDispatchGrant(
+ principal="mea-comms",
+ channel=Channel.SMS,
+ allowed_connectors=frozenset({"sms-primary"}),
+ not_after=NOW + timedelta(minutes=5),
+ max_attempts=1,
+ request_fingerprint=FINGERPRINT,
+ ),
+ _dispatch_claim(),
+ route,
+ (route,),
+ )
+ assert ledger.requests == []
+
+
+async def test_delegated_factory_denies_failover_policy_outside_custody_scope_before_reserving():
+ ledger = OutcomeLedger()
+ factory = CredentialCustodyInvokerFactory(
+ CredentialCustodyAuthorizer(
+ OutcomeVerifier(
+ _authorization(
+ max_uses=1,
+ allowed_connector_ids=frozenset({"sms-primary"}),
+ )
+ ),
+ ledger,
+ clock=lambda: NOW,
+ ),
+ OutcomeResolver(
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.ACCEPTED,
+ audit_ref="audit-1",
+ )
+ ),
+ {
+ "sms-primary": _handle(),
+ "sms-backup": _handle("sms-backup", "provider-backup"),
+ },
+ workload_id="mea-comms",
+ purpose="case_notification",
+ )
+ primary = _delegated_route()
+ backup = _delegated_backup_route()
+ with pytest.raises(AuthorizationDenied):
+ await factory.prepare(
+ CapabilityReference("authorization-ref-1"),
+ _delegated_request(),
+ _delegated_grant(allowed_connectors=frozenset({"sms-primary"})),
+ _dispatch_claim(),
+ primary,
+ (primary, backup),
+ )
+ assert ledger.requests == []
+
+
+def test_authorization_rejects_zero_provider_attempt_budget():
+ with pytest.raises(ValueError):
+ _authorization(max_provider_attempts=0)
+
+
+def test_outcome_allows_only_sanitized_failure_identifier():
+ result = SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.FAILED,
+ audit_ref="audit-1",
+ failure_code="provider_timeout",
+ )
+ assert result.failure_code == "provider_timeout"
+ assert result.retryable is False
+
+ with pytest.raises(ValueError):
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.FAILED,
+ audit_ref="audit-1",
+ failure_code="raw upstream detail: refused",
+ )
+
+ with pytest.raises(ValueError):
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.ACCEPTED,
+ audit_ref="audit-1",
+ retryable=True,
+ )
+
+
+@pytest.mark.parametrize("expire_during_reservation", [False, True])
+async def test_reserved_invoker_rechecks_expiry_before_provider_use(expire_during_reservation):
+ now = [NOW]
+
+ class DelayedLedger(OutcomeLedger):
+ async def reserve_attempt(self, reservation, use):
+ result = await super().reserve_attempt(reservation, use)
+ if expire_during_reservation:
+ now[0] += timedelta(minutes=5)
+ return result
+
+ ledger = DelayedLedger()
+ authorizer = CredentialCustodyAuthorizer.for_verified_outcomes(ledger, clock=lambda: now[0])
+ resolver = OutcomeResolver(SanitizedCustodyOutcome(
+ operation_id="operation-1", dispatch_id="dispatch-1", provider_key="provider-primary",
+ status=CustodyResultStatus.ACCEPTED, audit_ref="audit-1",
+ ))
+ factory = CredentialCustodyInvokerFactory(
+ authorizer, resolver, {"sms-primary": _handle()},
+ workload_id="mea-comms", purpose="case_notification",
+ )
+ route = _delegated_route()
+ invoker = await factory.prepare(
+ CapabilityReference("authorization-ref-1"), _delegated_request(),
+ _delegated_grant(), _dispatch_claim(), route, (route,),
+ )
+ if not expire_during_reservation:
+ now[0] += timedelta(minutes=5)
+ with pytest.raises(AuthorizationDenied):
+ await invoker.invoke(route, _delegated_request())
+ assert resolver.calls == []
+ assert ledger.attempt_count == int(expire_during_reservation)
diff --git a/tests/test_delegated_connector.py b/tests/test_delegated_connector.py
new file mode 100644
index 0000000..55cc630
--- /dev/null
+++ b/tests/test_delegated_connector.py
@@ -0,0 +1,552 @@
+"""Tests for provider dispatch orchestration without credential material."""
+
+from __future__ import annotations
+
+import asyncio
+import dataclasses
+from datetime import UTC, datetime, timedelta
+
+import pytest
+
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ ConnectorRoute,
+ DelegatedConnectorExecutor,
+ DeliveryOutcome,
+ DeliveryStatus,
+ DispatchBinding,
+ DispatchBindingConflict,
+ DispatchClaim,
+ DispatchInProgress,
+ DispatchRequest,
+ DispatchSignal,
+ DispatchSignalKind,
+ DispatchStateUnavailable,
+ MonitoringUnavailable,
+ ProviderAttemptOutcome,
+ VerifiedDispatchGrant,
+ dispatch_request_fingerprint,
+)
+
+
+def _fingerprint(
+ idempotency_key: str = "dispatch-once-1",
+ *,
+ dispatch_id: str | None = None,
+) -> str:
+ return dispatch_request_fingerprint(
+ dispatch_id=dispatch_id or f"dispatch-{idempotency_key}",
+ workflow_id="workflow-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ )
+
+
+FINGERPRINT = _fingerprint()
+
+
+def _request(
+ idempotency_key: str = "dispatch-once-1",
+ *,
+ dispatch_id: str | None = None,
+ request_fingerprint: str | None = None,
+) -> DispatchRequest:
+ resolved_dispatch_id = dispatch_id or f"dispatch-{idempotency_key}"
+ return DispatchRequest(
+ dispatch_id=resolved_dispatch_id,
+ workflow_id="workflow-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ request_fingerprint=request_fingerprint
+ or _fingerprint(idempotency_key, dispatch_id=resolved_dispatch_id),
+ )
+
+
+def test_dispatch_request_rejects_noncanonical_fingerprint():
+ with pytest.raises(ValueError, match="does not bind"):
+ DispatchRequest(
+ dispatch_id="dispatch-once-1",
+ workflow_id="workflow-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key="dispatch-once-1",
+ request_fingerprint="f" * 64,
+ )
+
+
+def _route(
+ connector_id: str,
+ priority: int,
+ *,
+ max_attempts: int = 1,
+ threshold: int = 3,
+ timeout_ms: int = 5000,
+) -> ConnectorRoute:
+ return ConnectorRoute(
+ connector_id=connector_id,
+ provider_key=f"provider-{connector_id}",
+ channel=Channel.SMS,
+ credential_handle=f"opaque-handle-{connector_id}",
+ priority=priority,
+ max_attempts=max_attempts,
+ circuit_breaker_threshold=threshold,
+ timeout_ms=timeout_ms,
+ )
+
+
+class AcceptedVerifier:
+ async def verify(
+ self, _capability: CapabilityReference, request: DispatchRequest
+ ) -> VerifiedDispatchGrant:
+ return VerifiedDispatchGrant(
+ principal="comms-runtime",
+ channel=request.channel,
+ allowed_connectors=frozenset({"primary", "backup"}),
+ not_after=datetime.now(UTC) + timedelta(minutes=5),
+ max_attempts=4,
+ request_fingerprint=request.request_fingerprint,
+ )
+
+
+class DeniedVerifier:
+ async def verify(
+ self, _capability: CapabilityReference, _request: DispatchRequest
+ ) -> VerifiedDispatchGrant:
+ raise AuthorizationDenied("denied")
+
+
+class MismatchedFingerprintVerifier(AcceptedVerifier):
+ async def verify(
+ self, capability: CapabilityReference, request: DispatchRequest
+ ) -> VerifiedDispatchGrant:
+ grant = await super().verify(capability, request)
+ return dataclasses.replace(grant, request_fingerprint="e" * 64)
+
+
+class OutcomeInvoker:
+ def __init__(self, outcomes: list[ProviderAttemptOutcome]):
+ self._outcomes = list(outcomes)
+ self.calls: list[str] = []
+
+ async def invoke(
+ self, route: ConnectorRoute, _request: DispatchRequest
+ ) -> ProviderAttemptOutcome:
+ self.calls.append(route.connector_id)
+ return self._outcomes.pop(0)
+
+
+class HangingInvoker:
+ async def invoke(
+ self, _route: ConnectorRoute, _request: DispatchRequest
+ ) -> ProviderAttemptOutcome:
+ await asyncio.sleep(60)
+ raise AssertionError("unreachable")
+
+
+class OutcomeInvokerFactory:
+ def __init__(self, invoker):
+ self.invoker = invoker
+ self.prepare_calls: list[str] = []
+
+ async def prepare(
+ self,
+ _capability: CapabilityReference,
+ _request: DispatchRequest,
+ _grant: VerifiedDispatchGrant,
+ _claim: DispatchClaim,
+ initial_route: ConnectorRoute,
+ _authorized_routes: tuple[ConnectorRoute, ...],
+ ):
+ self.prepare_calls.append(initial_route.connector_id)
+ return self.invoker
+
+
+class JournalCheckingFactory(OutcomeInvokerFactory):
+ def __init__(self, invoker, journal):
+ super().__init__(invoker)
+ self.journal = journal
+
+ async def prepare(
+ self,
+ capability: CapabilityReference,
+ request: DispatchRequest,
+ grant: VerifiedDispatchGrant,
+ claim: DispatchClaim,
+ initial_route: ConnectorRoute,
+ authorized_routes: tuple[ConnectorRoute, ...],
+ ):
+ assert request.idempotency_key in self.journal.running
+ return await super().prepare(
+ capability, request, grant, claim, initial_route, authorized_routes
+ )
+
+
+class MemoryJournal:
+ def __init__(self):
+ self.running: set[str] = set()
+ self.results: dict[str, DeliveryOutcome] = {}
+ self.fingerprints: dict[str, str] = {}
+ self.claims: dict[str, str] = {}
+
+ def _assert_binding(self, binding: DispatchBinding) -> None:
+ fingerprint = self.fingerprints.get(binding.idempotency_key)
+ if fingerprint is not None and fingerprint != binding.request_fingerprint:
+ raise DispatchBindingConflict("idempotency key is bound to another request")
+
+ async def completed(self, binding: DispatchBinding) -> DeliveryOutcome | None:
+ self._assert_binding(binding)
+ return self.results.get(binding.idempotency_key)
+
+ async def begin(self, binding: DispatchBinding) -> DispatchClaim | None:
+ self._assert_binding(binding)
+ if binding.idempotency_key in self.running:
+ return None
+ self.fingerprints[binding.idempotency_key] = binding.request_fingerprint
+ self.running.add(binding.idempotency_key)
+ claim_id = f"claim-{binding.idempotency_key}"
+ self.claims[binding.idempotency_key] = claim_id
+ return DispatchClaim(
+ claim_id=claim_id,
+ binding=binding,
+ lease_expires_at=datetime.now(UTC) + timedelta(minutes=1),
+ )
+
+ async def complete(self, claim: DispatchClaim, outcome: DeliveryOutcome) -> None:
+ self._assert_binding(claim.binding)
+ assert self.claims[claim.binding.idempotency_key] == claim.claim_id
+ self.running.discard(claim.binding.idempotency_key)
+ self.results[claim.binding.idempotency_key] = outcome
+
+ async def renew(self, claim: DispatchClaim) -> None:
+ self._assert_binding(claim.binding)
+ assert self.claims[claim.binding.idempotency_key] == claim.claim_id
+
+ async def abort(self, claim: DispatchClaim) -> None:
+ self._assert_binding(claim.binding)
+ if self.claims.get(claim.binding.idempotency_key) == claim.claim_id:
+ self.running.discard(claim.binding.idempotency_key)
+
+
+class CompleteFailingJournal(MemoryJournal):
+ async def complete(self, _claim: DispatchClaim, _outcome: DeliveryOutcome) -> None:
+ raise RuntimeError("injected durable completion failure")
+
+
+class SignalSink:
+ def __init__(self, failures_remaining: int = 0):
+ self.failures_remaining = failures_remaining
+ self.signals: list[DispatchSignal] = []
+
+ async def emit(self, signal: DispatchSignal) -> None:
+ if self.failures_remaining:
+ self.failures_remaining -= 1
+ raise RuntimeError("signal unavailable")
+ self.signals.append(signal)
+
+
+def _success(audit_ref: str = "audit-success") -> ProviderAttemptOutcome:
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus.ACCEPTED,
+ audit_ref=audit_ref,
+ )
+
+
+def _failure(
+ code: str = "provider_unavailable",
+ *,
+ retryable: bool = False,
+ failover_allowed: bool = False,
+ counts_toward_circuit: bool = False,
+) -> ProviderAttemptOutcome:
+ return ProviderAttemptOutcome(
+ status=DeliveryStatus.FAILED,
+ audit_ref=f"audit-{code}",
+ failure_code=code,
+ retryable=retryable,
+ failover_allowed=failover_allowed,
+ counts_toward_circuit=counts_toward_circuit,
+ )
+
+
+def _executor(invoker, journal=None, sink=None, routes=None, factory=None) -> DelegatedConnectorExecutor:
+ return DelegatedConnectorExecutor(
+ routes or [_route("primary", 1), _route("backup", 2)],
+ AcceptedVerifier(),
+ factory or OutcomeInvokerFactory(invoker),
+ journal or MemoryJournal(),
+ sink or SignalSink(),
+ sleep=lambda _delay: asyncio.sleep(0),
+ )
+
+
+async def test_success_returns_sanitized_outcome_only():
+ invoker = OutcomeInvoker([_success()])
+ sink = SignalSink()
+ outcome = await _executor(invoker, sink=sink).dispatch(
+ CapabilityReference("authorization-ref-1"), _request()
+ )
+
+ assert outcome.status is DeliveryStatus.ACCEPTED
+ assert outcome.provider_key == "provider-primary"
+ assert outcome.attempt_count == 1
+ fields = {field.name for field in dataclasses.fields(outcome)}
+ assert fields == {
+ "dispatch_id",
+ "workflow_id",
+ "channel",
+ "connector_id",
+ "provider_key",
+ "status",
+ "attempt_count",
+ "failover_used",
+ "audit_ref",
+ "failure_code",
+ }
+ assert [signal.kind for signal in sink.signals] == [DispatchSignalKind.ATTEMPT_SUCCEEDED]
+ assert sink.signals[0].dispatch_id == "dispatch-dispatch-once-1"
+ assert sink.signals[0].connector_id == "primary"
+
+
+async def test_authorization_denial_prevents_invocation_and_emits_signal():
+ invoker = OutcomeInvoker([_success()])
+ factory = OutcomeInvokerFactory(invoker)
+ sink = SignalSink()
+ executor = DelegatedConnectorExecutor(
+ [_route("primary", 1)],
+ DeniedVerifier(),
+ factory,
+ MemoryJournal(),
+ sink,
+ )
+
+ with pytest.raises(AuthorizationDenied):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert invoker.calls == []
+ assert factory.prepare_calls == []
+ assert sink.signals[0].kind is DispatchSignalKind.AUTHORIZATION_DENIED
+ assert sink.signals[0].dispatch_id == "dispatch-dispatch-once-1"
+
+
+async def test_authorization_fingerprint_mismatch_prevents_invocation():
+ invoker = OutcomeInvoker([_success()])
+ factory = OutcomeInvokerFactory(invoker)
+ sink = SignalSink()
+ executor = DelegatedConnectorExecutor(
+ [_route("primary", 1)],
+ MismatchedFingerprintVerifier(),
+ factory,
+ MemoryJournal(),
+ sink,
+ )
+
+ with pytest.raises(AuthorizationDenied):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert invoker.calls == []
+ assert factory.prepare_calls == []
+ assert sink.signals[0].kind is DispatchSignalKind.AUTHORIZATION_DENIED
+
+
+async def test_failed_primary_fails_over_to_backup_with_sanitized_signals():
+ invoker = OutcomeInvoker([_failure(failover_allowed=True), _success("audit-backup")])
+ sink = SignalSink()
+ outcome = await _executor(invoker, sink=sink).dispatch(
+ CapabilityReference("authorization-ref-1"), _request()
+ )
+
+ assert invoker.calls == ["primary", "backup"]
+ assert outcome.provider_key == "provider-backup"
+ assert outcome.failover_used is True
+ assert outcome.attempt_count == 2
+ assert [signal.kind for signal in sink.signals] == [
+ DispatchSignalKind.ATTEMPT_FAILED,
+ DispatchSignalKind.FAILOVER_USED,
+ DispatchSignalKind.ATTEMPT_SUCCEEDED,
+ ]
+ assert {signal.dispatch_id for signal in sink.signals} == {"dispatch-dispatch-once-1"}
+
+
+async def test_provider_failure_without_failover_approval_does_not_send_to_backup():
+ invoker = OutcomeInvoker([_failure("invalid_payload")])
+ outcome = await _executor(invoker).dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert outcome.status is DeliveryStatus.EXHAUSTED
+ assert invoker.calls == ["primary"]
+ assert outcome.failover_used is False
+
+
+async def test_open_circuit_skips_failing_primary_on_next_dispatch():
+ invoker = OutcomeInvoker(
+ [_failure(failover_allowed=True, counts_toward_circuit=True), _success(), _success()]
+ )
+ executor = _executor(
+ invoker,
+ routes=[_route("primary", 1, threshold=1), _route("backup", 2)],
+ )
+ first = await executor.dispatch(CapabilityReference("authorization-ref-1"), _request("one"))
+ second = await executor.dispatch(CapabilityReference("authorization-ref-2"), _request("two"))
+
+ assert first.provider_key == "provider-backup"
+ assert second.provider_key == "provider-backup"
+ assert invoker.calls == ["primary", "backup", "backup"]
+
+
+async def test_delivery_rejection_does_not_open_provider_circuit():
+ invoker = OutcomeInvoker([_failure("invalid_payload"), _success()])
+ executor = _executor(
+ invoker,
+ routes=[_route("primary", 1, threshold=1), _route("backup", 2)],
+ )
+ first = await executor.dispatch(CapabilityReference("authorization-ref-1"), _request("one"))
+ second = await executor.dispatch(CapabilityReference("authorization-ref-2"), _request("two"))
+
+ assert first.status is DeliveryStatus.EXHAUSTED
+ assert second.provider_key == "provider-primary"
+ assert invoker.calls == ["primary", "primary"]
+
+
+async def test_timeout_is_sanitized_and_fails_over():
+ invoker = HangingInvoker()
+ executor = _executor(
+ invoker,
+ routes=[_route("primary", 1, timeout_ms=1)],
+ )
+ outcome = await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert outcome.status is DeliveryStatus.EXHAUSTED
+ assert outcome.failure_code == "provider_timeout"
+
+
+async def test_completed_idempotency_key_returns_without_second_invocation():
+ invoker = OutcomeInvoker([_success()])
+ factory = OutcomeInvokerFactory(invoker)
+ journal = MemoryJournal()
+ executor = _executor(invoker, journal=journal, factory=factory)
+ first = await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+ second = await executor.dispatch(CapabilityReference("authorization-ref-2"), _request())
+
+ assert first == second
+ assert invoker.calls == ["primary"]
+ assert factory.prepare_calls == ["primary"]
+
+
+async def test_custody_preparation_occurs_only_after_idempotency_begin():
+ invoker = OutcomeInvoker([_success()])
+ journal = MemoryJournal()
+ factory = JournalCheckingFactory(invoker, journal)
+ outcome = await _executor(invoker, journal=journal, factory=factory).dispatch(
+ CapabilityReference("authorization-ref-1"), _request()
+ )
+
+ assert outcome.status is DeliveryStatus.ACCEPTED
+ assert factory.prepare_calls == ["primary"]
+
+
+async def test_open_circuit_without_attempt_does_not_prepare_custody_invoker():
+ invoker = OutcomeInvoker([_failure(counts_toward_circuit=True)])
+ factory = OutcomeInvokerFactory(invoker)
+ executor = _executor(
+ invoker,
+ routes=[_route("primary", 1, threshold=1)],
+ factory=factory,
+ )
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request("one"))
+ outcome = await executor.dispatch(CapabilityReference("authorization-ref-2"), _request("two"))
+
+ assert outcome.status is DeliveryStatus.EXHAUSTED
+ assert outcome.failure_code == "circuit_open"
+ assert outcome.connector_id == "primary"
+ assert factory.prepare_calls == ["primary"]
+
+
+async def test_changed_request_fingerprint_cannot_reuse_completed_idempotency_key():
+ invoker = OutcomeInvoker([_success()])
+ journal = MemoryJournal()
+ sink = SignalSink()
+ executor = _executor(invoker, journal=journal, sink=sink)
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ with pytest.raises(DispatchBindingConflict):
+ await executor.dispatch(
+ CapabilityReference("authorization-ref-2"),
+ _request(dispatch_id="dispatch-changed"),
+ )
+
+ assert invoker.calls == ["primary"]
+ assert sink.signals[-1].kind is DispatchSignalKind.IDEMPOTENCY_CONFLICT
+ assert sink.signals[-1].dispatch_id == "dispatch-changed"
+
+
+async def test_in_progress_idempotency_key_fails_closed():
+ journal = MemoryJournal()
+ journal.running.add("dispatch-once-1")
+ journal.fingerprints["dispatch-once-1"] = FINGERPRINT
+ executor = _executor(OutcomeInvoker([_success()]), journal=journal)
+
+ with pytest.raises(DispatchInProgress):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+
+async def test_signal_failure_prevents_unaudited_authorization_denial():
+ executor = DelegatedConnectorExecutor(
+ [_route("primary", 1)],
+ DeniedVerifier(),
+ OutcomeInvokerFactory(OutcomeInvoker([_success()])),
+ MemoryJournal(),
+ SignalSink(failures_remaining=1),
+ )
+
+ with pytest.raises(MonitoringUnavailable):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+
+async def test_success_is_not_resent_when_terminal_signal_must_be_retried():
+ invoker = OutcomeInvoker([_success()])
+ journal = MemoryJournal()
+ sink = SignalSink(failures_remaining=1)
+ executor = _executor(invoker, journal=journal, sink=sink)
+
+ with pytest.raises(MonitoringUnavailable):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+ outcome = await executor.dispatch(CapabilityReference("authorization-ref-2"), _request())
+
+ assert outcome.status is DeliveryStatus.ACCEPTED
+ assert invoker.calls == ["primary"]
+ assert [signal.kind for signal in sink.signals] == [DispatchSignalKind.ATTEMPT_SUCCEEDED]
+
+
+async def test_post_attempt_failure_does_not_abort_claim_or_immediately_resend():
+ invoker = OutcomeInvoker([_failure("provider_unavailable")])
+ journal = MemoryJournal()
+ sink = SignalSink(failures_remaining=1)
+ executor = _executor(invoker, journal=journal, sink=sink)
+
+ with pytest.raises(MonitoringUnavailable):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert "dispatch-once-1" in journal.running
+ with pytest.raises(DispatchInProgress):
+ await executor.dispatch(CapabilityReference("authorization-ref-2"), _request())
+ assert invoker.calls == ["primary"]
+
+
+async def test_post_provider_journal_failure_keeps_claim_and_does_not_resend():
+ invoker = OutcomeInvoker([_success()])
+ journal = CompleteFailingJournal()
+ executor = _executor(invoker, journal=journal)
+
+ with pytest.raises(DispatchStateUnavailable):
+ await executor.dispatch(CapabilityReference("authorization-ref-1"), _request())
+
+ assert "dispatch-once-1" in journal.running
+ with pytest.raises(DispatchInProgress):
+ await executor.dispatch(CapabilityReference("authorization-ref-2"), _request())
+ assert invoker.calls == ["primary"]
diff --git a/tests/test_delegated_runtime.py b/tests/test_delegated_runtime.py
new file mode 100644
index 0000000..d98df91
--- /dev/null
+++ b/tests/test_delegated_runtime.py
@@ -0,0 +1,875 @@
+"""Durable delegated connector runtime tests without credential material."""
+
+from __future__ import annotations
+
+import asyncio
+import sqlite3
+from dataclasses import dataclass, replace
+from datetime import UTC, datetime, timedelta
+
+import pytest
+
+from baton.credential_custody import (
+ AuthorizedCredentialUse,
+ ConsumptionReservationRequired,
+ CredentialUseRequest,
+ CustodyAuditEvent,
+ CustodyAuditKind,
+ CustodyAuthorizationDenied,
+ CustodyResultStatus,
+ ProviderChannel,
+ ProviderCredentialHandle,
+ SanitizedCustodyOutcome,
+ VerifiedDelegatedAuthorization,
+ VerifiedWorkloadAuthorization,
+)
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ ConnectorRoute,
+ DeliveryOutcome,
+ DeliveryStatus,
+ DispatchBinding,
+ DispatchRequest,
+ DispatchSignal,
+ DispatchSignalKind,
+ DispatchStateUnavailable,
+ VerifiedDispatchGrant,
+ dispatch_request_fingerprint,
+)
+from baton.delegated_runtime import (
+ ConfiguredVerifierBundle,
+ DelegatedAuthorizationContext,
+ DelegatedAuthorizationVerifier,
+ DelegatedConnectorRuntime,
+ DelegatedRuntimeComponents,
+ SinglePurposeCustodiedResolver,
+ SqliteDelegatedRuntimeState,
+)
+
+NOW = datetime(2026, 6, 4, tzinfo=UTC)
+
+
+def _fingerprint(
+ idempotency_key: str = "dispatch-once-1",
+ *,
+ dispatch_id: str | None = None,
+) -> str:
+ return dispatch_request_fingerprint(
+ dispatch_id=dispatch_id or f"dispatch-{idempotency_key}",
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ )
+
+
+FINGERPRINT = _fingerprint()
+
+
+@dataclass
+class MutableClock:
+ now: datetime = NOW
+
+ def __call__(self) -> datetime:
+ return self.now
+
+
+def _route() -> ConnectorRoute:
+ return ConnectorRoute(
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ channel=Channel.SMS,
+ credential_handle="handle-sms-primary-v3",
+ priority=1,
+ )
+
+
+def _handle() -> ProviderCredentialHandle:
+ return ProviderCredentialHandle(
+ handle_id="handle-sms-primary-v3",
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ channel=ProviderChannel.SMS,
+ version_ref="active-v3",
+ )
+
+
+def _request(
+ *,
+ idempotency_key: str = "dispatch-once-1",
+) -> DispatchRequest:
+ dispatch_id = f"dispatch-{idempotency_key}"
+ return DispatchRequest(
+ dispatch_id=dispatch_id,
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ request_fingerprint=_fingerprint(idempotency_key, dispatch_id=dispatch_id),
+ )
+
+
+def _custody_request(
+ *,
+ idempotency_key: str = "dispatch-once-1",
+ fingerprint: str | None = None,
+ claim_id: str = "claim-1",
+) -> CredentialUseRequest:
+ dispatch_id = f"dispatch-{idempotency_key}"
+ return CredentialUseRequest(
+ operation_id="operation-1",
+ dispatch_id=dispatch_id,
+ workload_id="mea-comms",
+ connector_id="sms-primary",
+ channel=ProviderChannel.SMS,
+ purpose="case_notification",
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ dispatch_claim_id=claim_id,
+ request_fingerprint=fingerprint
+ or _fingerprint(idempotency_key, dispatch_id=dispatch_id),
+ )
+
+
+def _delivery(dispatch_id: str = "dispatch-dispatch-once-1") -> DeliveryOutcome:
+ return DeliveryOutcome(
+ dispatch_id=dispatch_id,
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ status=DeliveryStatus.ACCEPTED,
+ attempt_count=1,
+ failover_used=False,
+ audit_ref="audit-provider-accepted",
+ )
+
+
+def _authorized_use(
+ *,
+ claim_id: str,
+ idempotency_key: str = "dispatch-once-1",
+ fingerprint: str = FINGERPRINT,
+ reservation_id: str | None = None,
+) -> AuthorizedCredentialUse:
+ return AuthorizedCredentialUse(
+ authorization_id=f"authorization-{idempotency_key}",
+ handle_id="handle-sms-primary-v3",
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ operation_id="operation-1",
+ dispatch_id=f"dispatch-{idempotency_key}",
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key=idempotency_key,
+ request_fingerprint=fingerprint,
+ dispatch_claim_id=claim_id,
+ reservation_id=reservation_id or f"reservation-{idempotency_key}",
+ )
+
+
+class DispatchVerifier:
+ def __init__(self):
+ self.contexts: list[DelegatedAuthorizationContext] = []
+
+ async def verify(
+ self,
+ _reference,
+ request: DispatchRequest,
+ context: DelegatedAuthorizationContext,
+ ) -> VerifiedDelegatedAuthorization:
+ self.contexts.append(context)
+ return VerifiedDelegatedAuthorization(
+ principal="mea-comms",
+ channel=request.channel,
+ allowed_connectors=frozenset({"sms-primary"}),
+ not_after=NOW + timedelta(minutes=10),
+ max_attempts=1,
+ request_fingerprint=request.request_fingerprint,
+ authorization_id=f"authorization-{request.idempotency_key}",
+ issuer="signet://issuer/mea",
+ audience=context.audience,
+ issuer_policy_ref=context.issuer_policy_ref,
+ rotation_policy_ref=context.rotation_policy_ref,
+ allowed_purposes=frozenset({"case_notification"}),
+ not_before=NOW - timedelta(minutes=1),
+ max_uses=1,
+ )
+
+
+class CustodyVerifier:
+ async def verify(
+ self,
+ _reference,
+ request: CredentialUseRequest,
+ ) -> VerifiedWorkloadAuthorization:
+ return VerifiedWorkloadAuthorization(
+ authorization_id=f"authorization-{request.idempotency_key}",
+ workload_id=request.workload_id,
+ allowed_connector_ids=frozenset({"sms-primary"}),
+ allowed_channels=frozenset({ProviderChannel.SMS}),
+ allowed_purposes=frozenset({"case_notification"}),
+ request_fingerprint=request.request_fingerprint,
+ not_before=NOW - timedelta(minutes=1),
+ not_after=NOW + timedelta(minutes=10),
+ max_uses=1,
+ max_provider_attempts=1,
+ )
+
+
+class RebindingDispatchVerifier(DispatchVerifier):
+ def __init__(self, **replacements):
+ super().__init__()
+ self.replacements = replacements
+
+ async def verify(
+ self,
+ reference,
+ request: DispatchRequest,
+ context: DelegatedAuthorizationContext,
+ ) -> VerifiedDelegatedAuthorization:
+ outcome = await super().verify(reference, request, context)
+ return replace(outcome, **self.replacements)
+
+
+class IndependentDispatchVerifier:
+ async def verify(
+ self,
+ _reference,
+ request: DispatchRequest,
+ _context: DelegatedAuthorizationContext,
+ ) -> VerifiedDispatchGrant:
+ return VerifiedDispatchGrant(
+ principal="mea-comms",
+ channel=request.channel,
+ allowed_connectors=frozenset({"sms-primary"}),
+ not_after=NOW + timedelta(minutes=10),
+ max_attempts=1,
+ request_fingerprint=request.request_fingerprint,
+ )
+
+
+class StaticOperation:
+ def __init__(self, outcome: SanitizedCustodyOutcome):
+ self.outcome = outcome
+ self.calls = 0
+
+ async def invoke(self) -> SanitizedCustodyOutcome:
+ self.calls += 1
+ return self.outcome
+
+
+class OperationFactory:
+ def __init__(self, outcome: SanitizedCustodyOutcome):
+ self.outcome = outcome
+ self.prepared = []
+ self.operations: list[StaticOperation] = []
+
+ async def prepare(self, handle, authorized_use) -> StaticOperation:
+ self.prepared.append((handle, authorized_use))
+ operation = StaticOperation(self.outcome)
+ self.operations.append(operation)
+ return operation
+
+
+class CompleteFailingLedger:
+ def __init__(self, ledger):
+ self._ledger = ledger
+
+ async def reserve_once(self, authorization, request):
+ return await self._ledger.reserve_once(authorization, request)
+
+ async def reserve_attempt(self, reservation, authorized_use):
+ return await self._ledger.reserve_attempt(reservation, authorized_use)
+
+ async def complete(self, _reservation, _outcome):
+ raise RuntimeError("injected durable outcome persistence failure")
+
+
+class OutcomeFailingAuditSink:
+ def __init__(self, sink):
+ self._sink = sink
+
+ async def emit(self, event):
+ if event.kind in (
+ CustodyAuditKind.INVOCATION_COMPLETED,
+ CustodyAuditKind.INVOCATION_FAILED,
+ ):
+ raise RuntimeError("injected durable custody audit failure")
+ await self._sink.emit(event)
+
+
+def _accepted_outcome() -> SanitizedCustodyOutcome:
+ return SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-dispatch-once-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.ACCEPTED,
+ audit_ref="audit-provider-accepted",
+ )
+
+
+def _runtime(
+ tmp_path,
+ operation_factory,
+ *,
+ clock=None,
+ verifier: DelegatedAuthorizationVerifier | None = None,
+) -> DelegatedConnectorRuntime:
+ return DelegatedConnectorRuntime.build_sqlite_reference(
+ routes=[_route()],
+ handles={"sms-primary": _handle()},
+ verifiers=ConfiguredVerifierBundle(
+ verifier=verifier or DispatchVerifier(),
+ audience="baton://delegated-provider-executor",
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=3,
+ authorization_lifetime_ceiling_seconds=15 * 60,
+ ),
+ operation_factory=operation_factory,
+ state_path=tmp_path / "delegated-runtime.sqlite3",
+ workload_id="mea-comms",
+ purpose="case_notification",
+ clock=clock,
+ )
+
+
+async def test_runtime_verifies_once_and_binds_exact_policy_context(tmp_path):
+ verifier = DispatchVerifier()
+ factory = OperationFactory(_accepted_outcome())
+ runtime = _runtime(tmp_path, factory, clock=lambda: NOW, verifier=verifier)
+
+ await runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ assert verifier.contexts == [
+ DelegatedAuthorizationContext(
+ audience="baton://delegated-provider-executor",
+ workload_id="mea-comms",
+ purpose="case_notification",
+ available_connector_ids=frozenset({"sms-primary"}),
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=3,
+ authorization_lifetime_ceiling_seconds=15 * 60,
+ )
+ ]
+ assert len(factory.prepared) == 1
+
+
+@pytest.mark.parametrize(
+ "replacements",
+ [
+ {"audience": "baton://other-executor"},
+ {"issuer_policy_ref": "signet://issuer-policy/other"},
+ {"rotation_policy_ref": "signet://rotation-policy/other"},
+ {"principal": "other-workload"},
+ {"allowed_connectors": frozenset({"sms-primary", "sms-unapproved"})},
+ {"allowed_purposes": frozenset({"other_purpose"})},
+ {"allowed_purposes": frozenset({"case_notification", "other_purpose"})},
+ {"not_before": NOW + timedelta(minutes=1)},
+ {"max_attempts": 4},
+ {"not_after": NOW + timedelta(minutes=16)},
+ ],
+)
+async def test_runtime_rejects_rebound_verified_context_before_provider_use(
+ tmp_path,
+ replacements,
+):
+ factory = OperationFactory(_accepted_outcome())
+ runtime = _runtime(
+ tmp_path,
+ factory,
+ clock=lambda: NOW,
+ verifier=RebindingDispatchVerifier(**replacements),
+ )
+
+ with pytest.raises(AuthorizationDenied):
+ await runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ assert factory.prepared == []
+
+
+async def test_runtime_rejects_independent_dispatch_only_verifier_outcome(tmp_path):
+ factory = OperationFactory(_accepted_outcome())
+ runtime = _runtime(
+ tmp_path,
+ factory,
+ clock=lambda: NOW,
+ verifier=IndependentDispatchVerifier(),
+ )
+
+ with pytest.raises(AuthorizationDenied):
+ await runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ assert factory.prepared == []
+
+
+async def test_runtime_replays_completed_dispatch_without_second_provider_operation(tmp_path):
+ first_factory = OperationFactory(_accepted_outcome())
+ first_runtime = _runtime(tmp_path, first_factory, clock=lambda: NOW)
+
+ first = await first_runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ second_factory = OperationFactory(_accepted_outcome())
+ second_runtime = _runtime(tmp_path, second_factory, clock=lambda: NOW)
+ second = await second_runtime.executor.dispatch(
+ CapabilityReference("another-opaque-ref"),
+ _request(),
+ )
+
+ assert first == second
+ assert len(first_factory.prepared) == 1
+ assert second_factory.prepared == []
+ assert not hasattr(first_factory.prepared[0][1], "material")
+ assert second_runtime.reference_state is not None
+ verification = await second_runtime.reference_state.verify_event_chain()
+ assert verification.ok is True
+ assert verification.event_count >= 3
+
+
+async def test_stale_claim_is_reclaimed_and_old_worker_cannot_complete(tmp_path):
+ clock = MutableClock()
+ state = SqliteDelegatedRuntimeState(
+ tmp_path / "runtime.sqlite3",
+ claim_lease_seconds=5,
+ clock=clock,
+ )
+ binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+ first_claim = await state.journal.begin(binding)
+ assert first_claim is not None
+
+ clock.now += timedelta(seconds=6)
+ second_claim = await state.journal.begin(binding)
+ assert second_claim is not None
+ assert second_claim.claim_id != first_claim.claim_id
+
+ with pytest.raises(DispatchStateUnavailable):
+ await state.journal.complete(first_claim, _delivery())
+
+ await state.journal.complete(second_claim, _delivery())
+ assert await state.journal.completed(binding) == _delivery()
+
+
+async def test_sqlite_reference_serializes_competing_claims_and_attempts(tmp_path):
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+
+ claims = await asyncio.gather(
+ state.journal.begin(binding),
+ state.journal.begin(binding),
+ )
+ active_claims = [claim for claim in claims if claim is not None]
+ assert len(active_claims) == 1
+ assert claims.count(None) == 1
+
+ claim = active_claims[0]
+ request = _custody_request(claim_id=claim.claim_id)
+ authorization = await CustodyVerifier().verify(None, request)
+ reservation = await state.ledger.reserve_once(authorization, request)
+ use = _authorized_use(
+ claim_id=claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ )
+
+ attempts = await asyncio.gather(
+ state.ledger.reserve_attempt(reservation, use),
+ state.ledger.reserve_attempt(reservation, use),
+ return_exceptions=True,
+ )
+ assert attempts.count(1) == 1
+ assert sum(isinstance(result, CustodyAuthorizationDenied) for result in attempts) == 1
+
+
+async def test_expired_claim_is_inactive_before_another_worker_reclaims_it(tmp_path):
+ clock = MutableClock()
+ state = SqliteDelegatedRuntimeState(
+ tmp_path / "runtime.sqlite3",
+ claim_lease_seconds=5,
+ clock=clock,
+ )
+ binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+ claim = await state.journal.begin(binding)
+ assert claim is not None
+ request = _custody_request(claim_id=claim.claim_id)
+ authorization = await CustodyVerifier().verify(None, request)
+ reservation = await state.ledger.reserve_once(authorization, request)
+
+ clock.now += timedelta(seconds=6)
+
+ with pytest.raises(DispatchStateUnavailable, match="no longer active"):
+ await state.journal.renew(claim)
+ with pytest.raises(DispatchStateUnavailable, match="no longer active"):
+ await state.journal.complete(claim, _delivery())
+ with pytest.raises(ConsumptionReservationRequired, match="active exact"):
+ await state.ledger.reserve_once(authorization, request)
+ with pytest.raises(ConsumptionReservationRequired, match="active exact"):
+ await state.ledger.reserve_attempt(
+ reservation,
+ _authorized_use(
+ claim_id=claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ ),
+ )
+
+
+async def test_durable_ledger_reuses_exact_reservation_and_rejects_rebinding(tmp_path):
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ first_binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+ first_claim = await state.journal.begin(first_binding)
+ assert first_claim is not None
+ first_request = _custody_request(claim_id=first_claim.claim_id)
+ first_authorization = await CustodyVerifier().verify(None, first_request)
+
+ first = await state.ledger.reserve_once(first_authorization, first_request)
+ replay = await state.ledger.reserve_once(first_authorization, first_request)
+ assert replay == first
+
+ other_fingerprint = _fingerprint("dispatch-once-2")
+ other_binding = DispatchBinding("dispatch-once-2", other_fingerprint)
+ other_claim = await state.journal.begin(other_binding)
+ assert other_claim is not None
+ other_request = _custody_request(
+ idempotency_key="dispatch-once-2",
+ fingerprint=other_fingerprint,
+ claim_id=other_claim.claim_id,
+ )
+ rebound_authorization = VerifiedWorkloadAuthorization(
+ authorization_id=first_authorization.authorization_id,
+ workload_id=other_request.workload_id,
+ allowed_connector_ids=frozenset({"sms-primary"}),
+ allowed_channels=frozenset({ProviderChannel.SMS}),
+ allowed_purposes=frozenset({"case_notification"}),
+ request_fingerprint=other_fingerprint,
+ not_before=NOW - timedelta(minutes=1),
+ not_after=NOW + timedelta(minutes=10),
+ max_uses=1,
+ max_provider_attempts=1,
+ )
+ with pytest.raises(CustodyAuthorizationDenied):
+ await state.ledger.reserve_once(rebound_authorization, other_request)
+
+
+async def test_provider_attempt_budget_survives_dispatch_claim_recovery(tmp_path):
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+ first_claim = await state.journal.begin(binding)
+ assert first_claim is not None
+ request = _custody_request(claim_id=first_claim.claim_id)
+ authorization = await CustodyVerifier().verify(None, request)
+ reservation = await state.ledger.reserve_once(authorization, request)
+ first_use = _authorized_use(
+ claim_id=first_claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ )
+
+ with pytest.raises(ConsumptionReservationRequired, match="does not bind"):
+ await state.ledger.reserve_attempt(
+ reservation,
+ _authorized_use(
+ claim_id=first_claim.claim_id,
+ idempotency_key="dispatch-once-2",
+ fingerprint="b" * 64,
+ reservation_id=reservation.reservation_id,
+ ),
+ )
+ assert await state.ledger.reserve_attempt(reservation, first_use) == 1
+ await state.journal.abort(first_claim)
+
+ second_claim = await state.journal.begin(binding)
+ assert second_claim is not None
+ replay_request = _custody_request(claim_id=second_claim.claim_id)
+ replay_reservation = await state.ledger.reserve_once(authorization, replay_request)
+ second_use = _authorized_use(
+ claim_id=second_claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ )
+
+ assert replay_reservation == reservation
+ with pytest.raises(CustodyAuthorizationDenied, match="budget is exhausted"):
+ await state.ledger.reserve_attempt(replay_reservation, second_use)
+
+
+async def test_stale_claim_cannot_reserve_authorization_or_provider_attempt(tmp_path):
+ clock = MutableClock()
+ state = SqliteDelegatedRuntimeState(
+ tmp_path / "runtime.sqlite3",
+ claim_lease_seconds=5,
+ clock=clock,
+ )
+ binding = DispatchBinding("dispatch-once-1", FINGERPRINT)
+ first_claim = await state.journal.begin(binding)
+ assert first_claim is not None
+ first_request = _custody_request(claim_id=first_claim.claim_id)
+ authorization = await CustodyVerifier().verify(None, first_request)
+ reservation = await state.ledger.reserve_once(authorization, first_request)
+
+ clock.now += timedelta(seconds=6)
+ second_claim = await state.journal.begin(binding)
+ assert second_claim is not None
+
+ with pytest.raises(ConsumptionReservationRequired, match="active exact"):
+ await state.ledger.reserve_once(authorization, first_request)
+ with pytest.raises(ConsumptionReservationRequired, match="active exact"):
+ await state.ledger.reserve_attempt(
+ reservation,
+ _authorized_use(
+ claim_id=first_claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ ),
+ )
+
+ assert await state.ledger.reserve_attempt(
+ reservation,
+ _authorized_use(
+ claim_id=second_claim.claim_id,
+ reservation_id=reservation.reservation_id,
+ ),
+ ) == 1
+
+
+async def test_hash_chain_detects_tampering(tmp_path):
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ await state.sink.emit(
+ DispatchSignal(
+ kind=DispatchSignalKind.ATTEMPT_SUCCEEDED,
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ dispatch_id="dispatch-1",
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ attempt_count=1,
+ )
+ )
+ assert (await state.verify_event_chain()).ok is True
+
+ with sqlite3.connect(state.path) as connection:
+ connection.execute(
+ "UPDATE operational_events SET payload_json = ? WHERE sequence = 1",
+ ('{"tampered":true}',),
+ )
+
+ verification = await state.verify_event_chain()
+ assert verification.ok is False
+ assert verification.broken_sequence == 1
+
+
+async def test_failure_notifications_are_durable_and_acknowledgeable(tmp_path):
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ event = CustodyAuditEvent(
+ kind=CustodyAuditKind.INVOCATION_FAILED,
+ operation_id="operation-1",
+ dispatch_id="dispatch-1",
+ workload_id="mea-comms",
+ connector_id="sms-primary",
+ provider_key="provider-primary",
+ authorization_id="authorization-1",
+ failure_code="provider_timeout",
+ )
+
+ await state.sink.notify(event)
+ await state.sink.notify(event)
+ pending = await state.pending_failure_notifications()
+
+ assert len(pending) == 1
+ assert pending[0]["payload"]["failure_code"] == "provider_timeout"
+ assert await state.acknowledge_failure_notification(pending[0]["notification_key"])
+ assert await state.pending_failure_notifications() == []
+ assert (await state.verify_event_chain()).ok is True
+
+
+async def test_runtime_failure_persists_sanitized_notification(tmp_path):
+ factory = OperationFactory(
+ SanitizedCustodyOutcome(
+ operation_id="operation-1",
+ dispatch_id="dispatch-dispatch-once-1",
+ provider_key="provider-primary",
+ status=CustodyResultStatus.FAILED,
+ audit_ref="audit-provider-timeout",
+ failure_code="provider_timeout",
+ )
+ )
+ runtime = _runtime(tmp_path, factory, clock=lambda: NOW)
+
+ outcome = await runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ assert outcome.status is DeliveryStatus.EXHAUSTED
+ assert outcome.failure_code == "provider_timeout"
+ assert runtime.reference_state is not None
+ pending = await runtime.reference_state.pending_failure_notifications()
+ assert len(pending) == 1
+ assert pending[0]["payload"]["failure_code"] == "provider_timeout"
+ assert "recipient-ref-1" not in str(pending)
+ assert "payload-ref-1" not in str(pending)
+
+
+@pytest.mark.parametrize("failure_mode", ["ledger", "audit"])
+async def test_post_provider_state_failure_is_non_retryable_and_does_not_resend(
+ tmp_path,
+ failure_mode,
+):
+ factory = OperationFactory(_accepted_outcome())
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=lambda: NOW)
+ ledger = CompleteFailingLedger(state.ledger) if failure_mode == "ledger" else state.ledger
+ audit_sink = OutcomeFailingAuditSink(state.sink) if failure_mode == "audit" else state.sink
+ runtime = DelegatedConnectorRuntime.compose(
+ routes=[_route()],
+ handles={"sms-primary": _handle()},
+ verifiers=ConfiguredVerifierBundle(
+ verifier=DispatchVerifier(),
+ audience="baton://delegated-provider-executor",
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=3,
+ authorization_lifetime_ceiling_seconds=15 * 60,
+ ),
+ operation_factory=factory,
+ components=DelegatedRuntimeComponents(
+ journal=state.journal,
+ ledger=ledger,
+ signal_sink=state.sink,
+ audit_sink=audit_sink,
+ failure_notifier=state.sink,
+ claim_lease_seconds=60,
+ ),
+ workload_id="mea-comms",
+ purpose="case_notification",
+ clock=lambda: NOW,
+ )
+
+ outcome = await runtime.executor.dispatch(
+ CapabilityReference("opaque-authorization-ref"),
+ _request(),
+ )
+
+ assert outcome.status is DeliveryStatus.EXHAUSTED
+ assert outcome.failure_code == "custody_state_unavailable"
+ assert outcome.attempt_count == 1
+ assert sum(operation.calls for operation in factory.operations) == 1
+ assert runtime.reference_state is None
+ pending = await state.pending_failure_notifications()
+ assert any(
+ notification["payload"]["failure_code"] == "custody_state_unavailable"
+ for notification in pending
+ )
+ assert "recipient-ref-1" not in str(pending)
+ assert "payload-ref-1" not in str(pending)
+
+
+async def test_single_purpose_resolver_returns_only_sanitized_outcome():
+ factory = OperationFactory(_accepted_outcome())
+ resolver = SinglePurposeCustodiedResolver(factory)
+ use = _authorized_use(claim_id="claim-1")
+
+ outcome = await resolver.invoke(_handle(), use)
+
+ assert outcome == _accepted_outcome()
+ assert not hasattr(factory.prepared[0][0], "material")
+ assert not hasattr(factory.prepared[0][1], "material")
+
+
+def test_runtime_builder_rejects_non_durable_or_incomplete_construction(tmp_path):
+ with pytest.raises(ValueError, match=":memory:"):
+ SqliteDelegatedRuntimeState(":memory:")
+
+ with pytest.raises(ValueError, match="missing exact custody handle"):
+ DelegatedConnectorRuntime.build_sqlite_reference(
+ routes=[_route()],
+ handles={},
+ verifiers=ConfiguredVerifierBundle(
+ verifier=DispatchVerifier(),
+ audience="baton://delegated-provider-executor",
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=3,
+ authorization_lifetime_ceiling_seconds=15 * 60,
+ ),
+ operation_factory=OperationFactory(_accepted_outcome()),
+ state_path=tmp_path / "runtime.sqlite3",
+ workload_id="mea-comms",
+ purpose="case_notification",
+ )
+
+ with pytest.raises(ValueError, match="claim lease must exceed"):
+ DelegatedConnectorRuntime.build_sqlite_reference(
+ routes=[_route()],
+ handles={"sms-primary": _handle()},
+ verifiers=ConfiguredVerifierBundle(
+ verifier=DispatchVerifier(),
+ audience="baton://delegated-provider-executor",
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=3,
+ authorization_lifetime_ceiling_seconds=15 * 60,
+ ),
+ operation_factory=OperationFactory(_accepted_outcome()),
+ state_path=tmp_path / "short-lease.sqlite3",
+ workload_id="mea-comms",
+ purpose="case_notification",
+ claim_lease_seconds=5,
+ )
+
+
+@pytest.mark.parametrize("operation", ["renew", "complete"])
+async def test_lock_wait_cannot_revive_expired_claim(tmp_path, monkeypatch, operation):
+ clock = MutableClock()
+ state = SqliteDelegatedRuntimeState(tmp_path / "runtime.sqlite3", clock=clock, claim_lease_seconds=1)
+ claim = await state.journal.begin(DispatchBinding("dispatch-once-1", FINGERPRINT))
+ original_connect = state._connect
+
+ class ConnectionAfterWait:
+ def __init__(self):
+ self.connection = original_connect()
+ self.waited = False
+
+ def execute(self, sql, *args):
+ # Model time passing while acquiring a write lock, without a flaky sleep.
+ if not self.waited and (sql == "BEGIN IMMEDIATE" or sql.lstrip().startswith("UPDATE")):
+ clock.now += timedelta(seconds=2)
+ self.waited = True
+ return self.connection.execute(sql, *args)
+
+ def __getattr__(self, name):
+ return getattr(self.connection, name)
+
+ monkeypatch.setattr(state, "_connect", ConnectionAfterWait)
+ with pytest.raises(DispatchStateUnavailable, match="no longer active"):
+ if operation == "renew":
+ await state.journal.renew(claim)
+ else:
+ await state.journal.complete(claim, _delivery())
+
+
+def test_runtime_rejects_misindexed_handle(tmp_path):
+ with pytest.raises(ValueError, match="exact custody handle"):
+ DelegatedConnectorRuntime.build_sqlite_reference(
+ routes=[_route()],
+ handles={"sms-primary": replace(_handle(), connector_id="sms-backup")},
+ verifiers=ConfiguredVerifierBundle(
+ verifier=DispatchVerifier(), audience="baton://executor",
+ issuer_policy_ref="issuer-policy", rotation_policy_ref="rotation-policy",
+ provider_attempt_ceiling=3, authorization_lifetime_ceiling_seconds=900,
+ ),
+ operation_factory=OperationFactory(_accepted_outcome()),
+ state_path=tmp_path / "runtime.sqlite3", workload_id="mea-comms", purpose="case_notification",
+ )
diff --git a/tests/test_signet_delegated_authorization.py b/tests/test_signet_delegated_authorization.py
new file mode 100644
index 0000000..2358f20
--- /dev/null
+++ b/tests/test_signet_delegated_authorization.py
@@ -0,0 +1,194 @@
+"""Key-free contract checks for the trusted Signet-to-Baton adapter."""
+
+from __future__ import annotations
+
+from dataclasses import replace
+from datetime import UTC, datetime, timedelta
+
+import pytest
+
+from baton.delegated_connector import (
+ AuthorizationDenied,
+ CapabilityReference,
+ Channel,
+ DispatchRequest,
+ dispatch_request_fingerprint,
+)
+from baton.delegated_runtime import DelegatedAuthorizationContext
+from baton.signet_delegated_authorization import (
+ SignetDelegatedAuthorizationAdapter,
+ SignetDelegatedProviderVerificationContext,
+ VerifiedSignetDelegatedProviderAuthorization,
+)
+
+NOW = datetime(2026, 6, 4, 22, 5, tzinfo=UTC)
+FINGERPRINT = dispatch_request_fingerprint(
+ dispatch_id="dispatch-1",
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key="dispatch-once-1",
+)
+
+
+def _request() -> DispatchRequest:
+ return DispatchRequest(
+ dispatch_id="dispatch-1",
+ workflow_id="operation-1",
+ channel=Channel.SMS,
+ recipient_ref="recipient-ref-1",
+ payload_ref="payload-ref-1",
+ idempotency_key="dispatch-once-1",
+ request_fingerprint=FINGERPRINT,
+ )
+
+
+def _runtime_context() -> DelegatedAuthorizationContext:
+ return DelegatedAuthorizationContext(
+ audience="baton://delegated-provider-executor",
+ workload_id="mea-comms",
+ purpose="case_notification",
+ available_connector_ids=frozenset({"sms-primary", "sms-backup"}),
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=2,
+ authorization_lifetime_ceiling_seconds=10 * 60,
+ )
+
+
+def _outcome() -> VerifiedSignetDelegatedProviderAuthorization:
+ return VerifiedSignetDelegatedProviderAuthorization(
+ authorization_id="authorization-1",
+ issuer="signet://issuer/mea",
+ audience="baton://delegated-provider-executor",
+ workload_id="mea-comms",
+ issued_at=(NOW - timedelta(minutes=5)).isoformat(),
+ not_before=(NOW - timedelta(minutes=4)).isoformat(),
+ not_after=(NOW + timedelta(minutes=5)).isoformat(),
+ channel="sms",
+ allowed_connector_ids=("sms-primary", "sms-backup"),
+ allowed_purposes=("case_notification",),
+ request_fingerprint=FINGERPRINT,
+ max_uses=1,
+ max_provider_attempts=2,
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ )
+
+
+class StaticSignetClient:
+ def __init__(self, outcome: VerifiedSignetDelegatedProviderAuthorization):
+ self.outcome = outcome
+ self.calls: list[tuple[str, SignetDelegatedProviderVerificationContext]] = []
+
+ async def verify(
+ self,
+ reference: str,
+ context: SignetDelegatedProviderVerificationContext,
+ ) -> VerifiedSignetDelegatedProviderAuthorization:
+ self.calls.append((reference, context))
+ return self.outcome
+
+
+def _adapter(client: StaticSignetClient) -> SignetDelegatedAuthorizationAdapter:
+ return SignetDelegatedAuthorizationAdapter(
+ client,
+ issuer="signet://issuer/mea",
+ clock=lambda: NOW,
+ )
+
+
+async def test_maps_one_trusted_signet_outcome_into_shared_baton_authorization():
+ client = StaticSignetClient(_outcome())
+ adapter = _adapter(client)
+
+ verified = await adapter.verify(
+ CapabilityReference("opaque-signet-authorization-ref"),
+ _request(),
+ _runtime_context(),
+ )
+
+ assert verified.authorization_id == "authorization-1"
+ assert verified.principal == "mea-comms"
+ assert verified.channel is Channel.SMS
+ assert verified.allowed_connectors == frozenset({"sms-primary", "sms-backup"})
+ assert verified.allowed_purposes == frozenset({"case_notification"})
+ assert verified.max_uses == 1
+ assert verified.max_attempts == 2
+ assert verified.issuer_policy_ref == "signet://issuer-policy/mea-comms"
+ assert verified.rotation_policy_ref == "signet://rotation-policy/mea-comms"
+ assert client.calls == [
+ (
+ "opaque-signet-authorization-ref",
+ SignetDelegatedProviderVerificationContext(
+ issuer="signet://issuer/mea",
+ audience="baton://delegated-provider-executor",
+ workload_id="mea-comms",
+ channel="sms",
+ available_connector_ids=("sms-backup", "sms-primary"),
+ purpose="case_notification",
+ request_fingerprint=FINGERPRINT,
+ issuer_policy_ref="signet://issuer-policy/mea-comms",
+ rotation_policy_ref="signet://rotation-policy/mea-comms",
+ provider_attempt_ceiling=2,
+ authorization_lifetime_ceiling_seconds=10 * 60,
+ ),
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "replacements",
+ [
+ {"issuer": "signet://issuer/other"},
+ {"audience": "baton://other-executor"},
+ {"workload_id": "other-workload"},
+ {"channel": "email"},
+ {"request_fingerprint": "b" * 64},
+ {"issuer_policy_ref": "signet://issuer-policy/other"},
+ {"rotation_policy_ref": "signet://rotation-policy/other"},
+ {"max_uses": 2},
+ {"max_provider_attempts": 0},
+ {"max_provider_attempts": 3},
+ {"allowed_connector_ids": ("sms-primary", "sms-unapproved")},
+ {"allowed_connector_ids": ("sms-primary", "sms-primary")},
+ {"allowed_purposes": ("other_purpose",)},
+ {"allowed_purposes": ("case_notification", "other_purpose")},
+ {"allowed_purposes": ("case_notification", "case_notification")},
+ {"issued_at": (NOW + timedelta(minutes=1)).isoformat()},
+ {"not_before": (NOW + timedelta(minutes=1)).isoformat()},
+ {"not_after": NOW.isoformat()},
+ {"not_after": (NOW + timedelta(minutes=6)).isoformat()},
+ {"not_after": "not-a-time"},
+ ],
+)
+async def test_rejects_rebound_or_invalid_signet_outcome(replacements):
+ adapter = _adapter(StaticSignetClient(replace(_outcome(), **replacements)))
+
+ with pytest.raises(AuthorizationDenied):
+ await adapter.verify(
+ CapabilityReference("opaque-signet-authorization-ref"),
+ _request(),
+ _runtime_context(),
+ )
+
+
+async def test_accepts_signet_scope_narrower_than_runtime_connector_ceiling():
+ client = StaticSignetClient(replace(_outcome(), allowed_connector_ids=("sms-primary",)))
+ adapter = _adapter(client)
+
+ verified = await adapter.verify(
+ CapabilityReference("opaque-signet-authorization-ref"),
+ _request(),
+ _runtime_context(),
+ )
+
+ assert verified.allowed_connectors == frozenset({"sms-primary"})
+
+
+@pytest.mark.parametrize("budget", [True, 1.5, float("nan"), float("inf")])
+async def test_rejects_non_integer_provider_attempt_budget(budget):
+ client = StaticSignetClient(replace(_outcome(), max_provider_attempts=budget))
+ with pytest.raises(AuthorizationDenied):
+ await _adapter(client).verify(CapabilityReference("opaque-ref"), _request(), _runtime_context())