|
| 1 | +<!-- |
| 2 | +SPDX-FileCopyrightText: Copyright (c) 2026 Axiru, Inc. |
| 3 | +SPDX-License-Identifier: Apache-2.0 |
| 4 | +--> |
| 5 | + |
| 6 | +# Supervisor Middleware Payment Gate |
| 7 | + |
| 8 | +> [!WARNING] |
| 9 | +> Supervisor middleware is a research preview. Its policy and service contracts may change without compatibility guarantees. Use it only to prototype and evaluate middleware integrations. |
| 10 | +
|
| 11 | +This example is a supervisor middleware for sandboxes whose agents hold Stripe credentials. OpenShell network policy decides whether the sandbox may reach `api.stripe.com` at all. This middleware decides whether a specific money-moving call should happen. |
| 12 | + |
| 13 | +It parses refunds, customer credits, transfers, payouts, dispute updates, and issuing approvals out of the Stripe request body, evaluates a deterministic policy (per-transfer ceiling, hold threshold, daily cap per sandbox, duplicate window for the same customer and charge, optional counterparty allowlist), and returns `DECISION_ALLOW` or `DECISION_DENY` in the `PRE_CREDENTIALS` phase. Reads and non-money endpoints pass through. |
| 14 | + |
| 15 | +On allow it writes a Stripe `Idempotency-Key` bound to the decision id, so a retry of the same decision cannot become a second refund. On deny it returns a reason code and a one-sentence rationale. A refund with no `amount` is a full refund in Stripe's API; the middleware denies it and asks for an explicit amount. After three denials from one sandbox in 24 hours it adds a `quarantine_recommended` finding with severity `critical`. Any internal error is a deny, and `policy.yaml` sets `on_error: fail_closed`. |
| 16 | + |
| 17 | +The evaluator is a pure function: policy, intent, prior decisions, and a timestamp in; verdict, reason codes, and a SHA-256 fingerprint out. There is no model in the decision path and the agent's free-text reason is recorded but never evaluated, so the decision cannot be talked into anything by a prompt. Same inputs, same fingerprint. |
| 18 | + |
| 19 | +Why a payment gate belongs here: most agent money losses have no attacker. A parsing bug that refunds a whole balance, a duplicate refund after a retry that looked like a failure, a loop of small refunds. The sandbox boundary sees the host; it does not see the amount. This example adds the amount. |
| 20 | + |
| 21 | +## Prerequisites |
| 22 | + |
| 23 | +Node.js 20 or later on the host for the middleware service. For the smoke run, the same prerequisites as the content guard example: `cargo`, `curl`, `jq`, `mise`, Docker or Podman, and the repository's mise tools. |
| 24 | + |
| 25 | +## Run the service |
| 26 | + |
| 27 | +```shell |
| 28 | +cd examples/supervisor-middleware-payment-gate |
| 29 | +npm install && npm run build |
| 30 | +AXIRU_MW_BIND=0.0.0.0:50052 npm start |
| 31 | +``` |
| 32 | + |
| 33 | +Bind to all host interfaces so a local containerized gateway and sandbox supervisor can reach it. |
| 34 | + |
| 35 | +Add the service registration to your local gateway TOML (see `gateway.toml.snippet`): |
| 36 | + |
| 37 | +```toml |
| 38 | +[[openshell.supervisor.middleware]] |
| 39 | +name = "axiru-payment-gate" |
| 40 | +grpc_endpoint = "http://host.openshell.internal:50052" |
| 41 | +allow_insecure_transport = true |
| 42 | +max_payload_bytes = 262144 |
| 43 | +timeout = "2s" |
| 44 | +``` |
| 45 | + |
| 46 | +Create a sandbox with the included policy: |
| 47 | + |
| 48 | +```shell |
| 49 | +openshell sandbox create --name support-agent --policy examples/supervisor-middleware-payment-gate/policy.yaml |
| 50 | +``` |
| 51 | + |
| 52 | +The policy allows only the listed Stripe money-moving endpoints and reads, routes every allowed call through the middleware, and fails closed. Adjust the thresholds under `network_middlewares.axiru-payment-gate.config` (values are minor units, so `50000` is 500.00 USD) and list the binaries your agent actually uses. |
| 53 | + |
| 54 | +## Try it from inside the sandbox |
| 55 | + |
| 56 | +Inside the sandbox, with a Stripe test key available to the agent: |
| 57 | + |
| 58 | +```shell |
| 59 | +# In policy: 45.00 USD refund, first one on this charge. Expect a normal Stripe response. |
| 60 | +curl -s https://api.stripe.com/v1/refunds -u "$STRIPE_KEY": -d charge=ch_test_1 -d amount=4500 -d customer=cus_test_1 |
| 61 | + |
| 62 | +# Same charge, same customer, inside the 30-day duplicate window. Expect the gateway to deny with reason_code AXIRU_DENY. |
| 63 | +curl -s https://api.stripe.com/v1/refunds -u "$STRIPE_KEY": -d charge=ch_test_1 -d amount=4500 -d customer=cus_test_1 |
| 64 | + |
| 65 | +# 250,000.00 USD transfer. Expect a deny with AMOUNT_EXCEEDS_TRANSFER_CEILING in the audit log metadata. |
| 66 | +curl -s https://api.stripe.com/v1/transfers -u "$STRIPE_KEY": -d amount=25000000 -d currency=usd -d destination=acct_test_x |
| 67 | +``` |
| 68 | + |
| 69 | +Every evaluation lands in the gateway audit log with `axiru.decision_id`, `axiru.verdict`, `axiru.policy`, `axiru.fingerprint`, and `axiru.reason_codes` metadata, and a finding of type `axiru.decision`. |
| 70 | + |
| 71 | +## Tests |
| 72 | + |
| 73 | +```shell |
| 74 | +npm test |
| 75 | +``` |
| 76 | + |
| 77 | +Covers the Stripe parser, pass-through for non-Stripe hosts, allow with idempotency pinning, duplicate denial inside the window, denial of an unspecified amount, and the quarantine signal after repeated denials. |
| 78 | + |
| 79 | +## Layout |
| 80 | + |
| 81 | +- `src/gate.ts`: types, the pure evaluator, and a small `Gate` class with a session ledger and optional hosted mode. |
| 82 | +- `src/stripe.ts`: maps Stripe API requests to payment intents. |
| 83 | +- `src/middleware.ts`: the evaluation logic, separated from gRPC so it can be unit tested. |
| 84 | +- `src/server.ts`, `src/cli.ts`: the `openshell.middleware.v1.SupervisorMiddleware` gRPC service. |
| 85 | +- `proto/`: vendored from this repository's `proto/` directory at the commit this example was added. |
| 86 | +- `policy.yaml`: reference sandbox policy. |
| 87 | + |
| 88 | +## Limits |
| 89 | + |
| 90 | +The local evaluator cannot see the original charge amount from a refund request, so the cumulative rule "refunds never exceed the charge" is only enforced when the middleware runs in hosted mode (set `AXIRU_API_KEY`) or when a Stripe read is placed in front of it. Only Stripe is mapped; adding a rail is one regex and one field mapping in `src/stripe.ts`. This example handles form-encoded and JSON request bodies for the listed endpoints and is not a general payments proxy. |
| 91 | + |
| 92 | +A maintained version of this middleware, plus the same gate as an MCP server and as plugins for other agent runtimes, lives at https://github.com/AxiruAI/axiru-gate. |
0 commit comments