5ddb56ef97
docker-compose.yml pulls registry.reversed.dev. This variant builds from the checkout instead and needs no .env — every variable falls back to a working localhost default, so the stack comes up ready to register the first admin. Also exposes Postgres on 5434 (clear of the dev database on 5433) and adds a backend healthcheck so `depends_on` ordering is meaningful. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
301 lines
10 KiB
Markdown
301 lines
10 KiB
Markdown
# 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=<random 32-byte hex>
|
||
|
||
# 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
|