fix: enforce assigned identity in managed cloud environments

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 12:40:16 -05:00
1 parent aa69c14e0b
commit 7b0265da40
3 files changed
+45

No files matched your search

+7
View File
@@ -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.
@@ -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();
@@ -17,6 +17,7 @@ import {
asString,
buildPaperclipEnv,
buildRuntimeToolsEnv,
isForbiddenConfigEnvKey,
joinPromptSections,
parseObject,
readPaperclipIssueWorkModeFromContext,
@@ -59,6 +60,7 @@ function asStringEnvMap(value: unknown): Record<string, string> {
const parsed = parseObject(value);
const env: Record<string, string> = {};
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<string, st
const { runId, agent, context, authToken } = ctx;
const env: Record<string, string> = {
...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,