Skip to content

About

One-line receipt gate for risky AI agent actions: receiver-owned authorization in front of the call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

280 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenLine Receipt Gate

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

Fire Drill — an honest local revocation challenge

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.

Green trace. Broken authority.

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
Loading

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.py

No 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.

Protect the final Python function

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.


Status

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.

Latest evidence: offline payment-authorization preflight (October 2026)

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

Five-minute demo

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.py

On Windows PowerShell:

.venv\Scripts\Activate.ps1

The 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.

Choose the integration you need

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

Guard a Python function

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.

Policy model

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.

Decisions

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.

Command-line flow

Create a receiver signing key:

olp-gate keygen .secrets/gate.key

Issue 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 300

Evaluate 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.jsonl

Verify 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.

Portable one-use permission

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.

Supported evidence inputs

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.

Optional modules

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

Jev Witness via Vercel AI Gateway (study)

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.py

The 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.

Security boundaries

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.

Test the repository

Run the unit and adversarial tests:

python -m unittest discover -s tests -t . -v

The -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.py

The 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.

Evidence and limits

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

Project map

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

Legacy API

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.

License

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.


The rest of OpenLine

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 @authorize function 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.

About

One-line receipt gate for risky AI agent actions: receiver-owned authorization in front of the call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages