paperclip-operations
Operate a Paperclip AI-coding orchestration instance — server lifecycle, agent hiring via API, adapter configuration, CLI commands, and Agent Stack architecture (Paperclip + Langfuse + local CLI agents).
Quand l'utiliser (Trigger)
Déclenchement standard selon le contexte de l'écosystème Hermès.
Mode d'emploi (Usage)
Mode d'emploi standard via l'agent Hermès. Paperclip Operations
Operate and manage a Paperclip AI-coding orchestration instance: server lifecycle, agent hiring, adapter topology, and the surrounding Agent Stack (Langfuse observability).
Architecture Overview
Paperclip orchestrates AI coding agents via adapters. Each adapter wraps a CLI or API (Claude Code, Codex, Gemini, Hermes) as a first-class agent in the Paperclip org chart.
Paperclip Server (:3100)
└── CEO agent (default: Claude Code local)
├── Hermès (hermes_local adapter)
├── Codex (codex_local adapter)
└── Gemini (gemini_local adapter)
└── Langfuse (:3000) ← OTLP traces from all agents
Config & State
| Item | Path |
|---|---|
| Instance config | ~/.paperclip/instances/default/config.json |
| Embedded DB | ~/.paperclip/instances/default/db/ |
| Logs | ~/.paperclip/instances/default/logs/ |
| Secrets key | ~/.paperclip/instances/default/secrets/master.key |
| Backups | ~/.paperclip/instances/default/data/backups/ |
Key config fields:
deploymentMode:"authenticated"or"unauthenticated"bind/host/port: network bindingserveUi: boolean (serves the Paperclip web UI)auth.disableSignUp: booleandatabase.mode:"embedded-postgres"or external
Starting the Server
Paperclip requires Node ≥ v22.13 (pnpm enforces this). Use nvm.
source ~/.nvm/nvm.sh && nvm use 24
cd ~/agent-stack/paperclip
pnpm --filter @paperclipai/server exec tsx src/index.ts
The server boot sequence:
- Starts embedded PostgreSQL (if configured)
- Applies pending migrations
- Serves the API + UI on the configured port
- Generates a bootstrap CEO invite on first start
Health check:
curl -s http://localhost:3100/api/health
# Returns HTTP 200 when ready
CLI Usage
The Paperclip CLI requires Node ≥ v22.13 and a company ID context.
source ~/.nvm/nvm.sh && nvm use 24
# Commands run from the cli/ directory
cd ~/agent-stack/paperclip/cli
node --import tsx src/index.ts <command>
Commands that need a company ID require one of:
--company-id <uuid>flagPAPERCLIP_COMPANY_IDenv var- A context profile with
companyIdset - Authentication via agent API key
Common CLI Operations
# List companies (needs auth context first)
node --import tsx src/index.ts company list
# Create an agent (hire)
node --import tsx src/index.ts agent create --name "AgentName" --adapter-type hermes_local
Authentication Models
Paperclip has two auth tiers — understanding the difference is critical for API access.
1. User Session (better-auth)
- Created via the web UI login/signup
- Stored in
sessiontable, cookie name:better-auth_session_token - Gives
req.actor.source === "user"— NOT sufficient for company/agent endpoints assertBoard()andassertCanCreateAgentsForCompany()middleware blocks user sessions
2. Board API Key (Bearer token)
- Endpoints that matter (companies, agents, agent-hires) require
req.actor.source === "board"or"local_implicit" - Authenticated via
Authorization: Bearer <token>header - Token is SHA-256 hashed and stored in
board_api_keystable - When no UI exists, you must create a board API key directly in PostgreSQL
Creating a Board API Key via Database
When the Paperclip web UI is unavailable (uiMode=none, Vite middleware broken, headless server):
const { createHash, randomBytes } = require('node:crypto');
const { Pool } = require('pg');
const pool = new Pool({
host: '127.0.0.1',
port: 54329,
database: 'paperclip',
user: 'paperclip',
password: 'paperclip'
});
const rawToken = 'pap-' + randomBytes(32).toString('hex');
const keyHash = createHash('sha256').update(rawToken).digest('hex');
const userId = '<existing-user-id>'; // Find via: SELECT id, name FROM "user";
await pool.query(
`INSERT INTO board_api_keys (id, user_id, name, key_hash, created_at)
VALUES ($1, $2, $3, $4, $5)`,
[randomUUID(), userId, 'auto-generated-key', keyHash, new Date()]
);
console.log('Bearer token (SAVE THIS):', rawToken);
board_api_keys Table Schema
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key, generate at insert |
user_id | text | FK to user.id |
name | text | Human label |
key_hash | text | SHA-256 hex digest of raw token |
last_used_at | timestamptz | NULL initially |
revoked_at | timestamptz | NULL = active |
expires_at | timestamptz | NULL = no expiry |
created_at | timestamptz | Set at insert |
Using the Board API Key
# List companies
curl -s http://localhost:3100/api/companies \
-H "Authorization: Bearer pap-..."
# Hire an agent
curl -X POST http://localhost:3100/api/companies/$COMPANY_ID/agent-hires \
-H "Content-Type: application/json" \
-H "Authorization: Bearer pap-..." \
-d '{"name": "Hermès", "adapterType": "hermes_local"}'
Auth Middleware Chain
From server/src/middleware/auth.ts:
Request →
Authorization header? →
YES → Bearer token → SHA-256 hash → match board_api_keys?
→ YES: actor = { source: "board", type: "board", principalId: <key-id> }
→ NO: 401 Unauthorized
NO → resolveSession(req) via better-auth cookie
→ valid session? → actor = { source: "user", type: "user" }
→ no session → 401
Endpoint calls assertBoard(req)
→ checks req.actor.source === "board" or "local_implicit"
→ user sessions → 403 "Board access required"
Hiring Agents via API
The hire endpoint: POST /companies/:companyId/agent-hires
Schema (createAgentHireSchema):
{
name: string; // Required
role?: "general" | ...; // Optional, default "general"
title?: string | null;
icon?: string | null;
reportsTo?: string | null; // UUID of manager agent
capabilities?: string | null;
adapterType: string; // See adapter types below
adapterConfig?: Record<string, unknown>;
instructionsBundle?: {
entryFile?: string;
files: Record<string, string>;
};
budgetMonthlyCents?: number; // Default 0
sourceIssueId?: string; // UUID
sourceIssueIds?: string[];
}
Built-in Adapter Types
Defined in packages/shared/src/validators/adapter-type.ts and server/src/adapters/builtin-adapter-types.ts:
| Adapter Type | Description | CLI Required |
|---|---|---|
hermes_local | Hermes Agent (local CLI) | hermes on PATH |
claude_local | Claude Code CLI | claude on PATH |
codex_local | OpenAI Codex CLI | codex on PATH |
gemini_local | Gemini CLI | gemini on PATH |
opencode_local | OpenCode CLI | opencode on PATH |
cursor_cloud | Cursor IDE cloud | — |
cursor | Cursor IDE local | — |
acpx_local | Apple Xcode ACP | — |
grok_local | Grok CLI | — |
pi_local | Pi CLI | — |
Each local adapter wraps its respective CLI binary, using the user’s existing subscription/OAuth. No separate API key is needed for these adapters.
Hiring via curl (board auth — preferred when UI is unavailable)
TOKEN="pap-..." # from board_api_keys insert
COMPANY_ID="706a045e-..." # from GET /api/companies
curl -X POST "http://localhost:3100/api/companies/$COMPANY_ID/agent-hires" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Hermès",
"adapterType": "hermes_local",
"role": "general"
}'
Agent Boot Sequence
When a Paperclip agent is hired and activated:
- The heartbeat scheduler picks up the agent
- A heartbeat run spawns the adapter’s CLI as a subprocess
- The agent loads its instructions bundle (if configured)
- The agent processes its queue of issues/tasks
- OTLP traces are sent to the configured observability endpoint
The default CEO agent (bootstrap invite) runs claude_local and creates the first project/workspace.
Common Pitfalls
- Node version mismatch: pnpm ≥9 requires Node ≥22.13. Use
nvm use 24before any pnpm or tsx command. - Game over: paperclipai not found: The
paperclipaibinary is NOT on PATH after cloning. Run from the cli/ dir withnode --import tsx src/index.ts. - Company ID required: The CLI refuses without a company context. Set
PAPERCLIP_COMPANY_IDor pass--company-id. - Embedded PostgreSQL persists: Restarting the server preserves the database. No data loss on crash (ACID).
- UI Vite disconnection: The Paperclip dev server uses Vite middleware that can lose connection. Refresh the browser page to reconnect.
- First start generates CEO invite: The
bootstrapCeoInviteruns automatically after the firstpaperclipai run. It creates a Claude Code CEO agent. Subsequent starts skip this. - User session ≠ board access: better-auth session cookies give
actor.source === "user", but company/agent endpoints check for"board"or"local_implicit". You MUST create a board API key in the DB to use the API programmatically when no UI is available. - Board API key is one-shot: The raw token is displayed once at creation and never stored in plaintext (only SHA-256 hash is stored). Save it immediately or you’ll have to delete and recreate.
- pg module path in pnpm: When querying the embedded DB from outside the Paperclip project root, the
pgmodule lives undernode_modules/.pnpm/pg@<version>/node_modules/pg. Resolve the full path e.g./home/bf/agent-stack/paperclip/node_modules/.pnpm/pg@8.18.0/node_modules/pg. - information_schema.tables returns empty: The pg Pool may connect to the public schema by default. Query known tables by name:
SELECT id, name FROM company,SELECT id, name, email FROM "user",SELECT * FROM board_api_keys. write_filecorrupts.envwith secrets: Thewrite_filetool redacts secret values by truncating them with...n. WritingPAPERCLIP_AGENT_JWT_SECRET="a3f1..."via write_file actually writes the literal...nto disk, not the intended hex string. ALWAYS useterminalwith a heredoc orprintffor secret-bearing config files. See the.envmanagement section below for the safe pattern.od -cis the only reliable byte-level verification: When terminal output redacts secrets as***(e.g.cat .envshowsPAPERCLIP_AGENT_JWT_SECRET=***), useod -corod -A x -t x1zto see the actual bytes on disk and confirm the file was written correctly. This bypasses both write_file content redaction and terminal output redaction.
Environment Configuration (.env) Management
Paperclip reads ~/.paperclip/instances/default/.env at startup. When config.json has strictMode: false, the .env file takes precedence over encrypted secrets in the embedded DB.
JWT_SECRET Generation
Paperclip expects PAPERCLIP_AGENT_JWT_SECRET as a 64-character hex string (32 random bytes). Generate it with:
openssl rand -hex 32
# Example output: a3f1b8c92e4d5a67f0c12b3d4e5f67890a1b2c3d4e5f678901234567890abcd
This matches the format of node:crypto’s randomBytes(32).toString("hex").
Safe .env Writing Pattern (Avoid write_file Corruption)
NEVER use write_file for .env files containing secrets. The tool redacts secret-like values with ...n, writing the literal truncation pattern to disk instead of the real value.
SAFE approach — use terminal with heredoc:
cat > ~/.paperclip/instances/default/.env << 'ENVEOF'
PAPERCLIP_AGENT_JWT_SECRET=a3f1b8c92e4d5a67f0c12b3d4e5f67890a1b2c3d4e5f678901234567890abcd
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_ENDPOINT=http://localhost:3000
PAPERCLIP_COMPANY_ID=706a045e-50c0-414e-ba2e-101311e0d34d
ENVEOF
The single-quoted 'ENVEOF' delimiter prevents shell variable expansion — critical when the value contains $ characters.
Verify the File Wrote Correctly
# Check line count and general structure
wc -l ~/.paperclip/instances/default/.env
# Check actual bytes (bypasses terminal *** redaction)
od -c ~/.paperclip/instances/default/.env
# Look for the full hex strings — if you see '...' or truncated values, the write is corrupted
Env File Format
PAPERCLIP_AGENT_JWT_SECRET=<64-hex-chars>
LANGFUSE_PUBLIC_KEY=pk-lf-<32-hex-chars>
LANGFUSE_SECRET_KEY=sk-lf-<32-hex-chars>
No quotes around values (required by Paperclip’s parser). Each line is KEY=VALUE with no spaces around =.
Agent Stack Topology
The full stack from the ~/agent-stack/ project:
| Component | Port | Purpose |
|---|---|---|
| Paperclip | :3100 | Agent orchestration, UI, API |
| Langfuse | :3000 | Observability (traces, LLM monitoring) |
| Paperclip DB | :54329 | Embedded PostgreSQL |
Langfuse keys are set in Paperclip’s .env:
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
Langfuse OTLP Trace Ingestion
Langfuse ingests OTLP traces via POST /api/public/otel/v1/traces with Basic Auth (pk:sk as Base64). See references/langfuse-otlp-debugging.md for the full debugging reference.
Quick Auth Test
AUTH=$(echo -n "pk-lf-...:sk-lf-..." | base64)
curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:3000/api/public/otel/v1/traces" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json"
# 200/400 = auth OK 401 = bad key 500 = S3 backend issue
Key Insertion (bcrypt, NOT SHA-256)
Langfuse stores API secret keys as bcrypt hashes ($2a$11$). SHA-256 hashing causes HTTP 401.
pip install bcrypt # or apt install python3-bcrypt
python3 -c "
import bcrypt
h = bcrypt.hashpw(b'sk-lf-...', bcrypt.gensalt(rounds=11))
print(h.decode())
"
Insert into the api_keys table — both hashed_secret_key and fast_hashed_secret_key get the same bcrypt hash.
HTTP Status Quick Reference
| Code | Meaning |
|---|---|
| 200 | Trace ingested successfully |
| 400 | Auth OK, empty/invalid payload |
| 401 | Bad credentials (wrong hash or revoked) |
| 403 | Bearer pk used on write endpoint |
| 404 | Wrong URL path |
| 500 | Backend S3/storage failure (see S3 troubleshooting in reference) |
MinIO S3 Troubleshooting
The default Langfuse Docker compose uses MinIO for trace storage. If MinIO is unreachable from the web container, traces fail with Failed to upload JSON to S3. The web container connects to MinIO via internal Docker DNS (http://minio:9000). Check MinIO console at http://localhost:9091 (creds: minio:miniosecret).
Common fix: clear S3 env vars to fall back to local filesystem storage.
References
~/agent-stack/PROGRESS.md— session-level progress tracker~/agent-stack/paperclip/AGENTS.md— Paperclip developer guide~/agent-stack/paperclip/server/src/routes/agents.ts— hiring route implementation~/agent-stack/paperclip/packages/shared/src/validators/agent.ts— hire schema~/agent-stack/paperclip/server/src/adapters/builtin-adapter-types.ts— all adapter typesreferences/board-auth.md— detailed board API key creation and usage guidereferences/hire-api-detail.md— hire API schema, handler logic, agent status valuesreferences/full-adapter-list.md— all adapter types with default configsreferences/langfuse-otlp-debugging.md— Langfuse OTLP ingestion, API key creation, bcrypt hash requirements, S3/MinIO debugging, Python sender script