# Pinchy Documentation — Complete Reference
# Source: https://docs.heypinchy.com
# This file contains the full documentation for Pinchy, an enterprise AI agent platform.
# License: AGPL-3.0 | Publisher: Helmcraft GmbH (https://heypinchy.com)

# Pinchy Documentation
URL: https://docs.heypinchy.com/

## What is Pinchy?

Pinchy is an enterprise layer on top of [OpenClaw](https://github.com/openclaw/openclaw) — the open-source AI agent runtime. OpenClaw is powerful for individual users. Pinchy adds what teams need: permissions, audit trails, user management, and governance.

We believe AI agents should run on **your** servers, with **your** data, under **your** control.

## Current Status

Pinchy is in early development. The core is working — setup, authentication, provider configuration, agent chat via OpenClaw, and cryptographic audit trail. We're building the remaining enterprise features (granular RBAC with custom roles and per-resource permissions, plugin marketplace) next.

---

# Quick Start
URL: https://docs.heypinchy.com/getting-started/

This guide walks you through getting Pinchy up and running — from cloning the repo to chatting with your first AI agent.

## Prerequisites

- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) (v2+)
- An API key from an LLM provider (Anthropic, OpenAI, or Google)

## 1. Download the Compose file

```bash
mkdir -p pinchy && cd pinchy
curl -fsSL https://raw.githubusercontent.com/heypinchy/pinchy/main/docker-compose.yml -o docker-compose.yml
```

## 2. Start the stack

```bash
docker compose pull && docker compose up -d
```

This starts three services:

- **Pinchy** — the web app on port 7777
- **OpenClaw** — the AI agent runtime (internal, not exposed)
- **PostgreSQL** — the database

Wait until you see `Pinchy ready on http://localhost:7777` in the logs.

## 3. Create your admin account

Open [http://localhost:7777](http://localhost:7777) in your browser. The setup wizard appears automatically on first run.

Enter your name, email, and password. This creates the first admin user.

## 4. Configure your AI provider

After creating your account, Pinchy redirects you to the provider setup page. Choose your LLM provider and enter your API key:

| Provider | Where to get a key |
|----------|-------------------|
| Anthropic | [console.anthropic.com](https://console.anthropic.com/) |
| OpenAI | [platform.openai.com](https://platform.openai.com/api-keys) |
| Google | [aistudio.google.com](https://aistudio.google.com/apikey) |

Your API key is encrypted at rest using AES-256-GCM before being stored in the database.

## 5. Chat with Smithers

Once your provider is configured, Pinchy redirects you to the chat interface. Your default agent — **Smithers** — greets you automatically.

Type a message and hit Enter. Smithers responds in real time via WebSocket.

## 6. Invite your team

Go to **Settings → Users** to invite team members. Enter their email address and Pinchy generates a one-time invite link.

Each invited user gets their own personal **Smithers** agent, created automatically when they claim the invite. Invite links are valid for 7 days.

## 7. Create a Knowledge Base Agent (optional)

You can create agents from templates. Creating an agent is simple: pick a template, give it a name, and click Create.

For example, a **Knowledge Base** agent can read files in directories you select — but has no access to anything else by default. After creating the agent, open its **Settings → Permissions** tab to configure which directories it can access.

To try it out, mount a directory with documents into the Pinchy container and create an agent from the Knowledge Base template. See the [Create a Knowledge Base Agent](/guides/create-knowledge-base-agent/) guide for the full walkthrough.

## What's next?

- **[Installation](/installation/)** — Learn about environment variables, Docker volumes, and development setup
- **[Architecture](/architecture/)** — Understand how Pinchy wraps OpenClaw and the request flow
- **[Agent Permissions](/concepts/agent-permissions/)** — Learn how Pinchy restricts what agents can access
- **[Audit Trail](/concepts/audit-trail/)** — Cryptographic audit logging for compliance and security
- **[API Reference](/reference/api/)** — REST API endpoints for managing agents and settings

---

# Installation
URL: https://docs.heypinchy.com/installation/

## System requirements

- Docker Engine 20.10+ and Docker Compose v2+
- 4 GB RAM minimum (for Pinchy + OpenClaw + PostgreSQL)
- An LLM provider API key (Anthropic, OpenAI, or Google)

## Docker Compose (recommended)

The simplest way to run Pinchy. One command starts the full stack.

```bash
mkdir -p pinchy && cd pinchy
curl -fsSL https://raw.githubusercontent.com/heypinchy/pinchy/main/docker-compose.yml -o docker-compose.yml
docker compose pull && docker compose up -d
```

### Services

The `docker-compose.yml` defines three services:

| Service | Image | Port | Purpose |
|---------|-------|------|---------|
| `pinchy` | Custom (Next.js) | 7777 (exposed) | Web UI, API, WebSocket bridge |
| `openclaw` | Custom (Node.js) | 18789 (internal) | AI agent runtime |
| `db` | `postgres:16` | 5432 (internal) | Database |

Only port **7777** is exposed to the host. OpenClaw and PostgreSQL are only reachable within the Docker network.

### Environment variables

Create a `.env` file in the project root to override defaults:

```bash
# Database password (default: pinchy_dev)
DB_PASSWORD=your-secure-password

# Auth.js session secret (default: dev-secret-change-in-production)
NEXTAUTH_SECRET=your-random-secret

# Encryption key for API keys — 64 hex characters (auto-generated if omitted)
ENCRYPTION_KEY=

# HMAC secret for audit trail signing (auto-generated if not provided)
AUDIT_HMAC_SECRET=
```

**`NEXTAUTH_SECRET`** — Used by Auth.js to sign JWT session tokens. Generate one with `openssl rand -hex 32`.

**`ENCRYPTION_KEY`** — Used to encrypt provider API keys at rest (AES-256-GCM). If omitted, Pinchy auto-generates a key and persists it in the `pinchy-secrets` Docker volume. For production, we recommend setting this explicitly with `openssl rand -hex 32`.

**`AUDIT_HMAC_SECRET`** — Used to sign audit trail entries with HMAC-SHA256. If omitted, Pinchy auto-generates a secret at startup. Set this explicitly if you need consistent signatures across restarts or deployments.

**`DB_PASSWORD`** — Password for the PostgreSQL `pinchy` user.

### Data persistence

Docker volumes ensure data survives container restarts:

| Volume | Mounted at | Purpose |
|--------|-----------|---------|
| `pgdata` | `/var/lib/postgresql/data` | PostgreSQL data |
| `openclaw-config` | `/root/.openclaw` (OpenClaw), `/openclaw-config` (Pinchy) | Shared OpenClaw configuration |
| `pinchy-data` | `/data` (OpenClaw) | Agent-accessible files (Knowledge Base documents) |
| `pinchy-secrets` | `/app/secrets` (Pinchy) | Auto-generated encryption and HMAC keys |
| `openclaw-extensions` | `/openclaw-extensions` (Pinchy), `/root/.openclaw/extensions` (OpenClaw) | Pinchy plugins for OpenClaw |

### Port configuration

To change the exposed port, update both `docker-compose.yml` and the `PORT` environment variable:

```yaml
# docker-compose.yml
services:
  pinchy:
    ports:
      - "3000:3000"
    environment:
      - PORT=3000
```

## Development mode

For development with hot reload (code changes reflect immediately in the browser), use the dev-mode Docker override:

```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
```

After the initial build, subsequent starts only need:

```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
```

### What hot-reloads and what doesn't

- **React components, pages, styles** — instant HMR via Next.js dev server
- **server.ts** — requires container restart (`docker compose restart pinchy`)
- **package.json / dependencies** — requires rebuild (`--build`)

## Development setup

For local development without Docker (except for the database and OpenClaw):

```bash
# Install dependencies
pnpm install

# Start database and OpenClaw in Docker (dev override exposes port 5433)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up db openclaw -d

# Set database URL for local dev
export DATABASE_URL=postgresql://pinchy:pinchy_dev@localhost:5433/pinchy

# Run database migrations
pnpm db:migrate

# Start the dev server
pnpm dev
```

The app starts at [http://localhost:7777](http://localhost:7777) with hot reload.

### Available commands

```bash
pnpm dev          # Start dev server with hot reload
pnpm build        # Production build
pnpm test         # Run test suite
pnpm lint         # Run ESLint
pnpm format       # Format code with Prettier
pnpm db:generate  # Generate new migration from schema changes
pnpm db:migrate   # Apply pending migrations
pnpm db:studio    # Open Drizzle Studio (database browser)
```

All commands run from the repository root and are forwarded to `packages/web/` via pnpm workspace.

---

# Architecture
URL: https://docs.heypinchy.com/architecture/

## Overview

Pinchy is **not a fork** of OpenClaw. It's a governance layer on top of it. OpenClaw handles agent execution, tool use, and model communication. Pinchy adds authentication, provider management, agent permissions, cryptographic audit trails, and (soon) team management.

```mermaid
graph TD
    Browser["Browser<br/>(Next.js React Frontend)"]
    Browser --> Pages
    Browser --> WS

    subgraph Pinchy["Pinchy (Node.js)"]
        Pages["Next.js Pages<br/>(HTTPS)"]
        Auth["Auth (Auth.js)"]
        WS["WS Bridge<br/>(/api/ws)"]
        ORM["Drizzle ORM"]
        Pages --- ORM
        Auth --- ORM
        WS --- ORM
    end

    ORM --> DB["PostgreSQL 16"]
    ORM --> OC["OpenClaw Gateway<br/>(port 18789)"]
```

## Request flow

When a user sends a message, it flows through three layers:

1. **Browser** → connects via WebSocket to `/api/ws` on Pinchy, sending a JSON message with `type`, `content`, and `agentId`
2. **Pinchy** → authenticates the user, verifies agent access, resolves the **session key** from the in-memory session cache, generates a message ID, and forwards the message to OpenClaw Gateway with the `agentId` and `sessionKey`
3. **OpenClaw** → routes the message to the correct agent from its config, processes it through the configured model, and streams the response back
4. **Pinchy** → attaches the message ID and forwards each chunk to the browser
5. **Browser** → renders the streaming response in real time

Each browser connection gets its own `ClientRouter` instance that manages agent access checks and session resolution. Pinchy acts as a bridge — it never interprets or modifies the AI response content.

### Agent routing

When a message arrives, the `ClientRouter` passes the `agentId` from the browser message to OpenClaw via `chatOptions`. OpenClaw uses this to select the matching agent from its `agents.list[]` config — each agent can have its own model, system prompt, tools, and workspace.

### Chat sessions

Pinchy derives a deterministic session key for each `(agentId, userId)` pair using the format `agent:<agentId>:user-<userId>`. This gives each user their own conversation per agent — even for shared agents. Session keys are resolved in-memory via a `SessionCache` that periodically syncs with OpenClaw's session list — no database table is needed. The session key is opaque and never leaves the server — the browser only sends an `agentId`, and Pinchy resolves the session internally.

The browser can request conversation history by sending a `{ type: "history", agentId }` message. Pinchy fetches the history from OpenClaw via `openclaw-node` and strips internal metadata (timestamps, thinking blocks) before returning it.

### Session key naming convention

Session keys follow the format `agent:<agentId>:<scope>`. OpenClaw validates that the `agentId` segment matches the `agentId` parameter passed to `chat()` — a mismatch causes silent failures.

Current format: `agent:<agentId>:user-<userId>` (per-user session for both personal and shared agents). This means converting a personal agent to a shared agent is seamless.

Planned formats: `agent:<agentId>:cron-<jobId>` (scheduled actions), `agent:<agentId>:hook-<hookId>` (webhook triggers).

### Session lifecycle and compaction

Sessions grow with each message. OpenClaw compacts automatically when the context window approaches its limit (`"compaction": { "mode": "safeguard" }`). There is also an explicit `sessions.compact(key, { maxLines })` API. Sessions track a `compactionCount` to record how many compactions have occurred.

## Authentication

Pinchy uses [Auth.js v5](https://authjs.dev/) with a credentials provider:

- Passwords are hashed with **bcrypt** before storage
- Sessions use **JWT** strategy (stateless, no server-side session store)
- JWTs carry the user's **role** (`admin` or `user`) for authorization checks
- The first user created via the setup wizard becomes the admin
- All app routes require authentication — unauthenticated requests redirect to `/login`

### Roles

Pinchy has two roles:

- **Admin** — can manage agents, users, invites, and settings
- **User** — can chat with agents and update their own profile

### Invite system

Admins invite new users by generating an invite token:

1. Admin creates an invite for an email address via **Settings → Users**
2. Pinchy generates a random token, stores its **SHA-256 hash** in the database, and returns the plaintext token as a one-time invite link
3. The invite recipient opens the link, sets their name and password, and their account is created
4. A personal **Smithers** agent is automatically created for the new user
5. Invite tokens expire after **7 days** and are single-use

## Permission layer

Pinchy uses an **allow-list** model for agent permissions: agents have **no tools by default**. Admins explicitly enable tools for each agent via the Permissions tab in Agent Settings.

Tools are organized into two categories:

- **Safe tools** — sandboxed directory access (`pinchy_ls`, `pinchy_read`). Only work within directories the admin has approved.
- **Powerful tools** — direct server access (shell commands, unrestricted file access, web access). Only for trusted use cases.

When an agent has safe tools enabled, the `pinchy-files` plugin validates every file access request against the agent's allowed directories, with symlink resolution to prevent escapes.

All agent-accessible files live under `/data/` in the container, mounted via Docker volumes. This means even if all software layers failed, the agent can only see files that were explicitly mounted.

### Agent access control

Pinchy restricts which agents a user can see:

- **Admins** can access all agents (personal and shared)
- **Users** can access shared agents and their own personal agent
- Only admins can view and modify agent permissions

For the full details, see [Agent Permissions](/concepts/agent-permissions/).

## Database

PostgreSQL 16, accessed via [Drizzle ORM](https://orm.drizzle.team/). The schema includes:

- **Auth tables** — `user`, `account`, `session`, `verificationToken` (managed by Auth.js adapter)
- **`agents`** — Agent configuration (name, model, template, allowed tools, plugin config, owner)
- **`invites`** — Invite tokens (SHA-256 hashed token, email, expiry, status)
- **`settings`** — Key-value store for app configuration (provider keys, onboarding state)
- **`auditLog`** — Append-only audit trail with HMAC-SHA256 signed rows, protected by PostgreSQL triggers preventing UPDATE and DELETE

Migrations are generated with `drizzle-kit generate` and applied automatically on container startup via `drizzle-kit migrate`.

## Encryption

Provider API keys are encrypted at rest using **AES-256-GCM**:

- A 256-bit encryption key is either provided via the `ENCRYPTION_KEY` environment variable or auto-generated and persisted in the `pinchy-secrets` Docker volume
- Each encrypted value stores the IV, auth tag, and ciphertext together
- Decryption happens on-demand when Pinchy writes the OpenClaw configuration file

## Audit Trail

Pinchy includes a cryptographic audit trail for compliance and security. Every significant action is logged to the `auditLog` table with an HMAC-SHA256 signature.

### Design principles

- **Append-only** — PostgreSQL triggers prevent UPDATE and DELETE operations on audit rows. Once written, entries are immutable.
- **Cryptographically signed** — Each row is signed with HMAC-SHA256 using a server-side secret (auto-generated if not provided via `AUDIT_HMAC_SECRET`).
- **Fire-and-forget** — Audit logging never blocks or breaks the main operation. If logging fails, the original action still succeeds.
- **Admin-only access** — Only admins can view, verify, or export the audit log.

### What gets logged

Pinchy logs 12 event types covering authentication, agent management, user management, configuration changes, and tool execution. Chat message content is **not** logged — only tool calls and permission-relevant actions.

### Integrity verification

Admins can verify the integrity of audit entries via the admin UI or the `/api/audit/verify` endpoint. Verification recomputes HMAC signatures and reports any tampered rows.

### CSV export

The audit log can be exported as CSV for compliance reporting via the admin UI or the `/api/audit/export` endpoint.

For the full details, see [Audit Trail](/concepts/audit-trail/).

## OpenClaw integration

OpenClaw runs as a separate Docker container. Pinchy communicates with it via `openclaw-node` (the official Node.js client) over WebSocket on port 18789. The browser never connects to OpenClaw directly.

### Config generation

Pinchy owns the OpenClaw configuration file (`openclaw.json`). Config generation happens in two scenarios:

1. **Initial setup** — during the onboarding wizard, `writeOpenClawConfig()` writes the first config with provider credentials and a randomly generated auth token
2. **Regeneration** — whenever agents, permissions, or providers change, `regenerateOpenClawConfig()` rebuilds the config from DB state

Regeneration is **idempotent**: it preserves only the `gateway` block (which contains the auth token and OpenClaw-generated fields) and rebuilds everything else — `env`, `agents`, and `plugins` — from the database. This ensures deleted providers and agents are cleaned up automatically.

An inotify-based wrapper script inside the OpenClaw container detects the config change and restarts the gateway automatically.

### Authentication

Pinchy authenticates to OpenClaw Gateway using a bearer token. The token is auto-generated on first setup via `crypto.randomBytes(24)` and stored in the `gateway.auth` block of `openclaw.json`. It never appears in source control.

## Tech stack

| Layer | Technology |
|-------|-----------|
| Frontend | Next.js 16, React 19, Tailwind CSS v4, shadcn/ui |
| Chat UI | assistant-ui (React) |
| Auth | Auth.js v5 (credentials provider, JWT sessions) |
| Database | PostgreSQL 16, Drizzle ORM |
| Agent runtime | OpenClaw Gateway (WebSocket) |
| Encryption | AES-256-GCM (Node.js crypto) |
| Testing | Vitest, React Testing Library |
| CI/CD | GitHub Actions, ESLint, Prettier |
| Deployment | Docker Compose |
| License | AGPL-3.0 |

---

# Agent Permissions
URL: https://docs.heypinchy.com/concepts/agent-permissions/

## Why agent permissions matter

AI agents are powerful — but in an enterprise setting, they must be constrained. An agent that can read any file on the server or execute arbitrary commands is a liability. Pinchy enforces **per-agent permissions** so each agent can only use the tools an admin explicitly enables.

## Allow-list approach

Pinchy uses an **allow-list** model: agents have **no tools by default**. An admin must explicitly enable each tool an agent is allowed to use. This is the opposite of a deny-list approach where everything is allowed unless blocked — with Pinchy, nothing is allowed unless granted.

This means a newly created agent cannot access files, run commands, or browse the web until an admin configures its permissions.

## Tool categories

Pinchy organizes tools into two categories:

### Safe Tools

Safe tools provide **sandboxed access** to directories the admin has approved. The agent cannot access anything outside those directories.

| Tool | What it does |
|------|-------------|
| **List approved directories** | List files in directories the admin has selected |
| **Read approved files** | Read file contents from approved directories only |

When you enable a safe tool, a directory picker appears so you can select which directories under `/data/` the agent can access. Every file access request is validated against these directories at runtime.

### Powerful Tools

Powerful tools give the agent **direct access to the server**. Only enable them if you understand the risks.

| Tool | What it does |
|------|-------------|
| **Run commands** | Execute shell commands on the server |
| **Read any file** | Read any file on the system, without restrictions |
| **Write files** | Create and modify files on the system |
| **Browse the web** | Fetch web pages |
| **Search the web** | Perform web searches |

An agent with powerful tools enabled has significantly broader capabilities. Use these only for trusted, internal use cases.

## The Permissions tab

Admins configure tool permissions in the **Permissions tab** of an agent's settings page. To access it:

1. Open an agent's chat
2. Click the settings icon (gear) to open Agent Settings
3. Select the **Permissions** tab

The Permissions tab shows all available tools grouped by category. Check or uncheck tools to control what the agent can do, then click **Save**.

> **Note:** The Permissions tab is only visible to admins and is not available for personal agents (the auto-created Smithers agent each user gets).


### Configuring directory access

When you enable a safe tool (like "List approved directories" or "Read approved files"), the **Allowed Directories** picker appears below the safe tools section. Select which directories under `/data/` this agent should be able to access.

For example, if you mounted your HR policies at `/data/hr-policies`, select that directory to let the agent read those documents — and nothing else.

See the [Mount Data Directories](/guides/mount-data-directories/) guide for instructions on making directories available.

## How permissions reach OpenClaw

OpenClaw uses a **deny-list** model internally: by default every tool group is available, and you block what you don't want. Pinchy's allow-list is the opposite — nothing is available until enabled.

To bridge these two models, Pinchy converts the allow-list into a deny-list at config generation time using `computeDeniedGroups()`. This function takes the list of tool IDs an admin has enabled and returns all tool groups that should be blocked. The result is written into each agent's `tools.deny` array in the OpenClaw config.

For example, if an admin enables only `pinchy_ls` and `pinchy_read` (safe tools), `computeDeniedGroups` returns all powerful tool groups (`computer`, `files`, `web_search`, `web_fetch`) so that OpenClaw blocks those capabilities for this agent.

This conversion runs automatically whenever the OpenClaw config is regenerated — admins never interact with the deny-list directly.

## Agent templates and default permissions

When you create an agent, you pick a template. Each template comes with different default tool permissions:

| Template | Default tools | Use case |
|----------|--------------|----------|
| **Knowledge Base** | `pinchy_ls`, `pinchy_read` (safe tools) | Answer questions from selected documents |
| **Custom Agent** | None | Start from scratch, configure permissions manually |

After creating an agent, you can change its permissions at any time via the Permissions tab.

## Defense in depth

Pinchy does not rely on any single layer for security. Four layers work together:

1. **Docker volumes** — only directories explicitly mounted into the container are accessible at all
2. **Allow-list enforcement** — only tools an admin explicitly enables are available to the agent
3. **Plugin path validation** — the `pinchy-files` plugin checks every requested path against the agent's allowed directories
4. **Symlink resolution** — paths are resolved to their real location before validation, preventing symlink-based escapes

If any one layer fails, the others still prevent unauthorized access.

## Agent access control

Not every user can see every agent. Pinchy enforces access rules:

- **Admins** can access all agents — personal and shared
- **Users** can access shared agents (created by anyone) and their own personal agent
- Users **cannot** see or access other users' personal agents

Only admins can view and modify the Permissions tab. Regular users can chat with agents they have access to, but cannot change what tools those agents use.

> **Note:** All permission changes are logged in the [audit trail](/concepts/audit-trail/). When an admin enables or disables tools for an agent, the change is recorded with the admin's identity, the affected agent, and the old and new permissions.


## The `/data/` convention

All agent-accessible files live under `/data/` inside the Pinchy container. This is mounted as a Docker volume, and you can bind-mount host directories into subdirectories of `/data/`.

For example, if you mount your company's HR policies at `/data/hr-policies`, an agent configured to access that directory can read those documents — and nothing else.

See the [Mount Data Directories](/guides/mount-data-directories/) guide for setup instructions.

---

# Audit Trail
URL: https://docs.heypinchy.com/concepts/audit-trail/

## What is the audit trail?

Pinchy includes a built-in audit trail that logs every significant action on the platform. Each entry is cryptographically signed with HMAC-SHA256 to detect tampering. The audit log is append-only — PostgreSQL triggers prevent any modification or deletion of existing entries.

The audit trail is designed for **compliance and security**. It answers the question: "Who did what, and when?"

## What gets logged

Pinchy logs 12 event types across five categories:

### Tool execution

| Event Type | Description |
|------------|-------------|
| `tool.execute` | An agent executed a tool (shell command, file read, web fetch, etc.) |
| `tool.denied` | An agent attempted to use a tool that was not in its allow-list |

### Authentication

| Event Type | Description |
|------------|-------------|
| `auth.login` | A user successfully logged in |
| `auth.failed` | A login attempt failed (wrong password, unknown email) |
| `auth.logout` | A user logged out |

### Agent management

| Event Type | Description |
|------------|-------------|
| `agent.created` | A new agent was created |
| `agent.updated` | An agent's settings or permissions were changed |
| `agent.deleted` | An agent was deleted |

### User management

| Event Type | Description |
|------------|-------------|
| `user.invited` | An admin invited a new user |
| `user.updated` | A user's profile or role was changed |
| `user.deleted` | A user account was deleted |

### Configuration

| Event Type | Description |
|------------|-------------|
| `config.changed` | A system configuration setting was changed (e.g., provider API key) |

### What is NOT logged

**Chat messages are not logged in the audit trail.** The audit trail records actions and events, not conversation content. Chat messages are stored separately in the conversation history managed by OpenClaw.

## How HMAC signing works

Each audit log entry is signed with **HMAC-SHA256** to ensure integrity:

1. When an audit event occurs, Pinchy constructs a payload from the entry's fields (event type, actor, timestamp, metadata).
2. The payload is signed using a server-side HMAC secret.
3. The resulting signature is stored alongside the entry in the `hmac` column.
4. The HMAC secret is auto-generated at startup if the `AUDIT_HMAC_SECRET` environment variable is not set.

If anyone modifies a row directly in the database, the HMAC signature will no longer match — and integrity verification will flag the tampered entry.

## How to verify integrity

Admins can verify the integrity of the audit log in two ways:

### Via the admin UI

1. Navigate to the **Audit** page in the admin area.
2. Click the **Verify Integrity** button.
3. Pinchy recomputes HMAC signatures for all entries and reports any mismatches.

### Via the API

Send a `GET` request to `/api/audit/verify`. Optional `fromId` and `toId` parameters let you verify a specific range of entries.

```bash
curl -b session_cookie https://your-pinchy-instance/api/audit/verify
```

The response indicates whether all entries are intact:

```json
{
  "verified": true,
  "entriesChecked": 142,
  "tamperedEntries": []
}
```

If tampered entries are found, `verified` is `false` and `tamperedEntries` contains the IDs of the affected rows.

## CSV export for compliance

The audit log can be exported as a CSV file for external compliance tools, auditors, or archival:

### Via the admin UI

1. Navigate to the **Audit** page.
2. Apply any desired filters (date range, event type, user).
3. Click **Export CSV**.

### Via the API

Send a `GET` request to `/api/audit/export` with optional filter parameters (`eventType`, `actorId`, `from`, `to`).

```bash
curl -b session_cookie "https://your-pinchy-instance/api/audit/export?from=2026-01-01&to=2026-02-01" -o audit-log.csv
```

## Immutability guarantees

The audit trail uses multiple layers to ensure entries cannot be modified:

1. **PostgreSQL triggers** — `BEFORE UPDATE` and `BEFORE DELETE` triggers on the `auditLog` table raise an exception, preventing any modification or deletion at the database level.
2. **HMAC signatures** — Even if triggers were somehow bypassed, any modification would invalidate the cryptographic signature.
3. **Append-only API** — The application code only inserts entries. There is no update or delete endpoint for audit entries.

## Fire-and-forget pattern

Audit logging uses a fire-and-forget pattern: if logging fails (e.g., database connection issue), the main operation still succeeds. This ensures that audit logging never degrades the user experience or blocks critical operations.

The trade-off is that in rare failure scenarios, an action might not be logged. For most enterprise deployments, this is preferable to having audit logging cause outages.

## Access control

Only **admins** can access the audit trail — both the UI page and the API endpoints. Regular users cannot view, verify, or export audit entries.

---

# Create a Knowledge Base Agent
URL: https://docs.heypinchy.com/guides/create-knowledge-base-agent/

This guide walks you through creating a Knowledge Base agent — an agent that can read and answer questions from your documents, but cannot access anything else on the system.

## Prerequisites

- Pinchy is running (see [Quick Start](/getting-started/))
- You have mounted at least one data directory (see [Mount Data Directories](/guides/mount-data-directories/))
- You are logged in as an admin

## 1. Mount your data directories

Before creating a Knowledge Base agent, make sure the documents you want the agent to access are mounted under `/data/` in the Pinchy container. See the [Mount Data Directories](/guides/mount-data-directories/) guide for detailed instructions.

## 2. Navigate to "New Agent"

In the Pinchy sidebar, click the **+** button next to "Agents" to open the agent creation form.

## 3. Select the "Knowledge Base" template

Choose **Knowledge Base** from the template options. This template pre-configures the agent with safe file access tools (list and read files in approved directories).

## 4. Enter a name and create

Give your agent a descriptive name, for example "HR Policy Assistant" or "Engineering Docs", then click **Create**.

The agent is created and you are taken to its chat page. The agent has safe file tools enabled by default (from the Knowledge Base template), but it does not yet know which directories to access. You need to configure that in the Permissions tab.

## 5. Configure directory access

Open the agent's settings by clicking the gear icon, then select the **Permissions** tab.

Under **Safe Tools**, you will see that "List approved directories" and "Read approved files" are already enabled (because the Knowledge Base template includes them). Below the safe tools checkboxes, the **Allowed Directories** picker shows all directories found under `/data/`.

Select the directories that contain the documents this agent should be able to read. For example, if you mounted your HR policies at `/data/hr-policies` and your engineering handbook at `/data/engineering`, select one or both.

Click **Save** to apply the permissions.

## 6. Chat with the agent

Go back to the agent's chat and start asking questions. The agent can:

- List files in the directories you selected
- Read the contents of those files
- Answer questions based on what it reads

The agent **cannot**:

- Access files outside the selected directories
- Execute shell commands
- Make network requests
- Read system files

## Changing permissions later

You can update an agent's permissions at any time by returning to the **Permissions** tab. Add or remove directories, or enable additional tools as needed. Changes take effect after clicking **Save**.

## How it works

Under the hood, Pinchy uses an allow-list approach. The agent starts with no capabilities and only gets the tools an admin explicitly enables. For a full explanation, see [Agent Permissions](/concepts/agent-permissions/).

---

# Mount Data Directories
URL: https://docs.heypinchy.com/guides/mount-data-directories/

Pinchy agents can only access files inside the `/data/` directory in the container. To make your documents available, you mount host directories into subdirectories of `/data/` using Docker volumes.

## The `/data/` convention

All agent-accessible files live under `/data/` inside the Pinchy container. This path is a Docker volume by default (`pinchy-data`), but you can bind-mount additional host directories as subdirectories.

This convention provides a clear boundary: anything under `/data/` is potentially accessible to agents (depending on their permissions), and nothing outside `/data/` is.

## Adding volumes in `docker-compose.yml`

To mount a local folder, add a bind mount under the `pinchy` service's `volumes` section:

```yaml
services:
  pinchy:
    volumes:
      - openclaw-config:/openclaw-config
      - pinchy-data:/data
      - openclaw-extensions:/openclaw-extensions
      # Add your data directories here:
      - /path/on/host/hr-policies:/data/hr-policies:ro
      - /path/on/host/engineering-docs:/data/engineering-docs:ro
```

The `:ro` suffix mounts the directory as read-only, which is recommended since agents only need to read files.

After updating `docker-compose.yml`, restart the stack:

```bash
docker compose down && docker compose up -d
```

## Examples

### Local folder

Mount a folder from your host machine:

```yaml
volumes:
  - /home/admin/company-docs:/data/company-docs:ro
```

### NAS or network drive

If your NAS is mounted on the host at `/mnt/nas/shared`, mount a subdirectory:

```yaml
volumes:
  - /mnt/nas/shared/legal:/data/legal:ro
  - /mnt/nas/shared/finance:/data/finance:ro
```

### Multiple directories

You can mount as many directories as you need:

```yaml
volumes:
  - /srv/docs/hr:/data/hr:ro
  - /srv/docs/engineering:/data/engineering:ro
  - /srv/docs/compliance:/data/compliance:ro
```

Each mounted directory appears as a separate selectable option in the Permissions tab when configuring an agent.

## How directories appear in the UI

When you configure an agent's permissions in the **Permissions tab** of Agent Settings, Pinchy scans `/data/` for subdirectories and displays them as selectable options. Each mounted directory shows up by name — for example, `/data/hr-policies` appears as "hr-policies".

Only top-level subdirectories of `/data/` are listed. Nested directories within those are accessible to the agent but are not listed separately in the directory picker.

## Verifying your mounts

To check that your directories are correctly mounted, you can exec into the running container:

```bash
docker compose exec pinchy ls /data/
```

You should see your mounted directories listed.

---

# API Reference
URL: https://docs.heypinchy.com/reference/api/

All API endpoints require authentication unless noted otherwise. Unauthenticated requests receive a `401 Unauthorized` response. Authenticate by including a valid session cookie (set during login via the web UI).

Some endpoints are **admin only** — non-admin users receive a `403 Forbidden` response.

## WebSocket (Chat)

Agent chat is handled over WebSocket at `/api/ws`, not via REST. The browser sends JSON messages and receives streaming responses. For details on the WebSocket protocol, message types, and session management, see the [Architecture](/architecture/#request-flow) page.

## Agents

### `GET /api/agents`

List agents visible to the current user. Admins see all agents. Non-admin users see shared agents and their own personal agents.

**Response:**

```json
[
  {
    "id": "uuid",
    "name": "HR Policy Assistant",
    "model": "anthropic/claude-haiku-4-5-20251001",
    "templateId": "knowledge-base",
    "allowedTools": ["pinchy_ls", "pinchy_read"],
    "pluginConfig": {
      "allowed_paths": ["/data/hr-policies"]
    },
    "isPersonal": false,
    "ownerId": "user-uuid",
    "createdAt": "2025-01-15T10:00:00.000Z",
    "updatedAt": "2025-01-15T10:00:00.000Z"
  }
]
```

### `POST /api/agents`

Create a new agent. The agent inherits default tool permissions from its template. Use `PATCH /api/agents/:id` to configure permissions after creation.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Display name for the agent |
| `templateId` | string | Yes | Template to use (`knowledge-base` or `custom`) |

**Example — Knowledge Base agent:**

```json
{
  "name": "HR Policy Assistant",
  "templateId": "knowledge-base"
}
```

**Example — Custom agent:**

```json
{
  "name": "General Assistant",
  "templateId": "custom"
}
```

**Response:** `201 Created` with the created agent object.

**Errors:**
- `400` — missing or invalid `name` or `templateId`
- `401` — not authenticated

### `GET /api/agents/:id`

Get a single agent by ID.

**Response:** The agent object, or `404` if not found.

### `PATCH /api/agents/:id`

Update an agent's settings. Any combination of fields can be included.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | New display name |
| `model` | string | No | New model identifier |
| `allowedTools` | string[] | No | List of tool IDs the agent can use (**admin only**) |
| `pluginConfig` | object | No | Plugin configuration, e.g. `{ "allowed_paths": ["/data/hr"] }` (**admin only**) |

**Example — update permissions:**

```json
{
  "allowedTools": ["pinchy_ls", "pinchy_read"],
  "pluginConfig": {
    "allowed_paths": ["/data/hr-policies"]
  }
}
```

**Response:** The updated agent object.

**Errors:**
- `400` — cannot change permissions for personal agents
- `401` — not authenticated
- `403` — only admins can change `allowedTools` or `pluginConfig`
- `404` — agent not found

### `DELETE /api/agents/:id`

Delete an agent. **Admin only.** Personal agents (auto-created Smithers) cannot be deleted.

**Response:** `200 OK`

```json
{
  "success": true
}
```

**Errors:**
- `400` — attempting to delete a personal agent
- `401` — not authenticated
- `403` — not an admin
- `404` — agent not found

## Templates

### `GET /api/templates`

List available agent templates.

**Response:**

```json
{
  "templates": [
    {
      "id": "knowledge-base",
      "name": "Knowledge Base",
      "description": "Answer questions from your docs"
    },
    {
      "id": "custom",
      "name": "Custom Agent",
      "description": "Start from scratch"
    }
  ]
}
```

## Data Directories

### `GET /api/data-directories`

List directories available under `/data/` for agent configuration.

**Response:**

```json
{
  "directories": [
    { "path": "/data/hr-policies", "name": "hr-policies" },
    { "path": "/data/engineering-docs", "name": "engineering-docs" }
  ]
}
```

Returns an empty array if `/data/` does not exist or contains no subdirectories. Hidden directories (starting with `.`) are excluded.

## Settings

### `GET /api/settings`

Get all application settings. Encrypted values (such as API keys) are masked in the response.

**Response:**

```json
[
  { "key": "default_provider", "value": "anthropic", "encrypted": false },
  { "key": "anthropic_api_key", "value": "--------", "encrypted": true }
]
```

### `POST /api/settings`

Update a setting.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | string | Yes | Setting key |
| `value` | string | Yes | Setting value |

Settings with `api_key` in the key name are automatically encrypted at rest.

**Response:** `{ "success": true }`

## Users (admin only)

### `GET /api/users`

List all users.

**Response:**

```json
[
  {
    "id": "uuid",
    "name": "Alice",
    "email": "alice@example.com",
    "role": "admin",
    "createdAt": "2025-01-15T10:00:00.000Z"
  }
]
```

### `DELETE /api/users/:id`

Delete a user and cascade-delete their personal agents.

**Response:** `200 OK`

```json
{
  "success": true
}
```

**Errors:**
- `400` — attempting to delete yourself
- `401` — not authenticated
- `403` — not an admin
- `404` — user not found

### `POST /api/users/:id/reset`

Generate a password reset token for a user. The admin constructs the reset URL from the returned token: `{origin}/invite/{token}`.

**Response:** `201 Created`

```json
{
  "token": "abc123..."
}
```

**Errors:**
- `401` — not authenticated
- `403` — not an admin
- `404` — user not found

## Invites (admin only)

### `POST /api/users/invite`

Create an invite for a new user. Returns the full invite object including a plaintext token (shown only once).

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `role` | string | Yes | Role for the invited user (`admin` or `user`) |
| `email` | string | No | Email address of the invited user (optional, for reference) |

**Response:** `201 Created`

```json
{
  "id": "uuid",
  "token": "abc123...",
  "email": "bob@example.com",
  "role": "user",
  "type": "invite",
  "expiresAt": "2025-01-22T10:00:00.000Z",
  "createdAt": "2025-01-15T10:00:00.000Z"
}
```

The admin constructs the invite URL from the returned token: `{origin}/invite/{token}`.

**Errors:**
- `400` — missing or invalid role
- `401` — not authenticated
- `403` — not an admin

### `GET /api/users/invites`

List all invites and their status.

**Response:**

```json
[
  {
    "id": "uuid",
    "email": "bob@example.com",
    "status": "pending",
    "expiresAt": "2025-01-22T10:00:00.000Z",
    "createdAt": "2025-01-15T10:00:00.000Z"
  }
]
```

### `DELETE /api/users/invites/:id`

Revoke a pending invite.

**Response:** `200 OK`

```json
{
  "success": true
}
```

**Errors:**
- `401` — not authenticated
- `403` — not an admin
- `404` — invite not found

## Invite Claim (public)

### `POST /api/invite/claim`

Claim an invite token to create a new account or reset a password. **This endpoint does not require authentication.**

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | string | Yes | The invite/reset token from the URL |
| `name` | string | Yes | Display name for the new user |
| `password` | string | Yes | Password (min 8 characters) |

**Response:** `201 Created`

```json
{
  "success": true
}
```

**Errors:**
- `400` — missing fields, invalid token, or token expired

## Audit Trail (admin only)

### `GET /api/audit`

Retrieve a paginated, filterable audit log. **Admin only.**

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | number | 1 | Page number |
| `limit` | number | 50 | Entries per page |
| `eventType` | string | — | Filter by event type (e.g., `auth.login`, `tool.execute`) |
| `actorId` | string | — | Filter by user ID |
| `from` | string | — | Start date (ISO 8601) |
| `to` | string | — | End date (ISO 8601) |

**Response:**

```json
{
  "entries": [
    {
      "id": "uuid",
      "eventType": "auth.login",
      "actorId": "user-uuid",
      "actorEmail": "alice@example.com",
      "metadata": {},
      "hmac": "sha256-hex-string",
      "createdAt": "2026-02-21T10:00:00.000Z"
    }
  ],
  "total": 142,
  "page": 1,
  "limit": 50
}
```

**Errors:**
- `401` — not authenticated
- `403` — not an admin

### `GET /api/audit/verify`

Verify the integrity of audit log entries by recomputing HMAC signatures. **Admin only.**

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fromId` | string | — | Start verification from this entry ID (optional) |
| `toId` | string | — | End verification at this entry ID (optional) |

**Response:**

```json
{
  "verified": true,
  "entriesChecked": 142,
  "tamperedEntries": []
}
```

If tampered entries are found, `verified` is `false` and `tamperedEntries` contains the IDs of entries with mismatched signatures.

**Errors:**
- `401` — not authenticated
- `403` — not an admin

### `GET /api/audit/export`

Export the audit log as a CSV file. Supports the same filters as `GET /api/audit`. **Admin only.**

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `eventType` | string | — | Filter by event type |
| `actorId` | string | — | Filter by user ID |
| `from` | string | — | Start date (ISO 8601) |
| `to` | string | — | End date (ISO 8601) |

**Response:** `200 OK` with `Content-Type: text/csv` and `Content-Disposition: attachment; filename="audit-log.csv"`.

**Errors:**
- `401` — not authenticated
- `403` — not an admin

## User Self-Service

### `PATCH /api/users/me`

Update your own display name.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | New display name |

**Response:**

```json
{
  "success": true
}
```

**Errors:**
- `400` — missing or empty name
- `401` — not authenticated

### `POST /api/users/me/password`

Change your own password.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `currentPassword` | string | Yes | Current password |
| `newPassword` | string | Yes | New password (min 8 characters) |

**Response:** `{ "success": true }`

**Errors:**
- `400` — missing fields or new password too short
- `401` — not authenticated or current password incorrect

---

