Reworks the frontend around phone-first interaction patterns and makes the app installable to a home screen. Shell - Bottom tab bar (Home / Requests / Agents / Alerts / Settings) below `md`, with the unread badge moved onto the Alerts tab; the bell stays on desktop. - Account menu opens as a bottom sheet on phones, dropdown on desktop. - Safe-area insets throughout: `viewport-fit=cover` plus `env(safe-area-inset-*)` on the sticky header, tab bar and sheet footers. Sheets - New `Sheet` primitive: drag-to-dismiss bottom sheet on phones, centred dialog from `sm` up. `Modal` now delegates to it, so every dialog inherits the gesture, the scroll lock and the safe-area padding. - Portalled to `<body>` — an ancestor with a transform (the page fade-in) was otherwise becoming the containing block and displacing the fixed overlay. - Scroll lock pins `<body>` and restores position, which iOS needs; plain `overflow: hidden` still rubber-bands there. - `ConfirmSheet` replaces `window.confirm` for destructive actions. Screens - Request detail gets a sticky decision bar above the tab bar; the sidebar decision card is now desktop-only. - Requests state filter becomes a swipeable pill row instead of a select. - Agent cards show two primary actions plus an overflow sheet on phones. - Diffs and code blocks contain their horizontal overscroll so a sideways swipe no longer triggers browser back; diffs gain a line-wrap toggle. - `min-w-0` on grid tracks — items default to `min-width: auto`, so truncated meta lines were widening columns past the viewport at 320px. Touch and input - 44px minimum touch targets, `:active` press feedback, no tap highlight. - 16px inputs on coarse pointers so iOS stops zooming on focus. - autocomplete/inputmode hints so password managers and keyboards behave. - `overscroll-behavior-y: none` disables pull-to-refresh; motion respects `prefers-reduced-motion`. PWA - Manifest, generated app icons (192/512/apple-touch) and standalone display metadata, so the app installs to a home screen without browser chrome. - Backend serves `.webmanifest` as `application/manifest+json`; rjweb's type map has no entry for it. 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