## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Express looks at incoming `X-Forwarded-For` headers only when
`app.set("trust proxy", …)` says it should, and uses that resolved
client IP downstream for rate-limiting, audit logging, and any
auth/abuse signal that ties back to source IP
> - The original PR #3729 added `TRUST_PROXY` accepting only `"true"` or
a positive integer, which forces operators to pick between two unsafe
defaults: hop-count (brittle if topology changes) or boolean-true (any
client can spoof `X-Forwarded-For` and bypass rate-limits or pollute
audit logs)
> - `trust proxy: true` is one of the most common Express
misconfigurations and trivially exploitable for IP-spoofing-based
rate-limit bypass; the safest config — trust only the LB's actual CIDR
or only loopback — was unreachable with the previous parser
> - This pull request replaces the parser with full Express 5 support —
unset / `false` / `0` (Express default), positive integer hop count,
comma-separated CIDR list, named subnets (`loopback`, `linklocal`,
`uniquelocal`) — and emits a startup error naming the offending token on
invalid input
> - The benefit is that operators can now trust *only* their actual
ingress and close the spoofing window without leaking client-IP
integrity to downstream layers, while preserving every
previously-working config as a strict superset
## Linked Issues or Issue Description
Refs #1690 — login returns 500 behind a reverse proxy because Express
`trust proxy` is not enabled; this PR ships the configuration surface
(`TRUST_PROXY` with CIDR lists and named subnets) that lets operators
enable it safely. It does not change the default, so #1690 still
requires the operator to set `TRUST_PROXY` — hence Refs, not Fixes.
No other existing issue covers this directly — remaining problem
described in-PR:
- The original `TRUST_PROXY` parser (PR #3729, which this PR supersedes)
accepted only `"true"` or a hop count, forcing operators to choose
between brittle hop-counting and the spoofable `trust proxy: true`.
- `trust proxy: true` lets any client spoof `X-Forwarded-For` and bypass
rate limits or pollute audit logs; the safest config — trusting only the
LB's actual CIDR or only loopback — was unreachable with the previous
parser.
Duplicate-PR search: #1854 / #1714 are earlier minimal trust-proxy
enablement PRs; this PR supersedes #3729 and generalizes beyond a
boolean enable (CIDR lists + named subnets).
## What Changed
- **`server/src/middleware/trust-proxy.ts`** — new helper exposing
`parseTrustProxyEnv` (testable) and `applyTrustProxy(app)` (one-call
boot wiring). Surface:
- Unset / `""` / `false` / `0` → no `app.set("trust proxy", …)` (Express
default: trust nothing).
- `true` → `app.set("trust proxy", true)`. Documented as unsafe in
untrusted-LB deployments.
- Positive integer (e.g. `"2"`) → hop count. Strict parse: rejects
`"01"`, leading/trailing whitespace.
- Comma-separated list of CIDRs and/or named subnets (e.g.
`"loopback,uniquelocal,10.0.0.0/8,fd00::/8"`) → array passed to
`app.set("trust proxy", [...])`.
- Anything else → startup error naming the offending token.
- **`server/src/app.ts`** — one import + one call to
`applyTrustProxy(app)`.
- **`server/src/__tests__/trust-proxy.test.ts`** — 12 cases: unset,
`"true"`, `"0"`, `"2"`, `"01"` rejected, `" 2 "` rejected, `"loopback"`,
`"loopback,uniquelocal"`, `"10.0.0.0/8"`, `"10.0.0.0/8,fd00::/8"`,
`"bogus"` rejected (error names the bad token), mixed-list with one bad
token rejected (error names the offending token specifically).
## Verification
- `pnpm --filter @paperclipai/server run typecheck` — clean.
- `npx vitest run trust-proxy` — 12/12 pass.
## Risks
- **No new required env vars.** Unset means default Express behavior
(trust nothing). Pure superset of #3729's surface — anything that worked
under #3729 still works here.
- **Strict parse.** `"01"` and `" 2 "` are rejected on purpose so
configuration mistakes surface at startup, not as silently-degraded
auth/rate-limit behavior. The error message names the offending token.
- **No runtime cost** — the parse runs once at boot. The downstream
`trust proxy` setting is internal to Express.
- Single-tenant local-first deploys unaffected by default.
## Model Used
Claude Opus 4.7 (1M context), extended thinking mode.
## Checklist
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Checked ROADMAP.md — not in conflict with planned core work
- [x] Tests run locally and pass (`trust-proxy` 12/12)
- [x] Added boundary cases (leading-zero, whitespace, unknown token,
mixed-list-with-bad-token)
- [x] No UI changes
- [x] Documented risks above
- [x] Will address all Greptile and reviewer comments before merge
Closes#3729.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies, and a
single server instance can host many companies.
> - The auth middleware (`server/src/middleware/auth.ts`) supports a
`cloud_tenant` mode where a trusted hosting proxy injects per-request
identity headers, designed originally for one-deployment-per-tenant
setups.
> - In that original setup, granting every cloud tenant the
`instance_admin` role was harmless; on a **shared, multi-tenant pool**
it means any paying tenant is admin of the whole instance and can reach
every other tenant's data.
> - A tenant only needs to own its own company — which it already gets
via the company membership the same code path upserts — so
instance-level admin is never appropriate for `cloud_tenant` actors.
> - This PR removes the `instance_admin` grant from the cloud-tenant
path and pins `isInstanceAdmin: false` on the resolved actor.
> - Greptile review then surfaced a follow-up gap: deployments that ran
the pre-hardening build still have stale `instance_admin` rows in
`instance_user_roles`, which other lookups (BetterAuth session path,
board API keys, and the authorization service's own DB re-check) would
still honor.
> - The follow-up commit closes that gap by purging stale rows at the
cloud-tenant auth boundary and by teaching the authorization service
that `cloud_tenant` actors are never instance admins.
> - The benefit is that shared-pool hosting becomes structurally safe:
tenants are company-scoped owners, never instance admins — including on
deployments upgrading from the older behavior.
## Linked Issues
- Refs #966 — managed SaaS multi-tenant hosting is the deployment shape
this hardening protects.
- Refs #5015 — same problem space: instance-admin-scoped credentials are
too broad for multi-company instances; tenants need company-scoped
access.
Neither issue is fully closed by this PR; it removes the instance-admin
grant from the `cloud_tenant` trusted-header path specifically.
## What Changed
- `server/src/middleware/auth.ts`
- Removed the `instanceUserRoles` insert that granted every cloud tenant
`instance_admin`; `resolveCloudTenantActor` now returns
`isInstanceAdmin: false` (was `true`).
- `resolveCloudTenantActor` now **deletes** any stale `instance_admin`
row for the authenticated tenant user on every trusted-header request,
so grants left behind by pre-hardening deployments are purged at the
source (closes the Greptile P2: stale rows could otherwise re-elevate
the user via the BetterAuth session path, board API keys, or the
authorization service).
- The function is `export`ed so it can be unit-tested directly.
- `server/src/services/authorization.ts`
- `authorizationService` previously re-checked `instanceUserRoles` from
the DB regardless of the actor flag, which would have elevated even
hardened `cloud_tenant` actors while a stale row lingered. Actors with
`source === "cloud_tenant"` are now never elevated to instance admin;
other board actors keep the existing lookup.
- `server/src/services/authorization.ts` +
`server/src/middleware/auth.ts` (follow-up commit `dc57a71c7`)
- CI on the merge ref surfaced that elevation removal alone strands real
cloud tenant users: board actors only ever reached `issue:read` /
`issue:mutate` through instance-admin elevation (`permissionForAction`
maps both to no grant key). `decide()` now grants `cloud_tenant` actors
with an **active membership in the resource company** the same read
surface as a same-company agent (`agent:read`, `company_scope:read`,
`issue:read`, `project:read`) plus `issue:mutate` for non-viewer members
— cross-company access stays denied (new `allow_company_member` reason).
- `resolveCloudTenantActor` seeds the standard role-default permission
grants (`ensureHumanRoleDefaultGrants`) so granted actions (e.g.
`tasks:assign`, `agents:create` for owners) work without elevation.
- Master-side route tests that stubbed cloud tenant actors with
`isInstanceAdmin: true` now seed a real membership and assert under the
hardened contract (`issue-identifier-routes`,
`multilingual-issues-routes`, `issue-comment-redaction`).
- Tests
- `server/src/middleware/cloud-tenant-actor.test.ts` (new): cloud tenant
is never instance-admin, is scoped to exactly the one company from its
stack, still upserts user/company/membership, purges stale
`instance_admin` rows, returns null without the server token, and maps
non-owner stack roles without elevating.
- `server/src/__tests__/auth-session-route.test.ts`: end-to-end
middleware regression — a user with a stale `instance_admin` row stops
being elevated via the session path once they authenticate through the
cloud-tenant path (with a control assertion showing the pre-purge
elevation).
- `server/src/__tests__/authorization-service.test.ts` (embedded
Postgres): a `cloud_tenant` actor with a stale `instance_admin` row in
the real DB cannot cross company boundaries, while a `session` actor
with the same row still resolves `allow_instance_admin`.
## Verification
Run from the repo root after `pnpm install --frozen-lockfile`:
```bash
cd server
npx vitest run src/middleware/cloud-tenant-actor.test.ts src/__tests__/auth-session-route.test.ts
# 9 tests passed
npx vitest run src/__tests__/authorization-service.test.ts
# 16 tests passed (embedded Postgres)
pnpm typecheck
# clean
```
Also ran the broader auth-related suites locally (`auth-routes`,
`authz-company-access`, `better-auth`, `adapter-routes-authz`,
`express5-auth-wildcard`): 8 files, 58 tests, all passing.
## Risks
- **This touches authentication and authorization paths directly.**
Mistakes here are security bugs in both directions; review accordingly.
- **Behavioral change for existing `cloud_tenant` deployments:** tenants
that previously (incorrectly) had instance-admin lose it — including the
ability to see/manage other companies on the instance. This is the
intended hardening, but any single-tenant deployment that relied on the
cloud-tenant identity for instance administration must provision a
separate admin identity.
- **The purge is destructive by design:** if an operator's
instance-admin identity is *also* provisioned through the cloud-tenant
headers (same user id), its `instance_admin` row will be deleted on the
next trusted-header request. Operators should hold admin through a
non-cloud-tenant identity.
- **Residual gap (documented, not fixed here):** a deployment that ran
the old cloud_tenant build and then *disabled* cloud-tenant mode keeps
stale rows until the affected user re-authenticates through the cloud
path. A data migration was considered and deliberately avoided: there is
no reliable SQL predicate for "cloud-tenant-provisioned user" (no source
column), so a migration risks deleting legitimate admins.
- No schema or migration changes; no UI changes.
## Model Used
- Claude Fable 5 (claude-fable-5, 1M context), extended thinking + tool
use, via Claude Code — this revision; original PR authored in an earlier
Claude Code 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 (none duplicate this; related issues Refs #966 / #5015 are
linked in the issue section)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (no UI changes)
- [x] I have updated relevant documentation to reflect my changes (no
existing docs reference `cloud_tenant` mode)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resubmits #5820 by @echokos. The original PR's head fork could not
accept maintainer edits (organization-owned fork without cross-org
maintainer-edit access), so we've resubmitted the commits here with
original authorship preserved. Thanks to @echokos for the contribution.
---
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies; the
server is an Express app with structured HTTP logging via pino-http.
> - The middleware in `server/src/middleware/logger.ts` defines a
`customProps` hook that attaches request context (`req.body` /
`req.params` / `req.query`) to every 4xx/5xx log entry so operators can
diagnose failed requests.
> - That hook copies the body verbatim. Better Auth's `POST
/api/auth/sign-in/email` carries an `{ email, password }` body — on a
wrong-password attempt the request lands in the 4xx branch and the
plaintext password is written to `~/.paperclip/logs/server.log`.
> - Two existing issues raise this (#3072 plaintext-password leak, #4759
similar concerns) and neither has a fix.
> - Same exposure surface applies to sign-up, reset-password, API key
creation, and any endpoint that accepts a credential in the body and can
return 4xx.
> - This pull request introduces a small `redactSensitive` walker that
returns a shallow copy of the input with values for known
credential-shaped keys replaced with `[REDACTED]`, and applies it at
every body/params/query log site in `customProps`.
> - The benefit is that operators can keep diagnostic logging on without
their disk silently accumulating user passwords and bearer tokens.
## What Changed
- `server/src/middleware/redact-sensitive.ts` (new): depth-capped,
case-insensitive walker. Sensitive keys covered: `password`,
`currentPassword`, `newPassword`, `passwordConfirmation`,
`passwordConfirm`, `confirmPassword` (+ snake_case variants), `secret`,
`client_secret`, `access_token`, `refresh_token`, `id_token`,
`auth_token`, `session_token`, `api_key`, `authorization`,
`private_key`. Bare `token` deliberately not in the list — pagination
cursors and CSRF tokens are not credentials (per Greptile review).
- `server/src/middleware/logger.ts`: wraps the six log sites in
`customProps` (3 ctx-path + 3 fallback-path) with `redactSensitive`.
- `server/src/__tests__/redact-sensitive.test.ts` (new): covers
plaintext password, case-insensitive matching, multiple credential keys,
nested objects/arrays, bare `token` left untouched, primitives
untouched, cycle safety.
- Depth-cap returns `undefined` (field absent from log line) rather than
a sentinel string, per Greptile review.
## Verification
- `pnpm --filter @paperclipai/server test redact-sensitive` should run
the new test file green.
- Manual: tail `~/.paperclip/logs/server.log`, hit `POST
/api/auth/sign-in/email` with a deliberately wrong password, confirm the
logged `reqBody.password` reads `[REDACTED]` (not the plaintext) and the
surrounding fields still appear for diagnosis.
## Risks
Low. The walker only rewrites values at the log-emit boundary — the
actual `req.body` object handed to downstream handlers is unchanged
because `redactSensitive` returns a new object. Standard log fields
(email, route path, status code) remain visible. The sensitive-key list
is conservative enough that the only risk is over-redacting a
non-credential field that happens to share a name with a known
credential; the bare `token` carve-out in this revision addresses the
most obvious such case.
## Model Used
- Claude Opus 4.7 (`claude-opus-4-7`), Anthropic, extended thinking
mode, working through Claude Code CLI.
## 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
- [ ] I have run tests locally and they pass (no `node_modules` in my
disposable PR-prep checkout; CI vitest will exercise)
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — server-only)
- [ ] I have updated relevant documentation to reflect my changes (no
doc surface affected)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Closes#3072. Refs #4759.
---
## Original Context
`customProps` in the HTTP logger copies `req.body` / `req.params` /
`req.query` verbatim into 4xx/5xx log entries. Better Auth's
wrong-password flow therefore writes:
```
{"reqBody":{"email":"…","password":"founding6gomez6croaking"},"msg":"POST /api/auth/sign-in/email 401"}
```
…to disk. This PR rewrites credential-shaped values to `[REDACTED]` at
that boundary.
---
## Cross-references and status (maintainer)
Closes#5820Closes#3095Closes#4760Closes#4886
---------
Co-authored-by: Aurora <aurora@majorimpact.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip orchestrates AI agents through a server-side control plane
> - That control plane depends on reliable issue state transitions,
plugin lifecycle behavior, import limits, and startup/shutdown handling
> - Several small runtime fixes had accumulated on the working branch
and were mixed with larger feature work
> - Keeping them separate makes the correctness fixes reviewable and
mergeable without waiting for cloud-sync UI work
> - This pull request groups the server/runtime control-plane fixes into
one standalone branch
> - The benefit is a tighter, safer runtime baseline for retries,
imports, plugin migrations, feedback flushing, and trusted cloud import
handling
## What Changed
- Fixed updated issue list pagination sorting and scheduled retry
comment handling.
- Re-applied pending plugin migrations during hot reload and fixed
plugin-schema worktree seed restore.
- Hardened public tenant DB startup, portable import body limits,
trusted cloud import errors, and trusted cloud tenant import mutation
access.
- Expired stale request confirmations after user comments.
- Added feedback export shutdown hardening so database-unavailable flush
loops stop cleanly.
- Guarded plugin worker `error` event emission when no listener is
registered.
## Verification
- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm --filter @paperclipai/plugin-sdk build`
- `npm run install --prefix
node_modules/.pnpm/sqlite3@5.1.7/node_modules/sqlite3`
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
server/src/__tests__/plugin-lifecycle-restart.test.ts
server/src/__tests__/server-startup-feedback-export.test.ts
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/body-limits.test.ts
server/src/__tests__/feedback-flush-controller.test.ts
server/src/__tests__/error-handler.test.ts
server/src/__tests__/board-mutation-guard.test.ts
packages/db/src/backup-lib.test.ts` initially exposed local setup issues
and two 5s test timeouts.
- Rerun after local prereq build: `pnpm exec vitest run --testTimeout
15000 server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/feedback-flush-controller.test.ts
server/src/__tests__/server-startup-feedback-export.test.ts` passed.
- Some embedded Postgres-backed tests skipped on this host because local
Postgres init was unavailable.
## Risks
- Runtime-touching branch: startup/shutdown and issue interaction
behavior should be reviewed carefully.
- The feedback export change disables repeated flush attempts only for
database connection-refused failures; other upload failures still log
normally.
- The plugin worker error guard avoids process crashes from unhandled
EventEmitter errors but may hide errors from code paths that expected an
emitted listener.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5-based coding agent with local shell/git/tool use.
Exact hosted model ID and context-window size are not exposed by the
local Paperclip adapter runtime.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Paperclip Cloud imports local company data into tenant Paperclip
stacks through trusted server-to-server calls.
> - Tenant imports authenticate as board actors with `source:
"cloud_tenant"` because they act on behalf of an authorized stack user.
> - The board mutation guard correctly protects browser session
mutations with trusted `Origin`/`Referer` checks.
> - But the guard treated trusted Cloud tenant calls like browser
session mutations, so server-to-server imports without a browser origin
failed with `403 Board mutation requires trusted browser origin`.
> - This pull request exempts trusted Cloud tenant actors from
browser-origin enforcement while preserving the session-backed browser
guard.
> - The benefit is that authorized Cloud imports can persist into tenant
Paperclip storage without weakening browser CSRF protections.
## What Changed
- Allow `req.actor.source === "cloud_tenant"` through
`boardMutationGuard` without requiring browser `Origin` or `Referer`
headers.
- Add a focused regression test for Cloud tenant POST mutations without
an origin.
- Preserve the existing session-backed rejection test for board
mutations that lack a trusted browser origin.
## Verification
- `pnpm exec vitest run
server/src/__tests__/board-mutation-guard.test.ts`
- Result: 10 tests passed.
## Risks
- Low risk: this only expands the existing non-browser exemption list to
trusted Cloud tenant actors that have already passed tenant-server-token
authentication.
- The browser-session path remains covered by the existing rejection
test, so missing-origin browser mutations still fail.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5 coding agent, tool-enabled local repository
editing and shell verification.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path
> - Paperclip is a local-first control plane for AI-agent companies.
> - Operators need predictable local dev behavior, recoverable instance
data, and scripts that do not churn the running app.
> - Several accumulated changes improve backup streaming, dev-server
health, static UI caching/logging, diagnostic-file ignores, and instance
isolation.
> - These are operational improvements that can land independently from
product UI work.
> - This pull request groups the dev-infra and backup changes from the
split branch into one standalone branch.
> - The benefit is safer local operation, easier manual backups, less
noisy dev output, and less cross-instance auth leakage.
## What Changed
- Added a manual instance database backup endpoint and route tests.
- Streamed backup/restore handling to avoid materializing large payloads
at once.
- Reduced dev static UI log/cache churn and ignored Node diagnostic
report captures.
- Added guarded dev auto-restart health polling coverage.
- Preserved worktree config during provisioning and scoped auth cookies
by instance.
- Added a Discord daily digest helper script and environment
documentation.
- Hardened adapter-route and startup feedback export tests around the
changed infrastructure.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run packages/db/src/backup-lib.test.ts
server/src/__tests__/instance-database-backups-routes.test.ts
server/src/__tests__/server-startup-feedback-export.test.ts
server/src/__tests__/adapter-routes.test.ts
server/src/__tests__/dev-runner-paths.test.ts
server/src/__tests__/health-dev-server-token.test.ts
server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/vite-html-renderer.test.ts
server/src/__tests__/workspace-runtime.test.ts
server/src/__tests__/better-auth.test.ts`
- Split integration check: merged after the runtime/governance branch
and before UI branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches server startup, backup streaming, auth cookie
naming, dev health checks, and worktree provisioning.
- Backup endpoint behavior depends on existing board/admin access
controls and database backup helpers.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Local development needs to work cleanly across linked git worktrees
because Paperclip itself leans on worktree-based engineering workflows
> - Dev-mode asset routing, Vite watch behavior, and workspace package
links are part of that day-to-day control-plane ergonomics
> - The current branch had a small but coherent set of
worktree/dev-tooling fixes that are independent from both the issue UI
changes and the heartbeat runtime changes
> - This pull request isolates those environment fixes into a standalone
branch that can merge without carrying unrelated product work
> - The benefit is a smoother multi-worktree developer loop with fewer
stale links and less noisy dev watching
## What Changed
- Serve dev public assets before the HTML shell and add a routing test
that locks that behavior in.
- Ignore UI test files in the Vite dev watch helper so the dev server
does less unnecessary work.
- Update `ensure-workspace-package-links.ts` to relink stale workspace
dependencies whenever a workspace `node_modules` directory exists,
instead of only inside linked-worktree detection paths.
## Verification
- `pnpm vitest run server/src/__tests__/app-vite-dev-routing.test.ts
ui/src/lib/vite-watch.test.ts`
- `node cli/node_modules/tsx/dist/cli.mjs
scripts/ensure-workspace-package-links.ts`
## Risks
- The asset routing change is low risk but sits near app shell behavior,
so a regression would show up as broken static assets in dev mode.
- The workspace-link repair now runs in more cases, so the main risk is
doing unexpected relinks when a checkout has intentionally unusual
workspace symlink state.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Humans interact with the system through a web UI that authenticates
a session and then issues mutations against the board
> - A CSRF-style guard (`boardMutationGuard`) protects those mutations
by requiring the request origin match a trusted set built from the
`Host` / `X-Forwarded-Host` header
> - Behind certain reverse proxies, neither header matches the public
URL — TLS terminates at the edge and the inbound `Host` carries an
internal service name (cluster-local hostname, IP, or an Ingress backend
reference)
> - Mutations from legitimate browser sessions then fail with `403 Board
mutation requires trusted browser origin`
> - `PAPERCLIP_PUBLIC_URL` is already the canonical "what operators told
us the public URL is" value — it's used by better-auth and `config.ts`
> - This pull request adds it to the trusted-origin set when set, so
browsers reaching the legit public URL aren't blocked
## What Changed
- `server/src/middleware/board-mutation-guard.ts` — parse
`PAPERCLIP_PUBLIC_URL` and add its origin to the trusted set in
`trustedOriginsForRequest`. Additive only.
## Verification
- `PAPERCLIP_PUBLIC_URL=https://example.com pnpm start` then issue a
mutation from a browser pointed at `https://example.com`: 200, as
before. From an unrecognized origin: 403, as before.
- Without `PAPERCLIP_PUBLIC_URL` set: behavior is unchanged.
## Risks
Low. Additive only. The default dev origins and the
`Host`/`X-Forwarded-Host`-derived origins continue to be trusted; this
just adds the operator-configured public URL on top.
## Model Used
Claude Opus 4.6 (1M context), extended thinking mode.
## Checklist
- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
pino-pretty's translateTime: "HH:MM:ss" formats all timestamps in UTC,
ignoring the process TZ environment variable. Changing the prefix to
"SYS:HH:MM:ss" instructs pino-pretty to format timestamps in the local
system timezone, so operators running in non-UTC zones see correct
wall-clock times in both the console and the server.log file.
Fixes: #2879
Pino logged full Authorization headers in plaintext to server.log,
exposing JWT tokens to any process with filesystem read access.
Add redact paths so Bearer values appear as [Redacted] in log output.
Closes#2385
Restructure the TelemetryClient to send the correct backend envelope
format ({app, schemaVersion, installId, events: [{name, occurredAt, dimensions}]})
instead of the old per-event format. Update all event dimension names
to match the backend registry (agent_role, adapter_type, error_code, etc.).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add the shared telemetry sender, wire the CLI/server emit points,
and cover the config and completion behavior with tests.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Behind a reverse proxy with a custom port (e.g. Caddy on :3443), the
browser sends an Origin header that includes the port, but the board
mutation guard only read the Host header which often omits the port.
This caused a 403 "Board mutation requires trusted browser origin"
for self-hosted deployments behind reverse proxies.
Read x-forwarded-host (first value, comma-split) with the same pattern
already used in private-hostname-guard.ts and routes/access.ts.
Fixes#1734
Extract attachErrorContext helper to DRY up the error handler, attach the
original Error object to res.err so pino can serialize stack traces, and
rename the log context key from err to errorContext so it doesn't clash
with pino's built-in err serializer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix (8151331) set res.err but pino-http wasn't picking it
up (likely Express 5 response object behavior). Switch to a custom
__errorContext property on the response that customErrorMessage and
customProps read directly, bypassing pino-http's unreliable res.err
check. Remove duplicate manual logger.error calls from the error
handler since pino-http now gets the full context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pino-http customErrorMessage now includes the real error message
- customProps includes reqBody, reqParams, reqQuery, and routePath for 4xx/5xx
- Error handler logs full request context (body, params, query) for both
HttpError 500s and unhandled errors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pino-http checks res.err before falling back to its generic
"failed with status code 500" error. Set res.err to the real error
in the error handler so logs include the actual message and stack trace.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pino-http was logging 500s at INFO level with a generic "failed with status
code 500" message. Now 500s log at ERROR level and include the actual error
(message, stack, name) via res.locals handoff from the error handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename all workspace packages from @paperclip/* to @paperclipai/* and
the CLI binary from `paperclip` to `paperclipai` in preparation for
npm publishing. Bump CLI version to 0.1.0 and add package metadata
(description, keywords, license, repository, files). Update all
imports, documentation, user-facing messages, and tests accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add /api/auth/get-session for board actors to retrieve their session
info. Wrap resolveSession in try/catch to prevent unhandled errors from
crashing requests when auth headers are missing or malformed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update dev origin allowlist and tests to use port 3100 only, matching
the unified dev server setup. Fix AGENTS.md and README accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reject requests from unrecognised Host headers when running
authenticated/private. Adds server middleware, CLI `allowed-hostname`
command, config-schema field, and prompt support for configuring
allowed hostnames during onboard/configure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up Better Auth for session-based authentication. Add actor middleware
that resolves local_trusted mode to an implicit board actor and authenticated
mode to Better Auth sessions. Add access service with membership, permission,
invite, and join-request management. Register access routes for member/invite/
join-request CRUD. Update health endpoint to report deployment mode and
bootstrap status. Enforce tasks:assign and agents:create permissions in issue
and agent routes. Add deployment mode validation at startup with guardrails
(loopback-only for local_trusted, auth config required for authenticated).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Require trusted browser origin (Origin or Referer header) for
mutating requests from board actors, preventing cross-origin
mutation attempts against the local-trusted API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add per-issue execution lock (executionRunId, executionAgentNameKey,
executionLockedAt) to prevent concurrent runs on the same issue.
Same-name wakes are coalesced into the active run; different-name
wakes are deferred and promoted when the lock holder finishes.
Includes checkout/release run ownership enforcement, agent run ID
propagation from JWT claims, wakeup deduplication across assignee
and mention wakes, and claimQueuedRun extraction for reuse. Adds
two DB migrations for checkoutRunId and execution lock columns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete storage objects for each attachment when an issue is removed.
Hide responseTime from pino-pretty console output to reduce noise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent management: hire endpoint with permission gates and pending_approval status,
config revision tracking with rollback, agent duplicate route, permission CRUD.
Block pending_approval agents from auth, heartbeat, and assignments.
Approvals: revision request/resubmit flow, approval comments CRUD, issue-approval
linking, auto-wake agents on approval decisions with context snapshot.
Costs: per-agent breakdown, period filtering (month/week/day/all), cost by agent
list endpoint.
Adapters: agentConfigurationDoc on all adapters, /llms/agent-configuration.txt
reflection routes. Inject PAPERCLIP_APPROVAL_ID, PAPERCLIP_APPROVAL_STATUS,
PAPERCLIP_LINKED_ISSUE_IDS into adapter environments.
Sidebar badges endpoint for pending approval/inbox counts. Dashboard and company
settings extensions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace auto-migrate-if-empty with interactive migration flow that inspects
pending migrations and prompts before applying. Add pino-pretty for structured
console + file logging. Add reapOrphanedRuns to clean up stuck heartbeat runs
on startup and periodically. Track runId through auth middleware, activity logs,
and all mutation routes. Add issue-run cross-reference queries, live-run and
active-run endpoints for issues, issue identifier lookup, reopen-via-comment
flow, and done/cancelled -> todo status transitions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add HS256 JWT-based authentication for local adapters (claude_local, codex_local)
so agents authenticate automatically without manual API key configuration. The
server mints short-lived JWTs per heartbeat run and injects them as PAPERCLIP_API_KEY.
The auth middleware verifies JWTs alongside existing static API keys.
Includes: CLI onboard/doctor JWT secret management, env command for deployment,
config path resolution from ancestor directories, dotenv loading on server startup,
event payload secret redaction, multi-status issue filtering, and adapter transcript
parsing for thinking/user message kinds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New routes: companies, approvals, costs, dashboard, authz. New
services: companies, approvals, costs, dashboard, heartbeat,
activity-log. Add auth middleware and structured error handling.
Expand existing agent and issue routes with richer CRUD operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Express server with CRUD routes for agents, goals, issues, projects,
and activity log. Includes validation middleware, structured error
handling, request logging, and health check endpoint with tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>