Files
space 5ddb56ef97 build: add a compose file that builds the image locally
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>
2026-08-01 00:43:35 +02:00

10 KiB
Raw Permalink Blame History

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 110 (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 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