fix(server): respect externally set PAPERCLIP_API_URL env var (#3472)

## Thinking Path

> - Paperclip server starts up and sets internal `PAPERCLIP_API_URL` for
downstream services and adapters
> - The server startup code was unconditionally overwriting
`PAPERCLIP_API_URL` with `http://localhost:3100` (or equivalent based on
`config.host`)
> - In Kubernetes deployments, `PAPERCLIP_API_URL` is set via a
ConfigMap to the externally accessible load balancer URL (e.g.
`https://paperclip.example.com`)
> - Because the env var was unconditionally set after loading the
ConfigMap value, the ConfigMap-provided URL was ignored and replaced
with the internal localhost address
> - This caused downstream services (adapter env building) to use the
wrong URL, breaking external access
> - This pull request makes the assignment conditional — only set if not
already provided by the environment
> - External deployments can now supply `PAPERCLIP_API_URL` and it will
be respected; local development continues to work without setting it

## What Changed

- `server/src/index.ts`: Wrapped `PAPERCLIP_API_URL` assignment in `if
(!process.env.PAPERCLIP_API_URL)` guard so externally provided values
are preserved
- `server/src/__tests__/server-startup-feedback-export.test.ts`: Added
tests verifying external `PAPERCLIP_API_URL` is respected and fallback
behavior is correct
- `docs/deploy/environment-variables.md`: Updated `PAPERCLIP_API_URL`
description to clarify it can be externally provided and the load
balancer/reverse proxy use case

## Verification

- Run the existing test suite: `pnpm test:run
server/src/__tests__/server-startup-feedback-export.test.ts` — all 3
tests pass
- Manual verification: Set `PAPERCLIP_API_URL` to a custom value before
starting the server and confirm it is not overwritten

## Risks

- Low risk — purely additive conditional check; existing behavior for
unset env var is unchanged

## Model Used

MiniMax M2.7 — reasoning-assisted for tracing the root cause through the
startup chain (`buildPaperclipEnv` → `startServer` → `config.host` →
`HOST` env var)

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Pawla Abdul (Bot) <pawla@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Chris Farhood
2026-04-15 07:43:48 -04:00
committed by GitHub
parent d0a8d4e08a
commit b816809a1e
3 changed files with 30 additions and 3 deletions

View File

@@ -18,6 +18,7 @@ All environment variables that Paperclip uses for server configuration.
| `PAPERCLIP_INSTANCE_ID` | `default` | Instance identifier (for multiple local instances) |
| `PAPERCLIP_DEPLOYMENT_MODE` | `local_trusted` | Runtime mode override |
| `PAPERCLIP_DEPLOYMENT_EXPOSURE` | `private` | Exposure policy when deployment mode is `authenticated` |
| `PAPERCLIP_API_URL` | (auto-derived) | Paperclip API base URL. When set externally (e.g., via Kubernetes ConfigMap, load balancer, or reverse proxy), the server preserves the value instead of deriving it from the listen host and port. Useful for deployments where the public-facing URL differs from the local bind address. |
## Secrets
@@ -35,7 +36,7 @@ These are set automatically by the server when invoking agents:
|----------|-------------|
| `PAPERCLIP_AGENT_ID` | Agent's unique ID |
| `PAPERCLIP_COMPANY_ID` | Company ID |
| `PAPERCLIP_API_URL` | Paperclip API base URL |
| `PAPERCLIP_API_URL` | Paperclip API base URL (inherits the server-level value; see Server Configuration above) |
| `PAPERCLIP_API_KEY` | Short-lived JWT for API auth |
| `PAPERCLIP_RUN_ID` | Current heartbeat run ID |
| `PAPERCLIP_TASK_ID` | Issue that triggered this wake |

View File

@@ -180,3 +180,27 @@ describe("startServer feedback export wiring", () => {
});
});
});
describe("startServer PAPERCLIP_API_URL handling", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.BETTER_AUTH_SECRET = "test-secret";
delete process.env.PAPERCLIP_API_URL;
});
it("uses the externally set PAPERCLIP_API_URL when provided", async () => {
process.env.PAPERCLIP_API_URL = "http://custom-api:3100";
const started = await startServer();
expect(started.apiUrl).toBe("http://custom-api:3100");
expect(process.env.PAPERCLIP_API_URL).toBe("http://custom-api:3100");
});
it("falls back to host-based URL when PAPERCLIP_API_URL is not set", async () => {
const started = await startServer();
expect(started.apiUrl).toBe("http://127.0.0.1:3210");
expect(process.env.PAPERCLIP_API_URL).toBe("http://127.0.0.1:3210");
});
});

View File

@@ -554,7 +554,9 @@ export async function startServer(): Promise<StartedServer> {
: runtimeListenHost;
process.env.PAPERCLIP_LISTEN_HOST = runtimeListenHost;
process.env.PAPERCLIP_LISTEN_PORT = String(listenPort);
process.env.PAPERCLIP_API_URL = `http://${runtimeApiHost}:${listenPort}`;
if (!process.env.PAPERCLIP_API_URL) {
process.env.PAPERCLIP_API_URL = `http://${runtimeApiHost}:${listenPort}`;
}
setupLiveEventsWebSocketServer(server, db as any, {
deploymentMode: config.deploymentMode,
@@ -792,7 +794,7 @@ export async function startServer(): Promise<StartedServer> {
server,
host: config.host,
listenPort,
apiUrl: process.env.PAPERCLIP_API_URL ?? `http://${runtimeApiHost}:${listenPort}`,
apiUrl: process.env.PAPERCLIP_API_URL!,
databaseUrl: activeDatabaseConnectionString,
};
}