diff --git a/doc/AGENT-IDENTITY.md b/doc/AGENT-IDENTITY.md index c10108092dd..cebdf9c8216 100644 --- a/doc/AGENT-IDENTITY.md +++ b/doc/AGENT-IDENTITY.md @@ -44,6 +44,13 @@ before its next turn. Private values are excluded from invocation metadata and execution configuration; output and failure diagnostics redact known private material. The run-log redactor handles split stdout/stderr chunks independently. +Managed remote providers, including Cursor Cloud, receive the same private key +as managed local processes. Selecting a runtime therefore trusts its host with +the agent's persistent identity: the agent and its host can retain the key and +sign outside Paperclip. Encryption at rest protects database storage, not an +executing runtime. Move an agent only between runtime hosts trusted with that +identity. + Independently hosted HTTP and gateway agents, including Runner's API-hosted Claude Managed and AWS AgentCore providers, do not receive private keys in v1. Their new agent records still get stored identities; their existing records stay unprovisioned until a supported managed run. diff --git a/packages/adapters/cursor-cloud/src/server/execute.test.ts b/packages/adapters/cursor-cloud/src/server/execute.test.ts index b57f8207814..156ed5d9607 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.test.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.test.ts @@ -1,3 +1,4 @@ +import { generateKeyPairSync, sign, verify } from "node:crypto"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AdapterExecutionContext } from "@paperclipai/adapter-utils"; import { createPromptContextFixture } from "@paperclipai/adapter-utils/test-fixtures/prompt-context"; @@ -266,6 +267,39 @@ describe("cursor_cloud execute", () => { ); }); + it("delivers the assigned identity to the managed cloud environment without exposing it in prompts or metadata", async () => { + const keys = generateKeyPairSync("ed25519"); + const identity = { + keyId: "sha256:assigned-identity", + publicKeyPem: keys.publicKey.export({ type: "spki", format: "pem" }).toString(), + privateKeyPem: keys.privateKey.export({ type: "pkcs8", format: "pem" }).toString(), + }; + const sdkAgent = createMockSdkAgent(); + createMock.mockResolvedValue(sdkAgent); + const ctx = createContext({ agentIdentity: identity }); + ctx.config.env = { + CURSOR_API_KEY: "cursor-secret", + PAPERCLIP_AGENT_KEY_ID: "configured-override", + PAPERCLIP_AGENT_PRIVATE_KEY: "configured-override", + paperclip_agent_private_key: "configured-override", + }; + + await execute(ctx); + + const env = createMock.mock.calls[0]?.[0]?.cloud?.envVars; + expect(env).toMatchObject({ + PAPERCLIP_AGENT_KEY_ID: identity.keyId, + PAPERCLIP_AGENT_PUBLIC_KEY: identity.publicKeyPem, + PAPERCLIP_AGENT_PRIVATE_KEY: identity.privateKeyPem, + }); + expect(env).not.toHaveProperty("paperclip_agent_private_key"); + const challenge = Buffer.from("managed cloud identity"); + expect(verify(null, challenge, identity.publicKeyPem, + sign(null, challenge, env.PAPERCLIP_AGENT_PRIVATE_KEY))).toBe(true); + const privateBody = identity.privateKeyPem.split("\n")[1]; + expect(JSON.stringify([ctx.meta, ctx.logs, sdkAgent.send.mock.calls])).not.toContain(privateBody); + }); + it("omits empty environment values while preserving nonempty values exactly", async () => { createMock.mockResolvedValue(createMockSdkAgent()); const ctx = createContext(); diff --git a/packages/adapters/cursor-cloud/src/server/execute.ts b/packages/adapters/cursor-cloud/src/server/execute.ts index c4335fb71b8..3a14ba1a20b 100644 --- a/packages/adapters/cursor-cloud/src/server/execute.ts +++ b/packages/adapters/cursor-cloud/src/server/execute.ts @@ -17,6 +17,7 @@ import { asString, buildPaperclipEnv, buildRuntimeToolsEnv, + isForbiddenConfigEnvKey, joinPromptSections, parseObject, readPaperclipIssueWorkModeFromContext, @@ -59,6 +60,7 @@ function asStringEnvMap(value: unknown): Record { const parsed = parseObject(value); const env: Record = {}; for (const [key, entry] of Object.entries(parsed)) { + if (isForbiddenConfigEnvKey(key)) continue; if (typeof entry === "string") { env[key] = entry; } else if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) { @@ -108,6 +110,8 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record = { ...configEnv, + // The selected managed cloud host is trusted with this persistent identity, + // just like a managed local host (see doc/AGENT-IDENTITY.md). ...buildPaperclipEnv(agent, ctx.agentIdentity), ...buildRuntimeToolsEnv(ctx.runtimeTools), PAPERCLIP_RUN_ID: runId,