Files
PaperClipAI/server
Devin Foley 2f42a4968d Treat cloud-managed instances as bootstrapped in the health gate (#9912)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip instances can be self-hosted, or provisioned and managed
by a cloud control plane that authenticates users through trusted
headers validated against `PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN`
(`resolveCloudTenantActor`)
> - In `authenticated` deployment mode, the health route reports
`bootstrapStatus: bootstrap_pending` until at least one `instance_admin`
exists, and the UI locks everyone out at the "waiting on its first
admin" claim screen until then — correct for self-hosted instances,
where a human operator must claim the instance
> - But the cloud-tenant trust middleware, by deliberate security
hardening, never grants `instance_admin` and actively purges legacy
grants — so a cloud-managed instance can never leave
`bootstrap_pending`: the gate demands a role the middleware forbids
> - Every control-plane-provisioned instance is therefore permanently
locked at the claim screen even though its users and memberships exist
> - This pull request makes the gate cloud-aware: when the tenant server
token is configured, the instance is considered bootstrapped, because
the control plane owns identity and there is no operator claim step
> - The benefit is that cloud-managed instances become usable while
self-hosted behavior stays byte-for-byte identical, now pinned by a
previously missing regression test

## Linked Issues or Issue Description

Refs #2927 (introduced the browser-native first-admin bootstrap flow
this gate feeds). No existing public issue for the deadlock; inline
description per the bug report template:

- **What happened?**: an instance configured with
`PAPERCLIP_DEPLOYMENT_MODE=authenticated` and
`PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN` reports `bootstrapStatus:
bootstrap_pending` forever. All users — including ones created via the
trusted-header path with owner-level company membership — are locked out
at the "This Paperclip is waiting on its first admin" screen.
- **Expected behavior**: a control-plane-managed instance has no
first-admin claim step; users arriving with control-plane identity
should reach the app.
- **Steps to reproduce**:
1. Run the server with `PAPERCLIP_DEPLOYMENT_MODE=authenticated` and a
`PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN` set
2. Create users only through trusted cloud headers (the middleware
upserts them but never grants `instance_admin`, and purges any legacy
grants)
3. `GET /api/health` → `bootstrapStatus` stays `bootstrap_pending`; the
UI shows the claim screen for every visitor, and no supported path
exists to create the `instance_admin` the gate requires
- **Paperclip version or commit**: reproducible on `master` as of
2026-07-20; present since the cloud-tenant `instance_admin` purge
hardening landed.

## What Changed

- `server/src/middleware/auth.ts`: new exported
`isCloudManagedInstance()` predicate beside the trust middleware that
defines the tenant-token contract.
- `server/src/routes/health.ts`: the authenticated-mode first-admin gate
is skipped when the instance is cloud-managed; `bootstrapStatus` reports
`ready`.
- `server/src/__tests__/health.test.ts`: two new tests — authenticated
without the token → `bootstrap_pending` (previously untested regression
baseline), and with the token → `ready` despite zero instance admins.

## Verification

- `pnpm vitest run src/__tests__/health.test.ts` in `server/` — 13/13
- `pnpm vitest run src/middleware/cloud-tenant-actor.test.ts` — 6/6
- Manual: with the env vars from the repro steps set, `GET /api/health`
now returns `bootstrapStatus: "ready"`; without the token, behavior is
unchanged

## Risks

- None for self-hosted deployments: without the env var the gate is the
prior behavior, now pinned by the new regression test.
- For cloud-managed instances the claim screen and
`bootstrapInviteActive` flow no longer appear — intended; browser-based
claim was already disabled in that configuration.

## Model Used

- Claude (Anthropic) — model id `claude-fable-5`, via the Claude Code
CLI harness with tool use (shell, file edits, test execution). Diagnosis
and change agent-assisted, human-directed.

## 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 (n/a —
behavior documented in code comments and pinned by tests)
- [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
2026-07-20 15:59:08 -04:00
..