No description
  • TypeScript 96.4%
  • JavaScript 3.3%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Bob Parsons ec30632110
All checks were successful
Deploy / deploy (push) Successful in 1m59s
Merge pull request 'Promote develop: paperclip 2026.1005.0' (#2) from develop into main
Reviewed-on: ⁨#2⁩
2026-10-09 11:31:35 -05:00
.forgejo/workflows mail-bridge: containerize and deploy alongside Paperclip 2026-09-26 21:08:21 -05:00
docs Add kirria-email Paperclip skill and console installer 2026-09-26 23:10:31 -05:00
mail-bridge mail-bridge: sign wakes with hmac_sha256 instead of bearer 2026-09-26 22:46:03 -05:00
skills Add kirria-email Paperclip skill and console installer 2026-09-26 23:10:31 -05:00
.env.example Deploy Paperclip at team.bobparsons.dev 2026-09-26 09:14:58 -05:00
.gitignore mail-bridge: scaffold package and config loader 2026-09-26 21:05:49 -05:00
CLAUDE.md docs: known wedge + reconnect recipe for expiring Claude connector 2026-09-27 08:15:10 -05:00
docker-compose.yml chore(deps): update paperclip to 2026.1005.0 2026-10-09 11:31:33 -05:00
README.md docs: mail-bridge runbook for adding a mailbox 2026-09-26 22:58:21 -05:00
renovate.json chore: onboard to Renovate 2026-10-09 11:31:27 -05:00

paperclip

Paperclip at https://team.bobparsons.dev. Official image, shared pg18 for the database, Traefik for routing (behind the Cloudflare tunnel), secrets from Infisical. Push to main deploys.

Runs authenticated + private so the agents can sign in with your AI subscriptions in the browser, which Paperclip turns off for public. The internet is kept out by Cloudflare Access (owner email only) plus a Traefik IP allowlist that stops anyone going around Access via the WAN IP.

One-time setup

  1. Database on the shared pg18:
    ssh server 'docker exec -i postgres18 psql -U postgres' <<'SQL'
    CREATE USER paperclip WITH ENCRYPTED PASSWORD '<pw>';
    CREATE DATABASE paperclip OWNER paperclip;
    SQL
    
    Paperclip runs its migrations on startup.
  2. Infisical: create a project with a prod env, grant the deploy machine identity read access, and add the keys from .env.example (DATABASE_URL=postgres://paperclip:<pw>@postgres18:5432/paperclip).
  3. Forgejo: add the repo variable INFISICAL_PROJECT_ID.
  4. Cloudflare: add a proxied CNAME team pointing at the tunnel (0f3f1dd3-e3ec-42b3-835c-b05dcc6c2354.cfargotunnel.com). The tunnel's *.bobparsons.dev route already sends traffic to Traefik.

First admin

With private exposure you can also claim the instance from the setup screen in the browser. Otherwise the admin comes from a CLI invite. The CLI refuses to run without a config file, but the server is configured only through env. Give the CLI a throwaway config in /tmp. Don't write one to /paperclip, because the server reads it too.

ssh server 'docker exec -u node -w /app paperclip sh -c '"'"'
cat > /tmp/boot.json <<EOF
{"\$meta":{"version":1,"updatedAt":"2026-01-01T00:00:00Z","source":"onboard"},
 "database":{"mode":"postgres"},"logging":{"mode":"file"},
 "server":{"deploymentMode":"authenticated","exposure":"public"},
 "auth":{"baseUrlMode":"explicit","publicBaseUrl":"https://team.bobparsons.dev"}}
EOF
pnpm --silent paperclipai auth bootstrap-ceo --config /tmp/boot.json \
  --expires-hours 24 --base-url https://team.bobparsons.dev; rm /tmp/boot.json'"'"''

Add --force to issue a new invite after an admin already exists. Open the invite URL while signed in. Then set PAPERCLIP_AUTH_DISABLE_SIGN_UP=true in Infisical and redeploy.

The startup banner shows Agent JWT missing. Ignore it: agent JWTs fall back to BETTER_AUTH_SECRET, and only the banner checks for the dedicated variable.

AI subscription logins

When Paperclip shows a login command (for example codex ... login --device-auth), run it inside the container as node. The credentials have to land in the server's /paperclip volume:

ssh -t server docker exec -it -u node paperclip sh -c '<command from the UI>'

Approve the device code in your browser, then return to the Paperclip screen. It imports the login and deletes the staging directory under ai-local-logins/, so seeing that directory gone afterwards is normal.

Data

The paperclip-data volume (/paperclip) holds uploads, agent workspaces, agent CLI logins and the secrets master key. Back it up together with the paperclip database, because stored secrets can't be decrypted without the key.

Mail bridge

The mail bridge watches Purelymail inboxes, wakes the owning Paperclip agent through a routine webhook, and exposes mailbox-scoped MCP tools for reading, triage, and approved sends. It keeps triage and send state in the mail-bridge-data volume (/data); message bodies are fetched live.

To add a mailbox, follow docs/mail-bridge-runbook.md. It has copy-paste console snippets and a troubleshooting table.

For older mail, run a bounded backfill. For an uncertain send, check the recipient side for its stored rfcMessageId before resolving it. Never resend under a new key without that check.

docker exec mail-bridge node src/cli.ts backfill <mailbox> <YYYY-MM-DD> [max=50]
docker exec mail-bridge node src/cli.ts resolve-send <mailbox> <idempotencyKey> sent|failed

A push that changes only mail-bridge/ rebuilds only the bridge. Changes to the paperclip service block, including this first rollout's network addition, recreate Paperclip and interrupt agent runs. Push those while no agents are running.