Skip to content

Architecture

Marcus Richards edited this page Oct 7, 2026 · 2 revisions

Architecture

Stdlib-only Python. There are no runtime dependencies: the HTTP layer is http.server, not Flask. State lives in process memory.

 agent (LLM, agent/react.py)
   │  tools only (agent/tools.py): attempt_spend, deliver, check_permit,
   │  delegate_subpermit, check_approval, complete_approved_spend
   ▼
 SpendPipeline (permit/flow.py) ──► PermitStore (permit/permit.py)
   │   authorize (only caller)            gate, reserve, delegate, tighten,
   │                                       approvals, revoke
   ▼                                        │
 PayPalClient ◄── ReleaseVerifier ──────────┤ receipts
 (mock | sandbox)  (settlement/verifier.py) ▼
                   capture (only caller)   Ledger (permit/ledger.py)

Modules

Path Role
permit/permit.py PermitStore: grant, four-clause gate, delegation, tighten, approvals, e-stop, subtree revoke, carve release. No PayPal imports
permit/flow.py SpendPipeline: the only path from attempt to hold. Approval continuation, e-stop/cascade voiding
permit/ledger.py Hash-chained receipt log
settlement/verifier.py ReleaseVerifier: predicates, the only capture path, reconcile, retry cleanup
settlement/paypal_client.py PayPalClient interface + MockPayPalClient (call recording, fault injection)
settlement/sandbox_client.py SandboxPayPalClient: real REST against api-m.sandbox.paypal.com
server.py JSON service (see HTTP API Reference)
dashboard/ Live view, delegation graph, e-stop, revoke-cascade, cap tighten, approve/deny
agent/ ReAct loop + permit-bound tools
scenarios/sla_escrow.py Agent-to-agent SLA settlement scenario
trace.py HTTP smoke driver for server.py (free loopback port)
demo*.py Demos. See Running the Demos

Design rules visible in code

  • Rail swap is the only difference between mock and sandbox. The pipeline and verifier are shared.
  • Locks: a per-permit lock serializes reservations. Lock order is store → parent and child → ancestor, documented as cycle-free.
  • Counters, not replay: reserved_cents and captured_cents are fields on each Permit, updated under its lock. They are not recomputed from the ledger.
  • Idempotency keys: authorize {permit_id}:{auth_id}:authorize, capture {permit_id}:{auth_id}:capture.
  • Duck-typed approval signal: flow.py catches NeedsPayerApproval by class name, so the permit package does not import settlement exceptions.

Ports

Port Who
8741 server.py default (trace.py now uses a free port)
8471 Dashboard default (demo_six_beat.py, demo_delegation.py, standalone)
8474 demo_approval.py dashboard

Configuration

Env var Used by
PERMIT_PAYPAL_CLIENT_ID / PERMIT_PAYPAL_CLIENT_SECRET server.py --sandbox
PERMIT_PAYPAL_MERCHANT_ID Merchant binding (optional, recommended)
PERMIT_GROK_CLI LLM runner path for live agent demos. Required, no default

Clone this wiki locally