fix(cursor-cloud): drop unreachable Paperclip API callback for remote… (#8546)

## 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 <sebastian@joinnova.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
seb-veto 2026-08-17 14:02:16 -07:00 committed by GitHub
parent 2ae6fa51b1
commit 93ff6a8771
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 0 deletions

View File

@ -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<string, string>;
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" });

View File

@ -135,6 +135,20 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
env.PAPERCLIP_API_KEY = authToken;
}
// cursor_cloud runs remotely in Cursor's cloud and is intentionally not
// issued a Paperclip run JWT (registry: supportsLocalAgentJwt=false).
// buildPaperclipEnv always sets PAPERCLIP_API_URL, defaulting to the local
// runtime host — which a remote worker can neither reach nor authenticate
// against, so any agent-initiated Paperclip API call would fail with a 401
// (or be unreachable) and add noise. When there is no usable key, drop the
// callback wiring so cloud-side Paperclip tools degrade to a clean no-op.
// Run results are delivered server-side via the Cursor Agent SDK (getRun /
// wait), not through this callback, so nothing is lost.
if (!trimNullable(env.PAPERCLIP_API_KEY)) {
delete env.PAPERCLIP_API_URL;
delete env.PAPERCLIP_API_BRIDGE_MODE;
}
const workspace = parseObject(context.paperclipWorkspace);
const workspaceMappings: Array<[string, unknown]> = [
["PAPERCLIP_WORKSPACE_CWD", workspace.cwd],