From 9c29394f4df27ca26104d9108175a67726202d72 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 19 May 2026 14:25:58 -0500 Subject: [PATCH] [codex] Allow cloud tenant import mutations without browser origin (#6378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- server/src/__tests__/board-mutation-guard.test.ts | 8 +++++++- server/src/middleware/board-mutation-guard.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/board-mutation-guard.test.ts b/server/src/__tests__/board-mutation-guard.test.ts index 6dc222d7..eb3721ab 100644 --- a/server/src/__tests__/board-mutation-guard.test.ts +++ b/server/src/__tests__/board-mutation-guard.test.ts @@ -5,7 +5,7 @@ import { boardMutationGuard } from "../middleware/board-mutation-guard.js"; function createApp( actorType: "board" | "agent", - boardSource: "session" | "local_implicit" | "board_key" = "session", + boardSource: "session" | "local_implicit" | "board_key" | "cloud_tenant" = "session", ) { const app = express(); app.use(express.json()); @@ -66,6 +66,12 @@ describe("boardMutationGuard", () => { expect([200, 204]).toContain(res.status); }); + it("allows trusted Cloud tenant mutations without origin", async () => { + const app = createApp("board", "cloud_tenant"); + const res = await request(app).post("/mutate").send({ ok: true }); + expect([200, 204]).toContain(res.status); + }); + it("allows board mutations from trusted origin", async () => { const app = createApp("board"); const res = await request(app) diff --git a/server/src/middleware/board-mutation-guard.ts b/server/src/middleware/board-mutation-guard.ts index 96e2a461..a6347b86 100644 --- a/server/src/middleware/board-mutation-guard.ts +++ b/server/src/middleware/board-mutation-guard.ts @@ -56,9 +56,14 @@ export function boardMutationGuard(): RequestHandler { return; } - // Local-trusted mode and board bearer keys are not browser-session requests. + // Local-trusted mode, board bearer keys, and trusted Cloud tenant calls are + // not browser-session requests. // In these modes, origin/referer headers can be absent; do not block those mutations. - if (req.actor.source === "local_implicit" || req.actor.source === "board_key") { + if ( + req.actor.source === "local_implicit" + || req.actor.source === "board_key" + || req.actor.source === "cloud_tenant" + ) { next(); return; }