From dfa0c55e3cc64c49fa537db22f2c67a2d90a3eaf Mon Sep 17 00:00:00 2001 From: luna Date: Sun, 19 Jul 2026 20:39:43 +0000 Subject: [PATCH] feat: prefill agent-edit modal, Ctrl+Enter submits modals, improve README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentFormModal: sync form state to current agent values when the modal opens (useEffect on open/agent), so edit always shows the agent's current name, description, website, icon URL, and max-pending value. - Modal: add optional onSubmit prop; Ctrl+Enter (or ⌘+Enter) fires it, covering AgentFormModal, DecisionModal, and both Settings modals (disable 2FA, delete account) without breaking textarea newlines or text-input behaviour. - README: substantially expanded — purpose, capabilities, architecture, quick-start with Docker Compose, dev setup, testing, agent integration (OpenClaw, generic MCP, REST), and privacy notes. - TODO.md: remove three completed items (prefill modal, Ctrl+Enter, human readme). Co-Authored-By: Claude Sonnet 4.6 --- README.md | 241 +++++++++++++++++++++------- TODO.md | 5 +- UI/src/components/AgentModals.tsx | 12 +- UI/src/components/DecisionModal.tsx | 1 + UI/src/components/Modal.tsx | 5 +- UI/src/pages/Settings.tsx | 2 + 6 files changed, 204 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index f014ba8..ad6dee5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > 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 the browser, then agents verify the platform-signed decision through the API before applying anything. +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. --- @@ -12,110 +12,204 @@ Agents submit structured change requests containing code diffs, configuration up ``` Agent PatchPass Human - │ create_request ───────────► │ │ - │ (diffs/config/custom) │ normalize · hash · store │ - │ ◄─── request_id + URL ────────│ ──── notify (WS) ──────────► │ reviews in browser - │ │ │ APPROVE / REJECT / REQUEST_CHANGES - │ get_request ──────────────► │ ◄──── signed decision ──────│ - │ ◄─── state + receipt ─────────│ │ - │ consume_approval ─────────► │ mark CONSUMED (single-use) │ - │ ◄─── receipt (HMAC) ──────────│ │ - │ apply changes │ │ + │ 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 │ │ ``` -- **Decisions are platform-signed.** Approvals and rejections carry an HMAC-SHA256 receipt bound to the exact reviewed content (`content_hash`). If the agent changes anything after approval, the receipt no longer matches. -- **Approvals are single-use.** An agent must `consume` an approval before proceeding; it cannot be replayed. -- **Two ways in for agents:** a REST API (`/v1/...`) and an MCP server (`/mcp`), both authenticated with a per-agent API key and backed by the same service layer. +**Key guarantees:** -## Request states +- **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. -`PENDING` · `CHANGES_REQUESTED` · `APPROVED` · `REJECTED` · `EXPIRED` · `CONSUMED` · `CANCELLED` +--- -No update is possible after approval, rejection, expiry, consumption, or cancellation. A `REQUEST_CHANGES` decision requires a comment and returns the request to the agent, which can `update_request` to resubmit (highlighted in the UI as **UPDATED**). +## 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 | 5 | +| Agents per human account | 5 | | Simultaneous pending requests per agent | 1–10 (human-configured, default 5) | -| Request expiry | default 30 min, max 12 h | +| Request expiry window | 60 s – 12 h (default 30 min) | +| Auto-delete retention | ≥ 7 days | + +--- ## Tech stack | Layer | Technology | |---|---| -| Backend | Node.js + TypeScript, [rjweb-server](https://server.rjweb.dev) | +| Backend | Node.js 24 + TypeScript (strict), [rjweb-server](https://server.rjweb.dev) | | ORM | Prisma 7 (`@prisma/adapter-pg`) | -| Database | PostgreSQL | -| Frontend | React + TypeScript + Tailwind CSS (Vite), served by the backend | +| Database | PostgreSQL 16 | +| Frontend | React 18 + TypeScript + Tailwind CSS, built by Vite and served by the backend | | Realtime | rjweb WebSocket channels | -| Tests | Vitest | +| 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/ Node + TypeScript API (rjweb-server) + Prisma +Backend/ src/ - index.ts server bootstrap + static UI serving - lib/ service layer, auth, signing, limits, crons, MCP tools - routes/ REST (/api, /v1), MCP (/mcp), WebSocket (/api/ws) - tests/ Vitest integration tests - prisma/schema.prisma -UI/ React + Tailwind frontend (builds to UI/build) -Dockerfile multi-stage build (UI + backend) -docker-compose.yml backend + postgres -.gitea/workflows/ CI: build · lint · test · push image + 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 + Postgres +example.env +.gitea/workflows/deploy.yml ``` -## Running locally +--- -Prerequisites: Node 24+, pnpm, and Postgres (or Docker). +## Quick start (Docker Compose) + +The simplest way to run PatchPass is with the pre-built image: ```bash -# 1. Start Postgres (Docker) +# 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. +``` + +--- + +## 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 -# 2. Backend +# Backend cd Backend -cp ../example.env .env # then edit DATABASE_URL / INSTANCE_SECRET +cp ../example.env .env # edit DATABASE_URL to point at localhost:5433 pnpm install -pnpm generate -pnpm migrate # apply migrations +pnpm generate # generate Prisma client types +pnpm migrate # apply database migrations pnpm dev # http://localhost:5000 -# 3. UI (separate terminal, for hot-reload dev) +# UI — hot-reload dev server (separate terminal) cd UI pnpm install -pnpm dev # http://localhost:3000 (proxies /api to :5000) +pnpm dev # http://localhost:3000, proxies /api and /v1 to :5000 ``` -For a production-style run, `cd UI && pnpm build` — the backend then serves `UI/build` at `/`. +For a production-style build without Docker, `cd UI && pnpm build` — the backend then serves `UI/build/` at `/`. -### Tests +### Running tests ```bash cd Backend -pnpm test # spins up against the test database (see vitest.config.ts) +pnpm test # Vitest integration suite — spins up against the test database ``` -## Deploying +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 -cp example.env .env # set DATABASE_URL=...@db:5432/..., a strong INSTANCE_SECRET -docker compose up -d --build +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 ``` -The backend applies migrations on boot and serves the UI. The first account to register becomes the **admin**. +--- ## Connecting an agent -Create an agent in the UI, then use its API key. +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`: +### OpenClaw + +Add to `~/.openclaw/openclaw.json`: ```json { @@ -131,20 +225,55 @@ Create an agent in the UI, then use its API key. } ``` -**REST**: +### Generic MCP client (Claude Desktop, Cursor, etc.) -```bash -curl -X POST https://your-host/v1/change-requests \ - -H "x-api-key: pp_agent_..." -H "Content-Type: application/json" \ - -d '{"title":"Bump timeout","changes":[{"type":"config","path":"timeout","before":30,"after":60,"content_type":"integer"}]}' +```json +{ + "mcpServers": { + "patchpass": { + "type": "streamable-http", + "url": "https://your-host/mcp", + "headers": { "x-api-key": "pp_agent_..." } + } + } +} ``` -Tell your agent: *before taking any consequential action, call `create_request`, wait for approval, then `consume_approval` before proceeding.* +### 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 diff --git a/TODO.md b/TODO.md index f0f5c49..abf639a 100644 --- a/TODO.md +++ b/TODO.md @@ -9,14 +9,11 @@ Thoughts: - Able to click on a agent either on home, agents list or in a request to show the agents past requests and overall actions from the list but moved over into a new page with a back button to return to the previous page. - Show QR for 2fa - invalid dates on admin action notifications -- pre fill data on agent edit modal - overall ux improvements - Fix popup for passwordmanagers filling in 2fa code on settings page -- ctrl enter submits modals (all) - better repo rules for agents - Nicer statistics on the dashboard - Instance Notice popup that popups up on every session (like a cookie notice), content by admins, dismissable - Instance Disabled message & setting for admins to set a message for when the instance is disabled, and a setting to allow users to see the message when the instance is disabled. (users can only delte or download their data when the instance is disabled) - Data Hoster notice for footer when people host an instance for other people to use, content by admins, not dismissable footer only notice, all pages -- mobile friendly improvements -- human readme \ No newline at end of file +- mobile friendly improvements \ No newline at end of file diff --git a/UI/src/components/AgentModals.tsx b/UI/src/components/AgentModals.tsx index 2ffe24b..8d65dbf 100644 --- a/UI/src/components/AgentModals.tsx +++ b/UI/src/components/AgentModals.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "react-toastify"; import { agents as agentsApi } from "../api/client"; import type { Agent } from "../api/types"; @@ -27,6 +27,15 @@ export function AgentFormModal({ const [maxPending, setMaxPending] = useState(agent?.max_pending_requests ?? 5); const [loading, setLoading] = useState(false); + useEffect(() => { + if (!open) return; + setName(agent?.name ?? ""); + setDescription(agent?.description ?? ""); + setWebsite(agent?.website ?? ""); + setIconUrl(agent?.icon_url ?? ""); + setMaxPending(agent?.max_pending_requests ?? 5); + }, [open, agent]); + const submit = async () => { if (!name.trim()) return toast.error("Name is required"); setLoading(true); @@ -55,6 +64,7 @@ export function AgentFormModal({ diff --git a/UI/src/components/DecisionModal.tsx b/UI/src/components/DecisionModal.tsx index c36a1c5..76574f4 100644 --- a/UI/src/components/DecisionModal.tsx +++ b/UI/src/components/DecisionModal.tsx @@ -75,6 +75,7 @@ export function DecisionModal({ diff --git a/UI/src/components/Modal.tsx b/UI/src/components/Modal.tsx index 58c9cd9..2c878e0 100644 --- a/UI/src/components/Modal.tsx +++ b/UI/src/components/Modal.tsx @@ -3,6 +3,7 @@ import { ReactNode, useEffect } from "react"; export function Modal({ open, onClose, + onSubmit, title, children, footer, @@ -10,6 +11,7 @@ export function Modal({ }: { open: boolean; onClose: () => void; + onSubmit?: () => void; title: string; children: ReactNode; footer?: ReactNode; @@ -19,6 +21,7 @@ export function Modal({ if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); + if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && onSubmit) onSubmit(); }; window.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; @@ -26,7 +29,7 @@ export function Modal({ window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; - }, [open, onClose]); + }, [open, onClose, onSubmit]); if (!open) return null; diff --git a/UI/src/pages/Settings.tsx b/UI/src/pages/Settings.tsx index acace9d..b367693 100644 --- a/UI/src/pages/Settings.tsx +++ b/UI/src/pages/Settings.tsx @@ -223,6 +223,7 @@ export function SettingsPage() { setDisable2faOpen(false)} + onSubmit={disable2fa} title="Disable 2FA" footer={ <> @@ -246,6 +247,7 @@ export function SettingsPage() { setDeleteOpen(false)} + onSubmit={deleteAccount} title="Delete account" footer={ <> -- 2.39.5