diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index 81ec8458b5..1bfea41d35 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -10,6 +10,7 @@ import { buildPersistentSkillSnapshot, buildRuntimeMountedSkillSnapshot, buildInvocationEnvForLogs, + buildPaperclipEnv, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, materializePaperclipSkillCopy, refreshPaperclipWorkspaceEnvForExecution, @@ -2581,3 +2582,58 @@ describe("appendWithByteCap", () => { expect(Buffer.byteLength(output, "utf8")).toBeLessThanOrEqual(7); }); }); + +describe("buildPaperclipEnv", () => { + const ENV_KEYS = [ + "PAPERCLIP_API_URL", + "PAPERCLIP_RUNTIME_API_URL", + "PAPERCLIP_LISTEN_HOST", + "PAPERCLIP_LISTEN_PORT", + "HOST", + "PORT", + ] as const; + + function withEnv(overrides: Record, fn: () => void) { + const saved = new Map(); + for (const key of ENV_KEYS) saved.set(key, process.env[key]); + try { + for (const key of ENV_KEYS) delete process.env[key]; + for (const [key, value] of Object.entries(overrides)) process.env[key] = value; + fn(); + } finally { + for (const [key, value] of saved) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } + } + + it("prefers an explicit PAPERCLIP_API_URL override over the derived runtime URL", () => { + withEnv( + { + PAPERCLIP_API_URL: "http://localhost:3100", + PAPERCLIP_RUNTIME_API_URL: "http://203.0.113.7:3100", + }, + () => { + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + expect(env.PAPERCLIP_API_URL).toBe("http://localhost:3100"); + expect(env.PAPERCLIP_AGENT_ID).toBe("agent-1"); + expect(env.PAPERCLIP_COMPANY_ID).toBe("company-1"); + }, + ); + }); + + it("falls back to the derived runtime URL when no explicit override is set", () => { + withEnv({ PAPERCLIP_RUNTIME_API_URL: "http://203.0.113.7:3100" }, () => { + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + expect(env.PAPERCLIP_API_URL).toBe("http://203.0.113.7:3100"); + }); + }); + + it("derives a listen-host URL when neither override is set", () => { + withEnv({ PAPERCLIP_LISTEN_HOST: "0.0.0.0", PAPERCLIP_LISTEN_PORT: "3200" }, () => { + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + expect(env.PAPERCLIP_API_URL).toBe("http://localhost:3200"); + }); + }); +}); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 7f4cf5cd70..80b9909b23 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -1993,9 +1993,12 @@ export function buildPaperclipEnv(agent: { id: string; companyId: string }): Rec process.env.PAPERCLIP_LISTEN_HOST ?? process.env.HOST ?? "localhost", ); const runtimePort = process.env.PAPERCLIP_LISTEN_PORT ?? process.env.PORT ?? "3100"; + // An explicit PAPERCLIP_API_URL override must win over the URL derived from + // authPublicBaseUrl: the derived URL can be unreachable from inside the + // runtime container (e.g. when the public base URL is VPN/tailnet-only). const apiUrl = - process.env.PAPERCLIP_RUNTIME_API_URL ?? process.env.PAPERCLIP_API_URL ?? + process.env.PAPERCLIP_RUNTIME_API_URL ?? `http://${runtimeHost}:${runtimePort}`; vars.PAPERCLIP_API_URL = apiUrl; return vars; diff --git a/server/src/__tests__/paperclip-env.test.ts b/server/src/__tests__/paperclip-env.test.ts index e397f08242..e48c32fdc6 100644 --- a/server/src/__tests__/paperclip-env.test.ts +++ b/server/src/__tests__/paperclip-env.test.ts @@ -29,7 +29,7 @@ afterEach(() => { }); describe("buildPaperclipEnv", () => { - it("prefers an explicit PAPERCLIP_RUNTIME_API_URL", () => { + it("prefers an explicit PAPERCLIP_API_URL override over the derived runtime URL", () => { process.env.PAPERCLIP_RUNTIME_API_URL = "http://203.0.113.42:3102"; process.env.PAPERCLIP_API_URL = "http://localhost:4100"; process.env.PAPERCLIP_LISTEN_HOST = "127.0.0.1"; @@ -37,6 +37,17 @@ describe("buildPaperclipEnv", () => { const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + expect(env.PAPERCLIP_API_URL).toBe("http://localhost:4100"); + }); + + it("falls back to PAPERCLIP_RUNTIME_API_URL when no explicit override is set", () => { + process.env.PAPERCLIP_RUNTIME_API_URL = "http://203.0.113.42:3102"; + delete process.env.PAPERCLIP_API_URL; + process.env.PAPERCLIP_LISTEN_HOST = "127.0.0.1"; + process.env.PAPERCLIP_LISTEN_PORT = "3101"; + + const env = buildPaperclipEnv({ id: "agent-1", companyId: "company-1" }); + expect(env.PAPERCLIP_API_URL).toBe("http://203.0.113.42:3102"); });