[codex] Allow cloud tenant import mutations without browser origin (#6378)

## 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
This commit is contained in:
Dotta
2026-05-19 14:25:58 -05:00
committed by GitHub
parent bfe6369ef5
commit 9c29394f4d
2 changed files with 14 additions and 3 deletions
@@ -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)
@@ -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;
}