From 93ff6a87718397739ff75a82f333be7032cfb17b Mon Sep 17 00:00:00 2001 From: seb-veto Date: Mon, 17 Aug 2026 14:02:16 -0700 Subject: [PATCH] =?UTF-8?q?fix(cursor-cloud):=20drop=20unreachable=20Paper?= =?UTF-8?q?clip=20API=20callback=20for=20remote=E2=80=A6=20(#8546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents run via per-adapter execute paths; the `cursor_cloud` adapter runs the agent in Cursor's cloud (remote), orchestrated server-side via the Cursor Agent SDK > - Local adapters receive a run-scoped Paperclip JWT (`supportsLocalAgentJwt=true`) injected as `PAPERCLIP_API_KEY` so the agent can call the Paperclip API; `cursor_cloud` is intentionally `supportsLocalAgentJwt=false` (no JWT minted for a remote worker) > - But `buildPaperclipEnv` always sets `PAPERCLIP_API_URL` (defaulting to the local runtime host), so the remote cloud worker is handed a callback URL it can neither reach nor authenticate against > - Any agent-initiated Paperclip API call from the cloud worker therefore fails with a 401 (or is unreachable), producing log noise and confusing failures > - This pull request drops the callback wiring when there is no usable key, so cloud-side Paperclip tools degrade to a clean no-op > - The benefit is no spurious 401s from remote cloud runs, with run results unaffected (delivered server-side via the Cursor Agent SDK) ## Linked Issues or Issue Description No existing public issue — describing the bug inline (per `.github/ISSUE_TEMPLATE/bug_report.yml`): **What happened** `cursor_cloud` runs emit 401s when the remote cloud agent attempts Paperclip API calls. Root cause: `buildPaperclipEnv` (`packages/adapter-utils/src/server-utils.ts`) always sets `PAPERCLIP_API_URL` (local runtime default), while `cursor_cloud` has `supportsLocalAgentJwt=false`, so no `PAPERCLIP_API_KEY` is minted — URL present, key absent → 401 / unreachable from `buildWakeEnv` in `packages/adapters/cursor-cloud/src/server/execute.ts`. **Expected behavior** A remote cloud worker that is not issued a run JWT should not attempt (and fail) Paperclip API callbacks. **Steps to reproduce** 1. Configure a `cursor_cloud` agent (runs in Cursor's cloud; `supportsLocalAgentJwt=false`). 2. Trigger a run that causes the cloud agent to make a Paperclip API call. 3. Observe a 401 (or unreachable) because `PAPERCLIP_API_URL` points at an unreachable local runtime and no key is present. **Paperclip version** Reproduced on current `master` (cutover base `e68188c43`). **Deployment mode** Self-hosted control plane, `cursor_cloud` adapter (remote execution in Cursor's cloud). **Related PRs (searched; none duplicate this fix):** - #8197 — `claude_local` opt-out of the sandbox *bridge* for direct-reachable remote SSH targets. Related family, but the opposite situation: that path keeps the callback because the remote is reachable **and** has a run token. `cursor_cloud` has neither, so here the callback is removed. - #8130, #4794, #8025 — `PAPERCLIP_API_URL`/loopback injection for **local** agents (distinct from the remote cloud worker case). - #401 — alternative agent-auth scheme (run-ID header when no bearer token); different approach, not overlapping with this targeted fix. ## What Changed - `packages/adapters/cursor-cloud/src/server/execute.ts`: in `buildWakeEnv`, when there is no usable `PAPERCLIP_API_KEY`, delete `PAPERCLIP_API_URL` and `PAPERCLIP_API_BRIDGE_MODE` so the remote worker performs no Paperclip API callbacks. Informational `PAPERCLIP_*` vars (run id, agent id, company id, task, wake reason) still flow. When a key *is* present (operator-provided), the URL is retained. - `packages/adapters/cursor-cloud/src/server/execute.test.ts`: new test asserting no callback vars are injected when no run JWT is present; positive assertion that the URL is retained when a key is present. ## Verification - `pnpm exec vitest run packages/adapters/cursor-cloud/src/server/execute.test.ts` → **5/5 pass**. - `pnpm --filter @paperclipai/adapter-cursor-cloud typecheck` → **green**. - Confirmed result delivery does not depend on this callback: `execute()` reads results server-side via `Agent.getRun()` and `run.wait()`. ## Risks - **Low risk.** Only affects the env handed to remote `cursor_cloud` workers. No schema/migration/behavioral change to result delivery (which is server-side). When an operator explicitly provides `PAPERCLIP_API_KEY`, the callback URL is retained, preserving intentional callback setups. ## Model Used - **Claude Opus 4.8** (Anthropic), extended/high reasoning mode, via the Cursor agent with tool use + code execution. Diagnosis grounded in the adapter/runtime code. ## 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 found) - [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 references) - [x] My branch name describes the change (`fix/cursor-cloud-skip-unreachable-callback`) 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 - [ ] I have updated relevant documentation to reflect my changes (N/A — no documented behavior changes) - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green (pending CI run) - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups (pending review) - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Sebastian Heyneman Co-authored-by: Cursor --- .../cursor-cloud/src/server/execute.test.ts | 26 +++++++++++++++++++ .../cursor-cloud/src/server/execute.ts | 14 ++++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/adapters/cursor-cloud/src/server/execute.test.ts b/packages/adapters/cursor-cloud/src/server/execute.test.ts index 145c4eec0d..e3b11d990b 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.test.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.test.ts @@ -179,6 +179,9 @@ describe("cursor_cloud execute", () => { PAPERCLIP_WAKE_REASON: "issue_commented", PAPERCLIP_API_KEY: "paperclip-run-jwt", }); + // When a run JWT is present the callback URL is retained so the worker can + // authenticate its Paperclip API calls. + expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).toHaveProperty("PAPERCLIP_API_URL"); expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).not.toHaveProperty("CURSOR_API_KEY"); expect(result).toMatchObject({ @@ -204,6 +207,29 @@ describe("cursor_cloud execute", () => { ); }); + it("omits the Paperclip API callback when no run JWT is issued (remote worker cannot call home)", async () => { + const run = createMockRun({ agentId: "agent-no-jwt" }); + const sdkAgent = createMockSdkAgent({ agentId: "agent-no-jwt", sendRun: run }); + createMock.mockResolvedValue(sdkAgent); + // cursor_cloud is registered with supportsLocalAgentJwt=false, so heartbeat + // passes no authToken. A remote cloud worker must not receive a callback URL + // it can neither reach nor authenticate against (the source of 401 noise). + const ctx = createContext({ authToken: undefined }); + + await execute(ctx); + + const envVars = (createMock.mock.calls[0]?.[0]?.cloud?.envVars ?? {}) as Record; + expect(envVars).not.toHaveProperty("PAPERCLIP_API_KEY"); + expect(envVars).not.toHaveProperty("PAPERCLIP_API_URL"); + expect(envVars).not.toHaveProperty("PAPERCLIP_API_BRIDGE_MODE"); + // Informational Paperclip env (non-credential) still flows through. + expect(envVars).toMatchObject({ + PAPERCLIP_RUN_ID: "run-heartbeat-1", + PAPERCLIP_AGENT_ID: "agent-1", + PAPERCLIP_COMPANY_ID: "company-1", + }); + }); + it("resumes a matching saved session when no active run can be reattached", async () => { getRunMock.mockResolvedValue(createMockRun({ status: "finished" })); const resumedRun = createMockRun({ id: "run-resumed", agentId: "agent-resumed" }); diff --git a/packages/adapters/cursor-cloud/src/server/execute.ts b/packages/adapters/cursor-cloud/src/server/execute.ts index e5309cdefc..97cbb9312b 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.ts @@ -135,6 +135,20 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record = [ ["PAPERCLIP_WORKSPACE_CWD", workspace.cwd],