From 30931d3f50cc6b1565bd4a9df0eea74b424ede34 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 25 Sep 2026 13:48:29 +0200 Subject: [PATCH 1/3] feat(paperclip): disable sign-up; document the Claude connection The first admin is claimed, so PAPERCLIP_AUTH_DISABLE_SIGN_UP flips to true. Spec: Claude auth is a Paperclip-managed subscription connection created once via the local sign-in. The server-env CLAUDE_CODE_OAUTH_TOKEN was never used (stripAiAuthBindings blanks it for managed runs; upstream accepts no pasted setup-token), so it is dropped from the spec. The login must run as the node user: exec sessions start as root and root-owned 0600 credentials are unreadable to the server. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-09-25-paperclip-design.md | 22 +++++++++++-------- kubernetes/apps/paperclip/paperclip.yaml | 3 +-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-paperclip-design.md b/docs/superpowers/specs/2026-09-25-paperclip-design.md index 4d0bf181..eb1ddc8e 100644 --- a/docs/superpowers/specs/2026-09-25-paperclip-design.md +++ b/docs/superpowers/specs/2026-09-25-paperclip-design.md @@ -16,7 +16,7 @@ credentials are out of scope (see "Deferred"). |---|---| | Execution model | **B — server-local runs** (upstream default): agent CLIs are child processes of the server, in the server container. Trial only. | | Upgrade path | If Paperclip is kept: **C** — `@paperclipai/plugin-kubernetes` sandbox provider + kubernetes-sigs agent-sandbox controller (`agents.x-k8s.io`), per-run Pods and Secrets. Separate spec. | -| Claude auth | **One shared subscription OAuth token** (`CLAUDE_CODE_OAUTH_TOKEN`, from `claude setup-token`) for all agents. | +| Claude auth | **One shared Claude subscription AI connection**, created once via Paperclip's local sign-in (`claude auth login` into `ai-local-logins/`); Paperclip stores and refreshes it. Server-env `CLAUDE_CODE_OAUTH_TOKEN` is not used: `stripAiAuthBindings` blanks it for managed runs, and upstream accepts no pasted setup-token (the device-code flow needs a Daytona sandbox). | | Bot credentials | Out of scope. Future "AIR-Manager" Paperclip role handles permission requests. | | Exposure | LAN-only, behind an Authentik forward-auth gate, then Paperclip's own login. | @@ -25,8 +25,8 @@ credentials are out of scope (see "Deferred"). Upstream `runChildProcess()` spawns every local adapter with `{...sanitizeInheritedPaperclipEnv(process.env), ...opts.env}`; the sanitizer strips only `PAPERCLIP_*` keys. Every agent therefore sees `DATABASE_URL`, `BETTER_AUTH_SECRET`, -`CLAUDE_CODE_OAUTH_TOKEN`, the shared filesystem (including every other agent's -workspace) and every agent-scoped secret it can read from disk. Under B, no bot may hold +the shared filesystem (including every other agent's workspace and the +`ai-local-logins` credentials) and every agent-scoped secret it can read from disk. Under B, no bot may hold a credential we would not tolerate any other bot using. Per-bot isolation is C's job. ## Architecture @@ -106,7 +106,6 @@ extract): - `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET` - `PAPERCLIP_SECRETS_MASTER_KEY` (64-char hex). Held in OpenBao, not on the PVC, so encrypted agent secrets in Postgres survive PVC loss. -- `CLAUDE_CODE_OAUTH_TOKEN` (shared subscription token) ### Ingress and SSO - Hostname `paperclip.renner.uno`, Traefik `Ingress`, `cert-manager.io/cluster-issuer: @@ -152,10 +151,15 @@ available since v0.35.0). First use of digest reflection in this repo. - Nothing is granted under B that we would not tolerate any agent misusing. ## Bootstrap (one-time) -1. Place `apps/paperclip` values in OpenBao (generated secrets + shared OAuth token). -2. Merge; Flux deploys. -3. Claim the admin account via the `bootstrap-ceo` invite, add yourself to - "Paperclip Users" in Authentik. +1. Place `apps/paperclip` values in OpenBao (the three generated secrets). +2. Merge; Flux deploys with sign-up enabled. +3. Sign up through the gate and claim the instance as admin; then set + `PAPERCLIP_AUTH_DISABLE_SIGN_UP=true`. +4. In onboarding "Connect a model", choose Claude subscription and run the shown login + **as the node user** (the entrypoint makes exec sessions root, and root-owned 0600 + credentials are unreadable to the server): + `kubectl exec -it -n paperclip deploy/paperclip -- gosu node sh -c 'export CLAUDE_CONFIG_DIR= && mkdir -p "$CLAUDE_CONFIG_DIR" && claude auth login'` + Repeat after loss of the PVC or database. ## Verification - Flux Kustomization `paperclip` Ready; CNPG `pclp-pg` healthy; first ScheduledBackup @@ -163,7 +167,7 @@ available since v0.35.0). First use of digest reflection in this repo. - `curl https://paperclip.renner.uno/api/health` through the gate returns healthy after Authentik login; unauthenticated request redirects to `auth.renner.uno`. - Direct pod access from another namespace is denied (CNP). -- A test agent (claude-local) completes a trivial task using the shared OAuth token. +- A test agent (claude-local) completes a trivial task using the shared Claude connection. - Flux ImagePolicy reports a `nightly@sha256:` digest. ## Deferred diff --git a/kubernetes/apps/paperclip/paperclip.yaml b/kubernetes/apps/paperclip/paperclip.yaml index 9225cf10..aaa8f393 100644 --- a/kubernetes/apps/paperclip/paperclip.yaml +++ b/kubernetes/apps/paperclip/paperclip.yaml @@ -49,9 +49,8 @@ spec: value: private - name: PAPERCLIP_PUBLIC_URL value: https://paperclip.renner.uno - # Sign-up stays open until the first admin is claimed; the gate restricts who reaches it. - name: PAPERCLIP_AUTH_DISABLE_SIGN_UP - value: "false" + value: "true" - name: PAPERCLIP_TELEMETRY_DISABLED value: "1" envFrom: -- 2.54.0 From 161d0561e7912a69a0872c9394d2a2a3fb98b0ea Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 25 Sep 2026 13:51:43 +0200 Subject: [PATCH 2/3] fix(paperclip): agents call the API on loopback, not through the gate Upstream derives the agents' PAPERCLIP_API_URL from the public URL, so agent API calls went out through Traefik to the paperclip-gate outpost. With intercept_header_auth the outpost treats the agent's Authorization header as an Authentik credential and returns 401; agents could not create tasks, post cards or update status. Agents run in the server pod (option B), so PAPERCLIP_API_URL is set to http://localhost:3100 (upstream keeps an explicit value and passes it to every run). Loopback passes the private-hostname guard and agent JWTs still authenticate each call; the gate stays fully enforced for browser traffic instead of carving out /api/. Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/specs/2026-09-25-paperclip-design.md | 3 +++ kubernetes/apps/paperclip/paperclip.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/superpowers/specs/2026-09-25-paperclip-design.md b/docs/superpowers/specs/2026-09-25-paperclip-design.md index eb1ddc8e..9fa760ee 100644 --- a/docs/superpowers/specs/2026-09-25-paperclip-design.md +++ b/docs/superpowers/specs/2026-09-25-paperclip-design.md @@ -79,6 +79,9 @@ All modeled on `kubernetes/apps/paperless/` unless stated. claim (`/bootstrap/claim`) requires an already signed-in Better Auth account. A follow-up flips it to `true`. - `PAPERCLIP_TELEMETRY_DISABLED=1` (read by `resolveTelemetryConfig`). + - `PAPERCLIP_API_URL=http://localhost:3100`: upstream otherwise hands agents the + public URL, and the gate (`intercept_header_auth`) rejects their bearer tokens. + Loopback passes the hostname guard; agent JWTs still authenticate each call. - `envFrom: secretRef paperclip-secrets` - Reloader: `secret.reloader.stakater.com/reload: paperclip-secrets`. diff --git a/kubernetes/apps/paperclip/paperclip.yaml b/kubernetes/apps/paperclip/paperclip.yaml index aaa8f393..2ea0d18c 100644 --- a/kubernetes/apps/paperclip/paperclip.yaml +++ b/kubernetes/apps/paperclip/paperclip.yaml @@ -49,6 +49,9 @@ spec: value: private - name: PAPERCLIP_PUBLIC_URL value: https://paperclip.renner.uno + # Agents share this pod; they reach the API on loopback, not through the Authentik gate. + - name: PAPERCLIP_API_URL + value: http://localhost:3100 - name: PAPERCLIP_AUTH_DISABLE_SIGN_UP value: "true" - name: PAPERCLIP_TELEMETRY_DISABLED -- 2.54.0 From 56e30bf065cc1ea73a53c7c0d0a3b1c4ca9fd12a Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 25 Sep 2026 13:52:48 +0200 Subject: [PATCH 3/3] fix(paperclip): agents use the Service name for the API The cluster-internal name keeps working when runs move to sandbox pods (option C), where loopback would not reach the server. The name is added to PAPERCLIP_ALLOWED_HOSTNAMES because the private-hostname guard otherwise returns 403 for it. Same-namespace traffic is already allowed by the baseline CNP in both directions. Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/specs/2026-09-25-paperclip-design.md | 8 +++++--- kubernetes/apps/paperclip/paperclip.yaml | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-paperclip-design.md b/docs/superpowers/specs/2026-09-25-paperclip-design.md index 9fa760ee..cba62c59 100644 --- a/docs/superpowers/specs/2026-09-25-paperclip-design.md +++ b/docs/superpowers/specs/2026-09-25-paperclip-design.md @@ -79,9 +79,11 @@ All modeled on `kubernetes/apps/paperless/` unless stated. claim (`/bootstrap/claim`) requires an already signed-in Better Auth account. A follow-up flips it to `true`. - `PAPERCLIP_TELEMETRY_DISABLED=1` (read by `resolveTelemetryConfig`). - - `PAPERCLIP_API_URL=http://localhost:3100`: upstream otherwise hands agents the - public URL, and the gate (`intercept_header_auth`) rejects their bearer tokens. - Loopback passes the hostname guard; agent JWTs still authenticate each call. + - `PAPERCLIP_API_URL=http://paperclip.paperclip.svc.cluster.local:3100` plus + `PAPERCLIP_ALLOWED_HOSTNAMES=paperclip.paperclip.svc.cluster.local`: upstream + otherwise hands agents the public URL, and the gate (`intercept_header_auth`) + rejects their bearer tokens. The Service name also works for sandbox pods under + option C; agent JWTs still authenticate each call. - `envFrom: secretRef paperclip-secrets` - Reloader: `secret.reloader.stakater.com/reload: paperclip-secrets`. diff --git a/kubernetes/apps/paperclip/paperclip.yaml b/kubernetes/apps/paperclip/paperclip.yaml index 2ea0d18c..cf9dd146 100644 --- a/kubernetes/apps/paperclip/paperclip.yaml +++ b/kubernetes/apps/paperclip/paperclip.yaml @@ -49,9 +49,11 @@ spec: value: private - name: PAPERCLIP_PUBLIC_URL value: https://paperclip.renner.uno - # Agents share this pod; they reach the API on loopback, not through the Authentik gate. + # Agents reach the API via the Service, not through the Authentik gate. - name: PAPERCLIP_API_URL - value: http://localhost:3100 + value: http://paperclip.paperclip.svc.cluster.local:3100 + - name: PAPERCLIP_ALLOWED_HOSTNAMES + value: paperclip.paperclip.svc.cluster.local - name: PAPERCLIP_AUTH_DISABLE_SIGN_UP value: "true" - name: PAPERCLIP_TELEMETRY_DISABLED -- 2.54.0