{"record":{"id":"30deb577576ca661","repo":"paperclipai/paperclip","slug":"paperclip-bridge-token-is-required","errorCode":null,"errorMessage":"PAPERCLIP_BRIDGE_TOKEN is required.","messagePattern":"PAPERCLIP_BRIDGE_TOKEN is required\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/adapter-utils/src/sandbox-callback-bridge.ts","lineNumber":1979,"sourceCode":"    stream.on(\"response\", (headers) => {\n      const rawStatus = headers[\":status\"];\n      status = typeof rawStatus === \"number\" ? rawStatus : Number(rawStatus) || 502;\n      responseHeaders = {};\n      for (const [key, value] of Object.entries(headers)) {\n        if (key.startsWith(\":\") || value == null) continue;\n        responseHeaders[key] = Array.isArray(value) ? value.join(\", \") : String(value);\n      }\n    });\n    stream.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n    stream.once(\"end\", () => settle(() => resolve({ status, headers: responseHeaders, body: Buffer.concat(chunks) })));\n    stream.once(\"error\", (error) =>\n      settle(() => reject(error instanceof Error ? error : new Error(String(error)))),\n    );\n    stream.once(\"aborted\", () => settle(() => reject(new Error(\"Bridge HTTP/2 stream aborted.\"))));\n    if (request.body.length > 0) {\n      stream.end(request.body);\n    } else if (!stream.writableEnded) {\n      stream.end();\n    }\n  });\n}\n\n/**\n * Create the sandbox HTTP/2 client gateway. It opens one HTTP/2 client\n * session on the transport `createConnection` returns, and forwards each\n * local request the caller hands it (already checked against the bridge\n * token — see {@link SandboxHttp2BridgeGatewayRequest.receivedToken}) as one\n * HTTP/2 stream. It keeps the header allowlist on the sandbox side, exactly\n * as the file-mode gateway does.\n */\nexport function createSandboxHttp2BridgeGateway(\n  options: CreateSandboxHttp2BridgeGatewayOptions,\n): SandboxHttp2BridgeGateway {\n  const authority = options.authority?.trim() || SANDBOX_HTTP2_GATEWAY_DEFAULT_AUTHORITY;\n  const headerAllowlist = options.headerAllowlist ?? DEFAULT_SANDBOX_CALLBACK_BRIDGE_HEADER_ALLOWLIST;\n  const session = http2.connect(`http://${authority}`, {","sourceCodeStart":1961,"sourceCodeEnd":1997,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/adapter-utils/src/sandbox-callback-bridge.ts#L1961-L1997","documentation":"Thrown at top level of the zero-dependency bridge server that getSandboxCallbackBridgeServerSource() generates; the script runs INSIDE the sandbox and refuses to start unless PAPERCLIP_BRIDGE_TOKEN is set in its environment. The token is the shared secret the sandbox uses to authenticate callbacks to the host, so a missing token cannot be defaulted or ignored.","triggerScenarios":"Spawning the generated bridge source without PAPERCLIP_BRIDGE_TOKEN in env; a host-side launcher that fails to inject the minted token into the sandbox process env; manually copy-pasting the generated script into a shell for debugging without exporting the variable.","commonSituations":"Refactoring the sandbox spawn path and dropping the env entry; environment scrubbing (a wrapper that clears env before exec); debugging the bridge standalone outside Paperclip; note file mode additionally requires PAPERCLIP_BRIDGE_QUEUE_DIR (separate error) while duplex mode only needs the token.","solutions":["Ensure the sandbox process env includes PAPERCLIP_BRIDGE_TOKEN with the token the host minted for this sandbox (pass it in the spawn options' env, merged with process.env).","If you run the generated script manually, export a token first: PAPERCLIP_BRIDGE_TOKEN=$(paperclip ... ) node bridge.mjs.","Check for an env-scrubbing wrapper (docker --env-file omission, su -l, set -u with unset vars) between the host launcher and the bridge process.","Never log the token while debugging; verify presence with [ -n \"$PAPERCLIP_BRIDGE_TOKEN\" ] instead of printing it."],"exampleFix":"// before\nconst child = spawn(process.execPath, [bridgeScriptPath], {\n  env: { PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir },\n});\n\n// after\nconst child = spawn(process.execPath, [bridgeScriptPath], {\n  env: { ...process.env, PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir, PAPERCLIP_BRIDGE_TOKEN: mintedToken },\n});","handlingStrategy":"validation","validationCode":"// Host side, before spawning the bridge inside the sandbox:\nif (!bridgeToken) {\n  throw new Error(\"refusing to start sandbox callback bridge: no token minted\");\n}\nconst env = { ...sandboxEnv, PAPERCLIP_BRIDGE_TOKEN: bridgeToken };","typeGuard":null,"tryCatchPattern":"try {\n  await startBridge(sandbox, token);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"PAPERCLIP_BRIDGE_TOKEN\")) {\n    // env injection bug on the host side; re-mint and re-spawn, never start the bridge without it\n    throw new Error(\"bridge env missing token; check sandbox spawn env injection\");\n  }\n  throw error;\n}","preventionTips":["Always merge the token into the child env explicitly at the spawn site; do not rely on inherited env.","Watch for env-scrubbing wrappers (docker env-file, su -l, sanitizers) between host and bridge.","Verify presence with a boolean check, never by printing the token.","In duplex mode remember QUEUE_DIR is not required; in file mode it is — check both up front."],"tags":["sandbox","bridge","environment-variable","auth","missing-env"],"backgroundTag":"missing-env-var","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-08-21T17:58:32.592Z","contentChangedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}