# PatchPass **A human approval layer for AI agents.** > Review changes. Approve intent. Let agents proceed. Agents submit structured change requests containing code diffs, configuration updates, or custom values. Humans review and approve them in a browser UI; agents then verify the platform-signed decision through the API before applying anything. Packaged as a single `docker compose` stack — no infrastructure beyond Postgres required. --- ## How it works ``` Agent PatchPass Human │ create_request ──────────► │ │ │ (diffs/config/custom) │ normalize · hash · store │ │ ◄── request_id + URL ────────│ ──── notify (WebSocket) ───► │ reviews in browser │ │ │ APPROVE / REJECT / REQUEST_CHANGES │ get_request ─────────────► │ ◄──── signed decision ──────│ │ ◄── state + receipt ─────────│ │ │ consume_approval ─────────► │ mark CONSUMED (single-use) │ │ ◄── receipt (HMAC-SHA256) ───│ │ │ apply changes │ │ ``` **Key guarantees:** - **Platform-signed decisions.** Every approval and rejection carries an HMAC-SHA256 receipt bound to `content_hash` — the hash of the exact content the human reviewed. If the agent changes anything after approval, the receipt no longer matches. - **Single-use approvals.** An agent must `consume` an approval before proceeding; the same receipt cannot be replayed. - **Two integration paths.** Agents connect via a streaming MCP server (`/mcp`) or a plain REST API (`/v1/...`), both authenticated with a per-agent API key and backed by the same service layer. - **Changes-requested loop.** A human can return a request to the agent with reviewer notes; the agent updates and resubmits, and the updated diff is highlighted in the UI. --- ## Request lifecycle | State | Meaning | |---|---| | `PENDING` | Submitted, awaiting human review | | `CHANGES_REQUESTED` | Returned to agent with reviewer notes | | `APPROVED` | Approved; agent must consume before acting | | `REJECTED` | Hard block; agent must submit a new request | | `EXPIRED` | Not reviewed within the expiry window | | `CONSUMED` | Approval used; agent has proceeded | | `CANCELLED` | Cancelled by agent or human | No mutation is possible after `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, or `CANCELLED`. --- ## Capabilities - **Three change types:** `diff` (unified diffs), `config` (before/after key-value), `custom` (arbitrary structured values) - **Real-time notifications** via WebSocket — the browser updates instantly when a new request arrives - **TOTP 2FA** for human accounts - **Admin panel** — global on/off switches for registration and request submission, user management, audit log for admin views of request payloads - **Data export and account deletion** — humans can download all their data as JSON and delete their account from Settings - **Auto-delete policy** — humans configure how long completed requests are retained --- ## Limits | Limit | Value | |---|---| | Requests per agent per hour | 15 | | Agents per human account | 5 | | Simultaneous pending requests per agent | 1–10 (human-configured, default 5) | | Request expiry window | 60 s – 12 h (default 30 min) | | Auto-delete retention | ≥ 7 days | --- ## Tech stack | Layer | Technology | |---|---| | Backend | Node.js 24 + TypeScript (strict), [rjweb-server](https://server.rjweb.dev) | | ORM | Prisma 7 (`@prisma/adapter-pg`) | | Database | PostgreSQL 16 | | Frontend | React 18 + TypeScript + Tailwind CSS, built by Vite and served by the backend | | Realtime | rjweb WebSocket channels | | Package manager | pnpm (Backend and UI are separate workspaces) | | Tests | Vitest (integration, hits a real test database) | | CI | Gitea Actions — lint · test · build Docker image | --- ## Repository layout ``` Backend/ src/ index.ts server bootstrap, static UI serving, SPA fallback lib/ ChangeRequestService.ts request lifecycle — all business logic lives here AgentService.ts agent CRUD Authentication.ts session (human) and API-key (agent) resolution Signing.ts HMAC-SHA256 approval receipts ChangeNormalization.ts diff normalization and content hashing Limits.ts rate, pending, and agent-count enforcement Notifications.ts notification fan-out WsHub.ts WebSocket connection registry SystemCrons.ts expiry and auto-delete background jobs DataManager.ts global (admin) settings mcp/ MCP JSON-RPC server and tool definitions middlewares/ RouteAuth.ts routes/ api/ human-facing REST — auth, account, agents, requests, notifications, admin v1/ agent-facing REST — /v1/change-requests mcp.ts MCP endpoint — /mcp ws.ts WebSocket — /api/ws/notifications tests/ Vitest integration tests prisma/ schema.prisma migrations/ UI/ React + Tailwind frontend (Vite), builds to UI/build/ Dockerfile multi-stage build (UI then backend) docker-compose.yml backend (pre-built image) + Postgres docker-compose.local.yml same stack, built from this checkout example.env .gitea/workflows/deploy.yml ``` --- ## Quick start (Docker Compose) The simplest way to run PatchPass is with the pre-built image: ```bash # 1. Copy and edit the environment file cp example.env .env ``` Open `.env` and set at minimum: ```bash # Generate a strong secret: openssl rand -hex 32 INSTANCE_SECRET= # Must point to the 'database' service in docker-compose.yml DATABASE_URL=postgresql://patchpass:patchpass@db:5432/patchpass # Public URL of your instance (used in notification links) UI_URL=https://your-host DOMAIN=your-host ``` ```bash # 2. Start docker compose up -d # The backend applies migrations on boot and serves the UI. # Open http://localhost:5000 — the first account to register becomes admin. ``` ### Building the image locally To run from this checkout instead of the pre-built image — no `.env` needed, every variable falls back to a working localhost default: ```bash docker compose -f docker-compose.local.yml up -d --build ``` BuildKit builds both Dockerfile stages in parallel, which can exceed Docker Desktop's memory limit during `pnpm install`. If the build fails with "cannot allocate memory", warm the first stage on its own and retry: ```bash docker build --target build -t patchpass-build . ``` > The local compose file ships a placeholder `INSTANCE_SECRET`. Override it before > using the instance for anything real. --- ## Development setup Prerequisites: **Node.js 24+**, **pnpm**, and PostgreSQL (or Docker for the database). ```bash # Start a local Postgres with Docker docker run -d --name patchpass-db \ -e POSTGRES_USER=patchpass -e POSTGRES_PASSWORD=patchpass -e POSTGRES_DB=patchpass \ -p 5433:5432 postgres:16-alpine # Backend cd Backend cp ../example.env .env # edit DATABASE_URL to point at localhost:5433 pnpm install pnpm generate # generate Prisma client types pnpm migrate # apply database migrations pnpm dev # http://localhost:5000 # UI — hot-reload dev server (separate terminal) cd UI pnpm install pnpm dev # http://localhost:3000, proxies /api and /v1 to :5000 ``` For a production-style build without Docker, `cd UI && pnpm build` — the backend then serves `UI/build/` at `/`. ### Running tests ```bash cd Backend pnpm test # Vitest integration suite — spins up against the test database ``` The test database URL is configured in `Backend/vitest.config.ts`. Tests hit a real database; there are no mocks at the DB layer. ### Before committing ```bash cd Backend && pnpm lint # ESLint (TypeScript strict) cd Backend && pnpm test # integration tests cd UI && pnpm lint # ESLint cd UI && pnpm build # Vite production build check ``` --- ## Connecting an agent Create an agent in the UI (**Agents → New agent**), copy the API key shown once at creation, then configure your agent client. ### OpenClaw Add to `~/.openclaw/openclaw.json`: ```json { "mcp": { "servers": { "patchpass": { "transport": "streamable-http", "url": "https://your-host/mcp", "headers": { "x-api-key": "pp_agent_..." } } } } } ``` ### Generic MCP client (Claude Desktop, Cursor, etc.) ```json { "mcpServers": { "patchpass": { "type": "streamable-http", "url": "https://your-host/mcp", "headers": { "x-api-key": "pp_agent_..." } } } } ``` ### REST API ```bash # Submit a change request curl -X POST https://your-host/v1/change-requests \ -H "x-api-key: pp_agent_..." \ -H "Content-Type: application/json" \ -d '{ "title": "Bump deployment timeout", "changes": [ { "type": "config", "path": "timeout", "before": 30, "after": 60, "content_type": "integer" } ] }' # Poll for the decision curl https://your-host/v1/change-requests/{request_id} -H "x-api-key: pp_agent_..." # Consume the approval before applying the change curl -X POST https://your-host/v1/change-requests/{request_id}/consume -H "x-api-key: pp_agent_..." ``` ### Agent instruction Tell your agent: > Before taking any consequential action (deploys, config changes, code edits), call `create_request`, wait for approval, then call `consume_approval` before proceeding. --- ## Privacy & data Humans can export all their data as JSON and delete their account (cascading to agents, requests, and notifications) from **Settings**. Agent icon URLs are fetched once for validation and never stored. See the in-app Privacy Policy and Terms of Service. --- ## License MIT-0