mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 08:53:19 +01:00
## Thinking Path > - Paperclip controls AI agent work across software updates. > - Authenticated instances must keep human sign-in available during an update. > - Better Auth 1.7.16 identifies accounts by provider and account ID. > - It creates credential accounts without the issuer required by the previous reader. > - A new account can therefore fail to sign in through a surviving 1.7.2 process. > - This pull request keeps the legacy credential issuer on trusted account creation. > - The security upgrade and provider/account uniqueness stay in place. ## Linked Issues or Issue Description Refs #18906. **What happened?** After the Better Auth upgrade, a credential account created by the new version has a NULL issuer. A surviving Better Auth 1.7.2 process rejects its correct password because it selects only the `local:credential` account identity. **Expected behavior** A new credential account must sign in through both versions during an update. The new version must keep its provider/account lookup and uniqueness rules. **Steps to reproduce** 1. Apply migration 0465 to a disposable database. 2. Create an email/password account with the new auth mount. 3. Sign in to the same database through Better Auth 1.7.2. 4. Without this change, signup and new-version sign-in return 200, but old-version sign-in returns 401. ## What Changed - Register issuer as an optional internal account field with client input and response output disabled. - Set `local:credential` in the trusted before-create hook. Preserve any nonempty issuer supplied by internal code. - Leave unconfigured providers without an issuer on the new NULL-issuer behavior. No social provider is configured by this module. - Add credential integration and issuer-policy controls. Document the exact compatibility scope. ## Verification - Node 24: `node scripts/agent-preflight.mjs` passed using the existing build cache. - `node cli/node_modules/tsx/dist/cli.mjs packages/db/src/test-postgres-command.ts -- node node_modules/vitest/vitest.mjs run --project @paperclipai/server server/src/__tests__/better-auth.test.ts server/src/__tests__/better-auth-credential-signup.integration.test.ts`: 22 passed. - Separate local qualification used the actual cached Better Auth 1.7.2 SDK and the current 1.7.16 mount against one disposable PostgreSQL database. Before this change, old-version sign-in returned 401 for the new account. After this change, signup through either writer and sign-in through both readers returned 200. - The integration covers forged client issuer input, the frozen 1.7.2 credential selector, current-version sign-in for existing NULL-issuer rows, and the unchanged provider/account unique index. - `pnpm --filter @paperclipai/server typecheck`: passed, including all server and test-tree layers. - The first required Runner support run failed three SDK proof suites because two generated lock references were stale on the current base. The existing generator corrected those references and the three suites passed locally (58 tests in 17.07 seconds). Master then merged the same repair in #20834 and advanced its runtime/dependency lock. This branch was rebased onto current master; the duplicate proof commit was omitted. The final diff contains only the four auth/test/doc files. The exact-head checks and review listed below passed on the rebased candidate. - Final candidate `577fd7612a6d80ac35e2f5ee9ea2ce9b77169687`: focused auth verification passed again (22 tests, 51.81 seconds). [Required CI](https://github.com/paperclipai/paperclip/actions/runs/43051216288) passed, including build, typecheck, server/Runner tests and all eight browser shards. One unchanged Rust process-lineage orphan-census test failed on the first attempt; the failed job was rerun after the workflow completed and passed. This PR changes no Rust source. - [Greptile review](https://github.com/paperclipai/paperclip/pull/20839#issuecomment-6799551882) is 5/5 on the final head with no actionable findings or open review threads. No deployment or original database change was performed for this branch. ## Risks The bridge applies to new credential accounts. It does not backfill existing NULL issuers or claim old-reader compatibility for unconfigured social providers. Trusted internal issuers retain their exact value. Account lookup, password checks, account linking, provider/account uniqueness, and migration 0465 remain with their existing owners. ## Model Used OpenAI Codex (GPT-6), with tool-assisted code editing and local PostgreSQL qualification. The exact serving model ID and context window are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
240 lines
10 KiB
Markdown
240 lines
10 KiB
Markdown
# Deployment Modes
|
|
|
|
Status: Canonical deployment and auth mode model
|
|
Date: 2026-02-23
|
|
|
|
### Paperclip Cloud sign-in
|
|
|
|
Cloud-managed instances use Cloud for human sign-in. The instance `/auth`
|
|
route waits for deployment metadata before rendering; it never renders the
|
|
email/password form when health identifies a Cloud-managed instance. A missing
|
|
instance session returns through Cloud's `/v1/stacks/:slug/entry-redirect`.
|
|
Cloud renews the tenant session from the existing Cloud session, or sends the
|
|
user through its sign-in flow. The original tenant path, query, and fragment
|
|
travel as `returnTo` so the user returns to the same task.
|
|
|
|
Both the Cloud origin and stack slug come from the server's health metadata
|
|
(`PAPERCLIP_CLOUD_API_ORIGIN` and `PAPERCLIP_STACK_SLUG`). Do not infer the
|
|
environment from the browser hostname or hardcode staging/production domains.
|
|
Missing configuration shows an unavailable state. An automatic recovery attempt
|
|
is limited per browser tab until a session is verified, with a five-minute
|
|
expiry and an explicit retry link if recovery fails. Network/server failures
|
|
show an error rather than treating the user as signed out.
|
|
|
|
Self-hosted authenticated instances retain their instance sign-in form, and
|
|
`local_trusted` instances retain their normal access path.
|
|
|
|
The account create hook keeps `local:credential` on new email/password accounts
|
|
so a surviving Better Auth 1.7.2 process can still sign those users in during an
|
|
upgrade. This internal field is not client input. Better Auth 1.7.16 still owns
|
|
account lookup and the unique `(provider_id, account_id)` key. Existing NULL
|
|
issuer rows remain readable by the new version. Cloud workspace handoff creates
|
|
users and sessions without account rows. No social provider is configured here;
|
|
other providers retain their trusted internal issuer, or NULL when absent. This
|
|
bridge does not claim old-reader compatibility for those unconfigured providers.
|
|
|
|
Cloud stack Admins can change experimental settings on their dedicated instance.
|
|
This permission comes from the current trusted Cloud stack role and does not
|
|
grant other instance-admin powers. Members, Support users, and agents cannot
|
|
change experiments. Operator-hidden settings and Cloud-managed feature values
|
|
remain protected. Staging allows saved overrides for its editable experiments;
|
|
production retains its managed feature policy.
|
|
|
|
## 1. Purpose
|
|
|
|
Paperclip supports two runtime modes:
|
|
|
|
1. `local_trusted`
|
|
2. `authenticated`
|
|
|
|
`authenticated` supports two exposure policies:
|
|
|
|
1. `private`
|
|
2. `public`
|
|
|
|
This keeps one authenticated auth stack while still separating low-friction private-network defaults from internet-facing hardening requirements.
|
|
|
|
Paperclip now treats **bind** as a separate concern from auth:
|
|
|
|
- auth model: `local_trusted` vs `authenticated`, plus `private/public`
|
|
- reachability model: `server.bind = loopback | lan | tailnet | custom`
|
|
|
|
## 2. Canonical Model
|
|
|
|
| Runtime Mode | Exposure | Human auth | Primary use |
|
|
|---|---|---|---|
|
|
| `local_trusted` | n/a | No login required | Single-operator local machine workflow |
|
|
| `authenticated` | `private` | Login required | Private-network access (for example Tailscale/VPN/LAN) |
|
|
| `authenticated` | `public` | Login required | Internet-facing/cloud deployment |
|
|
|
|
## Reachability Model
|
|
|
|
| Bind | Meaning | Typical use |
|
|
|---|---|---|
|
|
| `loopback` | Listen on localhost only | default local usage, reverse-proxy deployments |
|
|
| `lan` | Listen on all interfaces (`0.0.0.0`) | LAN/VPN/private-network access |
|
|
| `tailnet` | Listen on a detected Tailscale IP | Tailscale-only access |
|
|
| `custom` | Listen on an explicit host/IP | advanced interface-specific setups |
|
|
|
|
## 3. Security Policy
|
|
|
|
## `local_trusted`
|
|
|
|
- loopback-only host binding
|
|
- no human login flow
|
|
- optimized for fastest local startup
|
|
|
|
## `authenticated + private`
|
|
|
|
- login required
|
|
- low-friction URL handling (`auto` base URL mode)
|
|
- private-host trust policy required
|
|
- Better Auth request rate limiting is off by default for private mode to keep local/LAN repair loops from locking out the operator; set `PAPERCLIP_AUTH_RATE_LIMIT_ENABLED=true` to opt in
|
|
- bind can be `loopback`, `lan`, `tailnet`, or `custom`
|
|
|
|
## `authenticated + public`
|
|
|
|
- login required
|
|
- explicit public URL required
|
|
- stricter deployment checks and failures in doctor
|
|
- Better Auth request rate limiting is on by default; set `PAPERCLIP_AUTH_RATE_LIMIT_ENABLED=false` only when an explicit front-door limiter covers the deployment
|
|
- recommended bind is `loopback` behind a reverse proxy; direct `lan/custom` is advanced
|
|
- local stdio MCP runtime slots fail closed by default; set `PAPERCLIP_TRUSTED_MCP_RUNTIME_HOST` only when a trusted worker/runtime host is configured to supervise those processes. Remote HTTP MCP remains the preferred public-hosted path.
|
|
|
|
### Paperclip Cloud warm-pool identity
|
|
|
|
A Cloud-managed warm-pool process initially boots under a `pool-*` origin. It
|
|
receives only Cloud's public verification set in
|
|
`PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS`. Before Cloud activates a claimed stack,
|
|
the existing server-to-server health request carries a short-lived Ed25519 JWS
|
|
that binds the immutable `PAPERCLIP_CLOUD_STACK_ID`, pool claim, previous
|
|
origin, canonical HTTPS origin, and slug. Paperclip verifies and persists that
|
|
one-time assertion, updates its live public/API URL provider, and acknowledges
|
|
the exact origin in `/api/health` before the first user request is admitted.
|
|
|
|
The Harness signing private key is never present in Paperclip, browsers, or
|
|
other tenant stacks. A different claim or destination cannot replace the
|
|
persisted identity. On restart, the durable identity is loaded before auth,
|
|
routes, and child-runtime configuration, even when provider variables are
|
|
temporarily stale. Self-hosted deployments continue to use their configured
|
|
`PAPERCLIP_PUBLIC_URL` and do not participate in this protocol.
|
|
|
|
## 4. Onboarding UX Contract
|
|
|
|
Default onboarding remains interactive and flagless:
|
|
|
|
```sh
|
|
pnpm paperclipai onboard
|
|
```
|
|
|
|
Server prompt behavior:
|
|
|
|
1. quickstart `--yes` defaults to `server.bind=loopback` and therefore `local_trusted/private`
|
|
2. advanced server setup asks reachability first:
|
|
- `Trusted local` → `bind=loopback`, `local_trusted/private`
|
|
- `Private network` → `bind=lan`, `authenticated/private`
|
|
- `Tailnet` → `bind=tailnet`, `authenticated/private`
|
|
- `Custom` → manual mode/exposure/host entry
|
|
3. raw host entry is only required for the `Custom` path
|
|
4. explicit public URL is only required for `authenticated + public`
|
|
|
|
Examples:
|
|
|
|
```sh
|
|
pnpm paperclipai onboard --yes
|
|
npx paperclipai onboard --yes --bind lan
|
|
npx paperclipai run --bind tailnet
|
|
```
|
|
|
|
`configure --section server` follows the same interactive behavior.
|
|
|
|
## 5. Doctor UX Contract
|
|
|
|
Default doctor remains flagless:
|
|
|
|
```sh
|
|
pnpm paperclipai doctor
|
|
```
|
|
|
|
Doctor reads configured mode/exposure and applies mode-aware checks. Optional override flags are secondary.
|
|
|
|
## 6. Board/User Integration Contract
|
|
|
|
Board identity must be represented by a real DB user principal for user-based features to work consistently.
|
|
|
|
Required integration points:
|
|
|
|
- real user row in `authUsers` for Board identity
|
|
- `instance_user_roles` entry for Board admin authority
|
|
- `company_memberships` integration for user-level task assignment and access
|
|
|
|
This is required because user assignment paths validate active membership for `assigneeUserId`.
|
|
|
|
## 7. Local Trusted -> Authenticated Claim Flow
|
|
|
|
When running `authenticated` mode, if the only instance admin is `local-board`, Paperclip emits a startup warning with a one-time high-entropy claim URL.
|
|
|
|
- URL format: `/board-claim/<token>?code=<code>`
|
|
- intended use: signed-in human claims board ownership
|
|
- claim action:
|
|
- promotes current signed-in user to `instance_admin`
|
|
- demotes `local-board` admin role
|
|
- ensures active owner membership for the claiming user across existing companies
|
|
|
|
This prevents lockout when a user migrates from long-running local trusted usage to authenticated mode.
|
|
|
|
## 8. First Admin Setup For Fresh Authenticated Installs
|
|
|
|
Fresh authenticated installs start in `bootstrap_pending` until the first
|
|
`instance_admin` exists.
|
|
|
|
For `authenticated/private`, Paperclip supports a browser-first setup path:
|
|
|
|
1. open the Paperclip URL from the private network or appliance UI
|
|
2. sign in or create a Paperclip account
|
|
3. choose `Claim this instance` on the setup screen
|
|
|
|
That browser claim promotes the signed-in session user to the first instance
|
|
admin and then falls through to normal onboarding. The endpoint is available
|
|
only to real browser session actors in `authenticated/private`; unauthenticated
|
|
requests, agent keys, board API keys, and local implicit board actors are
|
|
rejected.
|
|
|
|
This is intentionally a first-claim bootstrap contract: before an instance
|
|
admin exists, the first authenticated browser session that completes the claim
|
|
wins. Operators must keep a `bootstrap_pending` private deployment on a trusted
|
|
network and complete setup before admitting untrusted users. This behavior is
|
|
not an account-recovery or public-deployment mechanism.
|
|
|
|
The CLI fallback remains supported in all authenticated setup states:
|
|
|
|
```sh
|
|
pnpm paperclipai auth bootstrap-ceo
|
|
```
|
|
|
|
That command prints a one-time first-admin invite URL. Browser claim and
|
|
bootstrap invite acceptance share the same first-admin transaction, so whichever
|
|
path wins first makes later attempts return a conflict.
|
|
|
|
For `authenticated/public`, browser first-admin claim is intentionally disabled.
|
|
Public deployments must use the high-entropy bootstrap invite path unless a
|
|
future public-hosted setup design explicitly changes this policy.
|
|
|
|
## 9. Current Code Reality (As Of 2026-02-23)
|
|
|
|
- runtime values are `local_trusted | authenticated`
|
|
- `authenticated` uses Better Auth sessions and bootstrap invite flow
|
|
- `local_trusted` ensures a real local Board user principal in `authUsers` with `instance_user_roles` admin access
|
|
- company creation ensures creator membership in `company_memberships` so user assignment/access flows remain consistent
|
|
|
|
## 10. Naming and Compatibility Policy
|
|
|
|
- canonical naming is `local_trusted` and `authenticated` with `private/public` exposure
|
|
- no long-term compatibility alias layer for discarded naming variants
|
|
|
|
## 11. Relationship to Other Docs
|
|
|
|
- implementation plan: `doc/plans/2026-02-23-deployment-auth-mode-consolidation.md`
|
|
- operator workflows: `doc/DEVELOPING.md` and `doc/CLI.md`
|
|
- invite/join state map: `doc/spec/invite-flow.md`
|