Skip to main content
Glama
AgentGuard

AgentGuard

by mohdqrm
README.md
# AgentGuard

**A safety gateway between AI agents and PayPal.**

An AI agent gets four guarded MCP tools and no PayPal credentials. Every call is authenticated, validated, risk-scored and checked against policy before anything reaches PayPal. Risky calls stop and wait for a human. Every step is written to a hash-linked audit log.

Built for the PayPal AI Hackathon. Runs against the **PayPal sandbox**, so no real money moves.

## The problem

Give an agent a PayPal API key and it can send any amount, to anyone, as many times as its loop decides to. There is no spending limit, no human in the loop, and no record of why it did what it did.

The usual answer is to tell the agent to behave. That is not a control — it is a request. An agent that has been argued out of its instructions by something it read in an invoice will send the money anyway.

AgentGuard takes a different line: **the agent is never trusted, and never holds the credentials.** It can ask. Something else decides.

## What it looks like

AgentGuard is not a separate dashboard you have to go and check. The console is injected straight into `sandbox.paypal.com` by a browser extension, so reviewing your agent's payments happens where the money already lives.

```
sandbox.paypal.com/myaccount/agentguard
├── Ask your agent        chat; it calls the guarded tools and reports back
├── Waiting for you       commands held for approval, with the reasons
├── Every command         full history, risk score, policy decision, errors
└── Audit log             the hash chain, verifiable in place
```

The agent you chat to runs on your own Claude subscription through Claude Code, so the demo costs nothing in API credits. Set `ANTHROPIC_API_KEY` instead and it switches to the Messages API.

## How it works

```
Claude agent ──MCP──▶ AgentGuard ──▶ PayPal sandbox
                          │
                     auth → validate → risk → policy
                          │
                   allow · block · ask a human
                          │
                    hash-linked audit log
```

The agent never holds PayPal credentials. It can only reach four tools, and each call runs the same gauntlet.

| Step | What it does |
|---|---|
| **Auth** | JWT with a role. Agents call tools; merchants approve. An agent cannot approve its own request. |
| **Validate** | Zod strict schemas. Unknown fields and malformed amounts are refused. |
| **Risk** | Rules and time windows: amount, first-time payee, requests per minute, 24h running total. Scores 0–100. |
| **Policy** | OPA (`policy/agentguard.rego`) returns `allow`, `approval` or `deny`. Fails closed: if OPA is unreachable, the command is blocked. |
| **Approve** | Risky commands wait for a human. The request hash is stored and re-checked on approval. |
| **Execute** | One PayPal call with an idempotency key. |
| **Audit** | Every state change appended to a SHA-256 hash chain. |

### Command states

```
received → evaluated ─┬→ blocked
                      ├→ allowed  ─┐
                      └→ pending ──┴→ approved → executing ─┬→ success
                                   └→ rejected              ├→ failed
                                                            └→ uncertain → reconciled
```

`uncertain` is the interesting one. If PayPal times out, AgentGuard does **not** retry, because a retry could pay twice. The command waits and is resolved by a signed webhook or by replaying the same idempotency key as a status query.

## Guarantees

- **No money without checks.** The agent has no PayPal credentials and no path to the adapter that skips the guard chain.
- **A convinced agent still cannot pay.** Persuading the agent is not the same as moving money. The hard cap denies, the first-time-payee rule holds, and the approval still has to come from a human holding a merchant token — whatever the agent has been talked into believing.
- **Approvals cannot be swapped.** The request is hashed when it goes to a human and re-hashed on approval. If the amount or payee changed in between, it is rejected.
- **No double payments.** Every execution carries an idempotency key. The uncertain path replays the same key rather than issuing a new payment.
- **Tamper-evident history.** The audit log is append-only and hash-linked, enforced in Postgres by a trigger. `GET /audit/verify` names the first broken entry.

## Run it

Needs Node 20+. These commands work the same on Windows, macOS and Linux.

```bash
npm install
npm run setup
```

`npm run setup` writes a `.env` and generates a signing key. It then asks you for one thing: your PayPal sandbox secret, from the Developer Dashboard under Apps & Credentials, behind the eye icon next to "Secret key 1".

```bash
npm test        # 29 tests, no network
npm run dev     # http://localhost:8080
```

Without `SUPABASE_URL` it stores state in memory, and without `OPA_URL` it uses built-in rules that mirror the Rego policy. Both are fine for a local demo.

### The PayPal console

```bash
npm run extension     # mints a merchant token into extension/config.json
```

Then in Chrome or Edge: `chrome://extensions` → **Developer mode** → **Load unpacked** → pick `extension/`.

Log in at `sandbox.paypal.com` with a sandbox account. An **AgentGuard** pill appears in the corner, and the full console is at `sandbox.paypal.com/myaccount/agentguard`.

### Connect your own agent

For Claude Code, `npm run connect-claude` registers the server with a fresh 7-day token. For any other MCP client:

```bash
npm run token -- agent my-agent 7d
```

```json
{
  "mcpServers": {
    "agentguard": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer <agent token>" }
    }
  }
}
```

The agent sees `create_order`, `capture_order`, `refund_capture` and `create_payout`, and gets back plain-language results including "this needs human approval, do not resubmit".

### Scripted demo

```bash
npm run demo
```

An auto-approved order, a payout held for review, a refused tampered approval, a blocked burst, and the audit chain. Point it at a deployment with `npm run demo -- https://your-app.onrender.com`.

## Current status

Honest state of things, because this is a hackathon build in progress:

- ✅ Guard chain, state machine, audit chain, MCP server, extension console, agent chat — working, 29 tests passing.
- ✅ End-to-end against the live sandbox: agent requests → policy holds it → human approves in the PayPal page → hash re-checked → PayPal called.
- ⚠️ **Payouts return `PAYOUT_NOT_AVAILABLE`** on the current sandbox business account because it is India-region. Needs a US-region sandbox account and a REST app bound to it.
- ⚠️ **Webhooks are not configured.** `PAYPAL_WEBHOOK_ID` is empty, so inbound webhooks are rejected by design. Needs a deployed URL first.
- ⬜ Not yet deployed; `render.yaml` is ready.
- ⬜ Injection-resistance test suite not yet written.

## Deploying

`render.yaml` deploys to Render as-is. After the first deploy:

1. Run `supabase/schema.sql` in the Supabase SQL editor, then set `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`.
2. In the PayPal Developer Dashboard, add a webhook at `https://<your-app>.onrender.com/webhooks/paypal` subscribed to:
   `PAYMENT.CAPTURE.COMPLETED`, `PAYMENT.CAPTURE.DENIED`, `PAYMENT.CAPTURE.REFUNDED`,
   `PAYMENT.PAYOUTSBATCH.SUCCESS`, `PAYMENT.PAYOUTSBATCH.DENIED`,
   `PAYMENT.PAYOUTS-ITEM.SUCCEEDED`, `PAYMENT.PAYOUTS-ITEM.FAILED`.
   Copy the Webhook ID into `PAYPAL_WEBHOOK_ID` or every webhook will be rejected.
3. Optional: set `ZAPIER_WEBHOOK_URL` to a Zapier Catch Hook for Slack or email alerts on held and uncertain payments.
4. Optional: run OPA with `opa run --server policy/` and set `OPA_URL`.

## API

| Method | Path | Who | What |
|---|---|---|---|
| POST | `/mcp` | agent | MCP endpoint, the four guarded tools and `get_command_status` |
| POST | `/agent/chat` | merchant | The dashboard agent |
| GET | `/commands?state=pending` | merchant | Review queue |
| GET | `/commands/:id` | merchant | One command plus its audit trail |
| POST | `/commands/:id/approve` | merchant | Body: `{"request_hash":"..."}` |
| POST | `/commands/:id/reject` | merchant | Body: `{"reason":"..."}` |
| POST | `/commands/:id/reconcile` | merchant | Resolve an uncertain command by status query |
| GET | `/audit` · `/audit/verify` | merchant | Full chain, and verification |
| POST | `/webhooks/paypal` | PayPal | Signature-verified events |

## Layout

```
src/guard/      auth (JWT/RBAC), Zod schemas, risk scoring, OPA client
src/gateway/    state machine and the orchestration that enforces it
src/paypal/     sandbox adapter, idempotency, webhook verification
src/agent/      the dashboard agent (Claude Code and Messages API backends)
src/audit/      hash chain
src/store/      in-memory and Supabase stores
src/mcp/        the four guarded tools and the read-only status check
extension/      the console injected into sandbox.paypal.com
policy/         agentguard.rego
supabase/       schema.sql
test/           29 tests, no network
```

## Built with

PayPal sandbox REST (Orders, Payments, Payouts, Webhooks) · Model Context Protocol · Claude, via Claude Code or the Messages API · OPA/Rego · Supabase (Postgres, append-only trigger) · Express · Zod · jose · Chrome MV3 extension · Render · Zapier

## Contributing

`TEAMMATES.md` is the cold-start guide: setup, the extension, what to type to see an allow, a pending and a block.

`.env` and `extension/config.json` are gitignored and must stay that way. The PayPal sandbox secret is shared person to person, never in the repo.

MIT licensed.