Providers and the credential boundary
The first protected action crosses a separate HTTP process with credentials and state unavailable to the protected agent. This is a local CrowdStrike-style mock, not a claim about the production CrowdStrike API. The shared runtime exposes its protected path through authenticated HTTP/MCP and human browser sessions.
Authority and wire contract
What a frozen expected version is worth depends on the vendor, and the deployment says which case a binding is in through its precondition class (ENFORCED_BY_PROVIDER, CHECKED_BY_CONNECTOR, ASSERTED_BY_EXECUTOR, UNAVAILABLE; an undeclared binding answers UNAVAILABLE, because a deployment that declared nothing has said nothing about the vendor). Both mocks offer a conditional write and refuse a mutation whose version has moved, which is ENFORCED_BY_PROVIDER. A vendor that offers none — Microsoft Graph returns no ETag for a user object and ignores If-Match — is CHECKED_BY_CONNECTOR: the connector reads the state, compares it with the composite the plan froze, and writes at most once, retaining both vendor responses and the measured gap between them. That is ExecBound's own check, with a time-of-check-to-time-of-use window it owns and reports rather than closes. The class is recorded on the execution row at dispatch and rendered beside the record, so nothing has to infer it from configuration that may since have changed. See the connector design and its evidence addendum.
The gateway derives ProviderCommand exclusively from committed frozen fields: tenant/account, shared canonical target, provider target ID, expected version, fixed action/operation, the action's normalized arguments, execution/operation IDs and intent/plan hashes. The provider re-checks the provider and provider operation against the production action catalog binding and re-normalizes the arguments before its transaction; a command that fails either is rejected with 400 and no journal. It never forwards caller claims, retry keys or arbitrary URLs. A digest binds the entire normalized command; echoed intent/plan hashes alone cannot hide a changed target or expected version.
The provider accepts exactly four POST paths. One dispatch route is registered per catalog binding of the configured provider, and the service refuses to start when the catalog binds an action its store cannot execute:
| Path | Credential | Result |
|---|---|---|
/v1/contain | Account dispatch credential | Conditional mutation or retained terminal evidence |
/v1/lift_containment | Account dispatch credential | Conditional mutation or retained terminal evidence |
/v1/journal | Account dispatch or control credential | Retained evidence; missing remains inconclusive |
/v1/cancel | Separate account control credential | Existing outcome or durable cancellation tombstone |
Account/tenant scope comes from the credential and must match the command. Agent credentials and dispatch credentials on cancellation are rejected. Duplicate authorization headers, alternate identity headers, query overrides, unknown/duplicate JSON fields, unsupported numbers and oversized bodies fail closed. No resource provisioning, failure injection, journal editing, reset or debug endpoint exists.
Provider transaction
The mock owns a separate SQLite file. A physical resource/version is keyed by tenant and canonical target; trusted account representations point to that resource. A new connection runs BEGIN IMMEDIATE, checks the operation journal, verifies the expected resource version, applies at most one mutation and writes the terminal journal before COMMIT. The gateway database is not used for this provider transaction. SQLite transaction semantics
contain sets containment and lift_containment clears it; either advances the version and records one mutation even when the state already holds, matching the identity mock. An EXECUTED endpoint.lift_isolation record carries endpoint_transition, with before and after EndpointState(contained), and is invalid evidence unless after.contained is false. That field is excluded from serialization when absent, and endpoint.isolate must not carry it: new contain journals keep the original endpoint wire contract byte for byte, so the retained fixtures verify unchanged.
The operation journal is append-only with update/delete triggers. Reusing an operation ID with the identical full command returns the retained result; a changed command conflicts. Expected-version mismatch records VERSION_MISMATCH with zero mutations. Unknown targets also receive terminal nonexecution evidence for that specific operation ID, preventing later replay after provisioning.
Cancellation uses the same write transaction. If dispatch committed first, cancellation returns its outcome. If cancellation committed first, a later dispatch returns CANCELLED_NOT_EXECUTED without touching the target. A missing journal lookup or another actor's current target state cannot release gateway impact. Resource re-provisioning belongs only to the private test harness; it cannot change the retained journal.
Evidence and gateway integration
After database commit, the provider signs the complete terminal record using fixed HMAC-SHA256, a key ID and a separate per-account evidence key. This authenticates evidence between trusted components; it is not public nonrepudiation. Preserve required historical verification keys when rotating configuration.
JournalVerifier validates strict structure, MAC, trusted key selection, account/tenant scope and exact complete command equality. Settlement retains the authenticated record and its digest. It distinguishes execution, expected-version rejection, cancellation and missing-target terminal rejection. A version mismatch becomes STALE_AUTHORIZATION; other negative proof becomes FAILED_NOT_EXECUTED. Positive evidence converts one reservation to confirmed impact without double charging.
dispatch_owned() consumes the gateway's one send attempt before HTTP I/O. HTTPX uses retries=0, follow_redirects=False, trust_env=False, explicit timeouts and bounded responses. Configured HTTPS origins use certificate verification; cleartext is restricted to literal loopback IPs. Credentials, paths and destinations cannot come from caller input. HTTPX transports, HTTPX environment configuration
Timeout, disconnect, invalid evidence or failed outcome/audit settlement leaves impact and target ownership unresolved. A worker cannot obtain another send attempt. reconcile_operation() requires current ADMIN authority and an audited reason before network access. After a missing/slow lookup, cancellation requires a fresh authority check and committed cancellation intent. No SQL transaction remains open during provider I/O. A plain “failed” assertion never replenishes capacity.
A vendor that co-signs nothing
Everything above describes a provider ExecBound configured and that shares an evidence key with it. Microsoft Graph does neither, so GraphConnector settles differently and the gateway holds a Connector family rather than one class. The protocol is stated as a structural Connector protocol in provider/connector.py: dispatch, lookup, cancel, rotate_keys, close, a verifier, and a config naming its tenant, account, provider, evidence ring and the secrets that authenticate it to its vendor.
A Graph connection is configured as GraphConnectionConfig, not ConnectionConfig: it has a directory, a client id and a client secret, and its origin is pinned in source so no operator can move it. Its evidence_keys sign and verify ExecBound's own record and authenticate nothing to Microsoft; one named member of that ring signs, and a rotation that drops it is refused whole. Deployment files declare these under graph_connectors, separately from the mock connectors.
GraphConnector.dispatch performs the read, computes the composite, compares it against the frozen version and writes at most once, then signs a ConnectorExchange recording the frozen and observed composites, the vendor's status and the measured window. ConnectorRecordVerifier checks that record binds this execution and returns it as terminal evidence. The signature is ExecBound's own and is never evidence Microsoft acted; every surface that renders one says whose it is. A binding declaring CHECKED_BY_CONNECTOR claims that comparison and that measured gap, and never atomicity.
The record admits EXECUTED, VERSION_MISMATCH and TARGET_NOT_FOUND, and refuses a cancellation claim. Anything else the exchange sees — a lost response, a refused write, an authentication failure — produces no record, and the execution is retained as uncertain. Graph publishes no journal, so lookup returns None and such an execution stays uncertain rather than settling on a re-read that could not tell this connector's own write from someone else's. A refusal the connector knows did not execute, such as a 403 on the write, is included in that: the verified vocabulary has no reason for a vendor refusing a write.
The measured window is established by the exchange and written at settlement, never at the dispatch attempt, which is committed before the connector performs anything.
What the live tenant established, 18 September 2026
The Graph connector ran against the authorized Entra lab tenant (#125 task 8), against a disposable user the founder's directory holds for this purpose. tests/test_graph_live_probe.py reproduces it and is skipped unless EXECBOUND_GRAPH_LIVE=1 is set with the execute registration's credential present, so CI never reaches a vendor.
The least permission that disables a user is User.EnableDisableAccount.All. The execute registration holds exactly that plus User.Read.All for the read half, and no directory role at all. User.ReadWrite.All is not required and is not granted. The issue's spelling was documentation until this ran; it is now confirmed against a real directory.
The composite recipe matches what Microsoft returns. $select=id,accountEnabled,userPrincipalName,onPremisesSyncEnabled answers with exactly those four fields and an @odata.context describing the response, which the reader drops before the allowlist sees it. No drift, and onPremisesSyncEnabled comes back null rather than absent for a cloud-only user, which is why the recipe admits None for it.
The write answers 204 with no body. The record carries the status the vendor returned rather than assuming one, which is what lets an EXECUTED record be held to a 2xx it actually saw.
Graph reads are not read-your-writes consistent, and staleness is not an early phase that passes. This is what the probe found that no documentation stated and no fake could show. Measured directly, with one token reused so that nothing else was in the way: 116 reads over 40 seconds following a 204, giving the sequence FFFFTFFFF... — a single stale read arriving 1.25 seconds after the write, in the middle of a run of correct ones. A stale answer is not an opening phase that ends; it is one replica answering at an arbitrary moment.
Three consequences, none of them conservatism:
GraphConnector.lookupreturnsNonebecause no number of reads establishes current state. A re-read cannot confirm the connector's own write even before the question of who else may have changed the account, so an uncertain Graph operation stays uncertain.- A frozen precondition can be stale before the dispatch begins, through nobody's fault, when the version was frozen from a lagging replica. The probe's first run did exactly that and was refused
STALE_AUTHORIZATION, and the module still re-issues on that refusal rather than pretending it does not happen. That is the kernel being right about a directory that had moved. - The measured window is a gap between two reads of a system that does not promise either is current. It is evidence a reader can weigh, and
CHECKED_BY_CONNECTORclaims nothing more.
Be careful how much of this you blame on the vendor. The probe was unstable for three rounds and only one cause was Microsoft's. The other two were the probe's own: it re-issued after a stale refusal under the same idempotency key, which binds to one execution and which admission correctly refused, and it minted a fresh OAuth token for every poll, four a second. Both were visible in an exception that was not read for several rounds. With a key per re-issue and one token reused, the module ran six times consecutively with no failure, in 13 to 81 seconds depending on how quickly the directory settled. Replication is real and is measured above; it was not the reason this was hard to get green.
A denied action reached the vendor not at all. The forbidden case is asserted rather than assumed: the connector's HTTP transport counts every request it makes, and a request denied by a forbid rule produced an empty list — no token request, no read, no write. The hard denial wins before a ticket exists.
Local service configuration
Use the locked Python environment. The mock factory reads EXECBOUND_MOCK_DATABASE_PATH and EXECBOUND_MOCK_ACCOUNTS_PATH. The accounts file is a JSON array of trusted records with tenant_id, account_id, dispatch_token_hash, control_token_hash, signing_key_hex and key_id. Generate independent random bearer secrets and a separate evidence key of at least 32 bytes; store only SHA-256 bearer hashes in the provider account file. Gateway ConnectionConfig holds the raw bearer secrets and trusted evidence keys. Keep both configurations in ignored private local data and outside agent authority.
Provision synthetic resources and account representations with the internal MockStore harness before starting the service, or start it through python -m execbound.boot with EXECBOUND_MOCK_RESOURCES_JSON set to a JSON list of {"tenant_id", "account_id", "provider_id", "canonical_target_id", "version"} entries (optional "contained" or "account_enabled" booleans), which provisions the store idempotently at every start. The same canonical identities and expected versions must be provisioned in the trusted gateway registry. These methods have no HTTP routes.
$env:EXECBOUND_MOCK_DATABASE_PATH = "$PWD\local-data\mock\provider.sqlite3"
$env:EXECBOUND_MOCK_ACCOUNTS_PATH = "$PWD\local-data\mock\accounts.json"
uv run uvicorn execbound.provider.mock_app:create_mock_app --factory --host 127.0.0.1 --port 8010 --no-access-log
tests/test_protected_path.py creates fresh private configurations and starts an actual separate provider process, so the full regression suite is reproducible without manual account setup. Tests inspect mutation counts and retained journals directly. They cover approval then one effect, forbids, agent credential rejection, version changes, lost responses/crashes, journal recovery, cancellation before delayed arrival, outcome-audit rollback, inverse changes and unsafe transport responses. Concurrent SQLite connections exercise first-operation and cancel/dispatch races; the gateway uses guarded local PostgreSQL.
These are controlled mock guarantees. HTTP/MCP parity, browser approval/CSRF and continuation MACs have separate surface tests; the separate identity provider reuses the protected protocol with explicit action contracts. Replay and the reproducible validation package are implemented. No provider or public gateway has been deployed to Supabase or an internet host.