Stop a delegated agent from spending beyond its human-approved limit—at the receiving tool boundary.
Your IAM may authenticate the agent's service identity, and OpenTelemetry may report that its API call succeeded. Those facts do not by themselves verify that a three-hop agent handoff preserved the human's exact task authority. Conventional IAM/OPA can enforce this when properly configured; OpenLine's local reference gate checks the receiver's explicit bounds before calling the Python tool.
Run the local Fire Drill · Run the 60-second multi-hop replay · Evaluate an independent receiver against JWT · Guard a Python tool · OpenTelemetry integration · Evidence and limits
From a normal repository clone, the scripted local Fire Drill runs with the original independent receiver kit; no native Wallet/adoption-branch checkout is required. Exact instructions and independent verifier.
This is synthetic local HTTP/SQLite with public fixture keys and no live AI, merchant or payment. After the receiver learns Agent A is revoked, competent EdDSA JWT and OpenLine both refuse 4/4 protected attempts; both also allow one direct unguarded SQLite write. The intentionally weak shared-bearer arm is a teaching fixture, not a competent competitor. Signed decisions prove what the local receiver recorded, not payment settlement or completeness of all backend effects. No commercial or production superiority is established.
A manager authorizes a $500 refund for ticket 8842, bound to invoice_v1. The orchestrator's compressed handoff says to clear the $4,800 disputed balance. A billing sub-agent proposes a $4,800 payout.
sequenceDiagram
participant H as Human (Refund up to $500)
participant O as Orchestrator
participant S as Billing sub-agent
participant G as OpenLine Receipt Gate
participant P as Payment simulator
H->>O: Approve refund, ticket 8842, invoice_v1
O->>S: Resolve full disputed balance
S->>G: Propose $4,800 payout
Note over G: Verify route, cap and receiver current state
G-->>S: DENY (mandate:payment_limit_exceeded + mandate:target_not_allowed)
Note over P: Protected payout never executes
Two-run replay: An intentionally coarse service IAM rule lets the simulated payout return HTTP 200. The same proposal through real olp_gate.authorize DENIES it before the payment function executes. Separate tests confirm that a $4,800 refund also violates the amount cap and a proper $500 refund remains allowed. Two regression cases independently lower the verified manager-signed cap and the receiver's policy cap to $100: either one alone stops a $500 refund with a distinct refusal reason. The stricter limit always wins.
git clone https://github.com/terryncew/openline-receipt-gate.git
cd openline-receipt-gate
python -m pip install -e .
python examples/multi_hop_refund_drift.pyNo money is moved. The ordinary terminal replay is illustrative, not live SDK telemetry. To capture actual spans and signed provisional OpenTelemetry trace receipts, install the separate openline-otel package and run python examples/multi_hop_refund_drift.py --otel (instructions). Signed capture does not prove complete or truthful observation. We do not claim a fixed 2 ms latency or production-ready payment interception.
from olp_gate import authorize, payment_semantics
@authorize(
policy="refund_policy.json", # receiver-owned mandate and route
tool="process_refund",
target="refund://process",
semantics=payment_semantics("amount_cents"),
state_source=current_state, # receiver-owned, not model output
evidence_sources={"refund_authority": refund_authority},
)
def process_refund(amount_cents: int, customer_id: str):
return payment_api.refund(amount_cents, customer_id)The current_state and refund_authority callbacks must be controlled by the receiver (working example). This uses the real @authorize interface, not an invented decorator, and only protects paths actually wrapped by the gate. See the existing reference guide below for limits and receiver requirements.
| Item | Current state |
|---|---|
| Package | openline-receipt-gate |
| Version | 0.6.0rc6 |
| Python | 3.10 or newer |
| License | MIT |
| Maturity | Release candidate and reference implementation |
This repository is not a hosted authorization service or a production certification. A production deployment must own its keys, policies, evidence providers, replay state, and every route to the protected effect.
A worker proposes a $1 purchase. The receiver checks the owner's signed permission and the worker's signed purchase terms before making the decision. In a controlled offline test: APPROVED → owner revocation → CARD_PAUSED → new worker authorized → APPROVED. The three signed receipts can be verified independently.
104/104 required Linux checks passed. This is a synthetic, local proof—not a deployed Lithic integration, real payment, or official partnership. The next external test remains gated on provider callback matching and failure behavior.
Read the plain-English results, public evidence, and exact limitations · Passing CI · Unmerged PR #108
From a clean checkout:
git clone https://github.com/terryncew/openline-receipt-gate.git
cd openline-receipt-gate
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python examples/langgraph_refund_guard/demo.pyOn Windows PowerShell:
.venv\Scripts\Activate.ps1The demo protects a refund function:
| Proposed action | Result |
|---|---|
| Refund $25 under standing authority | Executes |
| Refund $500 without manager approval | QUARANTINE |
| Refund $500 after manager approval | Executes |
| Refund $1,000.01 despite approval | DENY |
The blocked calls never enter the refund function. The complete example and its policy are in examples/langgraph_refund_guard.
| If you need to... | Start here |
|---|---|
| Protect a Python or LangGraph tool in the same process | @authorize |
| Protect a fixed action at a separate receiver's HTTP boundary, with signed evidence | Independent receiver evaluation kit — matched JWT baseline; Wallet export not directly compatible |
| Turn existing receipts into a signed receiver decision | olp-gate decide |
| Carry one exact permission to another service or process | VerifiedCommitLedger |
| Check whether explicit decision state survived an agent handoff | Handoff Check |
| Keep sensitive action fields out of a portable audit receipt | Field-tier projections |
from olp_gate import AuthorizationBlocked, authorize, payment_semantics
def current_state(call):
customer_id = call.arguments["customer_id"]
return {
"customer_id": customer_id,
"customer_version": customer_versions[customer_id],
"manager_approved": customer_id in manager_approved,
}
def refund_authority(call):
amount = call.arguments["amount_cents"]
customer_id = call.arguments["customer_id"]
if amount <= 5_000:
return {"basis": "standing_under_50_rule"}
if customer_id in manager_approved:
return {"basis": "manager_approval"}
return None
@authorize(
policy="refund_policy.json",
tool="process_refund",
target="refund://process",
semantics=payment_semantics("amount_cents"),
state_source=current_state,
evidence_sources={"refund_authority": refund_authority},
)
def process_refund(amount_cents: int, customer_id: str):
return payment_api.refund(amount_cents, customer_id)
try:
process_refund(amount_cents=50_000, customer_id="C-1042")
except AuthorizationBlocked as blocked:
print(blocked.decision, blocked.reason_codes)The important boundary is ownership:
- The agent supplies the proposed tool arguments.
- The receiver supplies the policy, current-state callback, evidence callbacks, trusted keys, and replay state.
- Model output must never be wired into state_source or evidence_sources as trusted data.
- Arguments are frozen before evaluation. The executed call is rebuilt from those frozen values rather than caller-owned mutable objects.
For LangGraph, keep its decorator outside OpenLine's:
@tool
@authorize(...)
def consequential_tool(...):
...No LangGraph dependency is required by the gate itself. See docs/TOOL_ADAPTER.md for the adapter contract.
The guarded-tool policy has two parts:
| Part | Purpose |
|---|---|
| Mandate | Hard limits: agent, purpose, allowed action types, targets, amounts, delegation, and expiry |
| Permission policy | Evidence required for a particular tool and target, accepted issuers, freshness, and what to do when evidence is unavailable |
The request cannot add a trusted key, replace the receiver's policy, choose an executable verifier, or broaden its own mandate. Start with the working refund_policy.json.
For protected function calls, the normal outcomes are:
| Decision | Meaning | Function runs? |
|---|---|---|
| COMMIT | Required checks passed and the exact action was authorized | Yes |
| QUARANTINE | Required evidence is missing or the result is undecidable | No |
| DENY | A hard rule, integrity check, or trusted evidence check failed | No |
Two additional dispositions are used by specialized evaluation profiles:
- NO_BADGE: an evaluation claim lacks enough support.
- ROLLBACK_REQUEST: a signed request asks a declared external actuator to reverse an effect. It does not perform the rollback itself.
A valid signature proves who signed the bytes. It does not prove that the evidence was sufficient. Signature validity alone can still produce QUARANTINE.
Create a receiver signing key:
olp-gate keygen .secrets/gate.keyIssue a short-lived challenge bound to the expected source receipt:
olp-gate challenge state/sessions.json \
--run-id run-123 \
--session-id session-123 \
--source-hash <source-receipt-sha256> \
--ttl 300Evaluate a request using receiver-controlled policy and trust files:
olp-gate decide request.json \
--policy policy.json \
--trust trust.json \
--key .secrets/gate.key \
--issuer procurement-gate \
--ledger state/sessions.json \
--out receipts/decision_receipts.jsonlVerify the signed result independently:
olp-gate verify-decision receipts/decision_receipts.jsonl \
--gate-key "$TRUSTED_GATE_PUBLIC_KEY"
node verify-decision-node.mjs receipts/decision_receipts.jsonl \
--gate-key "$TRUSTED_GATE_PUBLIC_KEY"The trusted public key must come from receiver configuration, not from the receipt being checked. Repeat --gate-key only for an explicit key-rotation window.
A signed COMMIT is evidence of a decision. It becomes portable tool permission only when it also contains an exact Verified Commit authorization. That authorization binds the tool, target, settings hash, run, evidence, policy, expiry, and a receiver-held one-use code.
from olp_gate import VerifiedCommitLedger
result = VerifiedCommitLedger("state/commit-ledger.json").execute_once(
signed_decision,
exact_action,
one_use_code=receiver_held_code,
trusted_gate_keys=[receiver_gate_public_key],
executor=lambda: destination_tool(**exact_action["settings"]),
)
if not result["authorized"]:
raise PermissionError(result["reason_codes"])Keep the check and the side effect inside one entry point. Calling check_and_consume() separately creates a gap between approval and execution. See docs/VERIFIED_COMMIT.md.
| Input | What Receipt Gate checks | Extra dependency |
|---|---|---|
| OLP Wire Canon 0.1 | Payload hash, Ed25519 signature, receipt kind, and amendment continuity | None |
| Agent Receipts v0.1-v0.5 | Signature, profile, chain, issuer continuity, sequence, parents, and terminal marker | None |
| Pipelock ActionReceipt v1 | Official verifier result plus receiver policy; an upstream allow is evidence, never automatic permission | Pinned pipelock-verify source |
| Assay Evidence Contract v1 | Official bundle and Trust Basis result plus receiver policy | Assay v3.32.0 CLI |
| Legacy Receipt Gate v0.1.1 | Local hash-chain continuity | None |
Adapters preserve the source system's result. They do not silently upgrade a failed source claim, and compatibility does not grant OpenLine the source system's enforcement powers. Details: docs/COMPATIBILITY.md.
| Module | Use it for | Boundary |
|---|---|---|
| Handoff Check | Rebuild explicit decision state from Claude Code, Codex, or generic JSON/JSONL history | Ordinary assistant prose is not promoted to trusted rationale |
| Verified Commit | Bind one signed decision to one exact destination call | Local exactly-once authorization, not globally exactly-once side effects |
| x402 Transaction Airlock | Recheck exact payment state before settlement and release | Reference profile; receiver owns snapshot, settlement, confirmation, and release callbacks |
| Role-Confusion Consequence Gate | Block an unsupported effect even after an agent adopts an untrusted instruction | Does not classify or repair the prompt |
| Field-tier projections | Commit complete action parameters while exposing only receiver-declared policy attributes | The commitment is integrity, not secrecy or proof that a remote client projected honestly |
| Verified Model Swap and Continuation | Test whether decision-relevant state survives a model or agent handoff | Current public runs are deterministic fixtures, not live-provider proof |
OpenLine Jev Witness (Vercel) takes a real Jev judgment through Vercel AI
Gateway (typesafe-ai/jev), binds it as signed OpenLine evidence to the
exact proposed action, and lets receiver-owned authority and policy decide
whether the action proceeds. Jev produces a probability judgment; OpenLine
treats each judgment as evidence inside existing receiver-owned authority
and policy checks — the receiver enforces its own threshold on top of Jev
evidence, and revocation stops a previously-witnessed action.
Run the one-command demo with a Vercel AI Gateway key:
AI_GATEWAY_API_KEY=... python3 examples/jev_vercel_witness/demo.pyThe study lives on the study/jev-witness-vercel-001 branch under
experiments/jev-witness-vercel-001/ with a hashed preregistration and
frozen acceptance cases V1–V5. The 2026-09-20 run froze as
INCONCLUSIVE_PROVIDER_RUNTIME: the live call was refused (HTTP 403) because
the Vercel account needs a credit card on file before the gateway serves
model traffic — no Jev judgment was returned, so no claim is earned.
What is not claimed: Jev correctness; improved Jev calibration; any official integration with TypeSafe AI or Vercel; universal Jev safety; market demand; adoption; or production readiness.
Receipt Gate is useful only where it actually controls the effect:
- Every consequential route must pass through the gate. A bypass remains a bypass.
- The receiver must control policy files, trust configuration, signing keys, evidence providers, and replay storage.
- The default local runtime is for reference use. A production service should use appropriate protected key custody and shared atomic state.
- Local one-use consumption prevents replay inside that ledger. It does not guarantee globally exactly-once side effects across external systems.
- A crash after authorization consumption fails closed and requires a new authorization.
- Raw evidence is verified locally but excluded from the portable decision receipt. The receipt carries hashes, assessments, policy identity, bindings, reason codes, and the decision.
- A terminal receipt proves that the declared chain has an ending marker. It cannot prove that every real-world event was recorded.
Read docs/THREAT_MODEL.md and docs/CLAIM_BOUNDARY.md before making production claims.
Run the unit and adversarial tests:
python -m unittest discover -s tests -t . -vThe -t . matters: byte-frozen historical test modules are excluded
from current-development discovery by tests/init.py (their
source-closure assertions are obsolete once production evolves; their
historical verification runs via
python scripts/freeze_governance.py verify-all against reconstructed
frozen snapshot roots). Each closure's exact freeze commit is recorded as
an explicit retrieval locator in scripts/freeze_governance.py and every
reconstructed path is hash-verified against the untouched FREEZE manifest
before use, so verification works in shallow CI checkouts with no history
search (locate remains as a local diagnostic).
Run the complete release gate with the archived warning-time policy:
python scripts/verify_warning_time_release.py --release-check
python scripts/verify_manifest.pyThe full gate generates demonstration outputs. Run it in a disposable clean checkout if you do not want those files in your working tree.
The warning-time calibration profile is historically intact but expired. The wrapper accepts only that single archival condition. Any additional verifier error still fails closed.
The repository includes reproducible mechanism tests. They support narrow claims; they are not product certifications.
| Test | Question | Current result |
|---|---|---|
| Payment settlement ordering | Can a protected effect occur before a later settlement failure? | Yes, in the pinned official Python MCP wrapper; the matched airlock withheld release |
| Post-compromise action gate | Can receiver-owned evidence stop an unsupported effect after model compromise? | 13 frozen cases met their expected receiver-side outcomes |
| Early-warning benchmark | Did a synthetic trace metric create an intervention window? | Historical result preserved; calibration expired and has no live authority |
| One-use execution | Can one decision authorize one exact local execution? | Mutation, expiry, replay, and concurrent double use were blocked in the disclosed suite |
| Measurement routing | Can prediction choose the next assay without replacing physical measurement? | No promotable signal; sequence routing saved 0.8% versus the best fixed order, below the frozen bar |
| Path | Purpose |
|---|---|
| olp_gate/tool_adapter.py | @authorize function boundary |
| olp_gate/gateway.py | Receipt appraisal and signed decision creation |
| olp_gate/verified_commit.py | Exact one-use authorization and local atomic consumption |
| olp_gate/adapters.py | External receipt-format adapters |
| examples/ | Small runnable integrations |
| docs/ | Architecture, threat model, schemas, and module contracts |
| benchmarks/ | Frozen tests, protocols, and result artifacts |
| scripts/ | Independent verifiers and release checks |
The original context-manager API still emits an unsigned local hash chain:
from olp_gate import gate
with gate(
action_type="tool_call",
claim="Search customer records",
evidence_required=True,
) as receipt:
result = search_customer_records(query)
receipt.commit(result, evidence={"query_hash": "sha256:..."})Use @authorize or the proof-to-policy CLI for signed decisions and external evidence. The legacy wrapper remains for backward compatibility.
MIT — copyright 2026 Terrynce White.
See NOTICE for attribution notices (including the Apache-2.0 terms that continue to cover vendored OpenLine Wallet components), CITATION.cff for a ready-made software citation, and ATTRIBUTION.md for what the licenses require and do not require.
Proof travels. Permission belongs to the receiver.
Wallet is one part of a larger public stack. These are shipped in separate repositories, not bundled into this package:
- OpenLine Lite — the front door: receiver-owned verification, model handoffs, evidence-impact analysis, and a GitHub Action.
- OpenLine Receipt Gate — receiver-owned authorization for consequential Python and LangGraph tool calls, including the
@authorizefunction guard. - OpenLine Airlock — lets coding agents search for changes while keeping acceptance rules outside the agents.
- openline-otel — Python OpenTelemetry receipt capture and Evidence Gateway.
- openline-otel-js — JavaScript OpenTelemetry receipt capture with reciprocal Python/Node conformance.
- OpenLine Claim Graph — traces which accepted claims and decisions need reconsideration when upstream evidence changes.
- OLP Wire Canon — the portable byte-level receipt contract shared by producers and verifiers.
If you are new to OpenLine, start with the OpenLine Lite Start Here guide. You do not need to install the whole stack.