The state pills and the agent select sat 10px apart, and the pill row was sliced mid-chip at the viewport edge with nothing to say it scrolled. - New `ScrollRow`: fades whichever edge still has content behind it, so a clipped chip reads as "scroll for more" instead of as a broken layout. Reused for the admin and connect-modal tab strips. - Drop scroll-snap from the pill row. It clamped the resting scrollLeft to the container's 12px padding, so the first chip sat flush against the screen edge with no gutter; flick-snapping through short chips felt wrong regardless. - Breathing room: pills to select 10px -> 14px, filters to list 16px -> 20px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
consumean 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 |
| 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:
# 1. Copy and edit the environment file
cp example.env .env
Open .env and set at minimum:
# 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
# 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:
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:
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).
# 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
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
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:
{
"mcp": {
"servers": {
"patchpass": {
"transport": "streamable-http",
"url": "https://your-host/mcp",
"headers": { "x-api-key": "pp_agent_..." }
}
}
}
}
Generic MCP client (Claude Desktop, Cursor, etc.)
{
"mcpServers": {
"patchpass": {
"type": "streamable-http",
"url": "https://your-host/mcp",
"headers": { "x-api-key": "pp_agent_..." }
}
}
}
REST API
# 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 callconsume_approvalbefore 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