From 6aec34bc60120c3ea3b8bea46e56529be7fd6156 Mon Sep 17 00:00:00 2001 From: Jamieson O'Reilly <6668807+orlyjamie@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:08:03 +1100 Subject: [PATCH 1/4] fix(gateway): prevent auth bypass when behind unconfigured reverse proxy (#1795) * fix(gateway): prevent auth bypass when behind unconfigured reverse proxy When proxy headers (X-Forwarded-For, X-Real-IP) are present but gateway.trustedProxies is not configured, the gateway now treats connections as non-local. This prevents a scenario where all proxied requests appear to come from localhost and receive automatic trust. Previously, running behind nginx/Caddy without configuring trustedProxies would cause isLocalClient=true for all external connections, potentially bypassing authentication and auto-approving device pairing. The gateway now logs a warning when this condition is detected, guiding operators to configure trustedProxies for proper client IP detection. Also adds documentation for reverse proxy security configuration. * fix: harden reverse proxy auth (#1795) (thanks @orlyjamie) --------- Co-authored-by: orlyjamie Co-authored-by: Peter Steinberger --- CHANGELOG.md | 3 + docs/gateway/security.md | 17 ++++++ src/gateway/server.auth.e2e.test.ts | 21 +++++++ .../server/ws-connection/message-handler.ts | 56 +++++++++++++++++-- src/security/audit.test.ts | 24 ++++++++ src/security/audit.ts | 18 ++++++ 6 files changed, 133 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4edf42b68..3d00e7319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Docs: https://docs.clawd.bot ### Changes - TBD. +### Fixes +- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. + ## 2026.1.24-2 ### Fixes diff --git a/docs/gateway/security.md b/docs/gateway/security.md index ed0054411..05e1673c6 100644 --- a/docs/gateway/security.md +++ b/docs/gateway/security.md @@ -63,6 +63,23 @@ downgrade—prefer HTTPS (Tailscale Serve) or open the UI on `127.0.0.1`. `clawdbot security audit` warns when this setting is enabled. +## Reverse Proxy Configuration + +If you run the Gateway behind a reverse proxy (nginx, Caddy, Traefik, etc.), you should configure `gateway.trustedProxies` for proper client IP detection. + +When the Gateway detects proxy headers (`X-Forwarded-For` or `X-Real-IP`) from an address that is **not** in `trustedProxies`, it will **not** treat connections as local clients. If gateway auth is disabled, those connections are rejected. This prevents authentication bypass where proxied connections would otherwise appear to come from localhost and receive automatic trust. + +```yaml +gateway: + trustedProxies: + - "127.0.0.1" # if your proxy runs on localhost + auth: + mode: password + password: ${CLAWDBOT_GATEWAY_PASSWORD} +``` + +When `trustedProxies` is configured, the Gateway will use `X-Forwarded-For` headers to determine the real client IP for local client detection. Make sure your proxy overwrites (not appends to) incoming `X-Forwarded-For` headers to prevent spoofing. + ## Local session logs live on disk Clawdbot stores session transcripts on disk under `~/.clawdbot/agents//sessions/*.jsonl`. diff --git a/src/gateway/server.auth.e2e.test.ts b/src/gateway/server.auth.e2e.test.ts index a2645a75d..17a8802b2 100644 --- a/src/gateway/server.auth.e2e.test.ts +++ b/src/gateway/server.auth.e2e.test.ts @@ -351,6 +351,27 @@ describe("gateway server auth/connect", () => { } }); + test("rejects proxied connections without auth when proxy headers are untrusted", async () => { + const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; + delete process.env.CLAWDBOT_GATEWAY_TOKEN; + const port = await getFreePort(); + const server = await startGatewayServer(port); + const ws = new WebSocket(`ws://127.0.0.1:${port}`, { + headers: { "x-forwarded-for": "203.0.113.10" }, + }); + await new Promise((resolve) => ws.once("open", resolve)); + const res = await connectReq(ws); + expect(res.ok).toBe(false); + expect(res.error?.message ?? "").toContain("gateway auth required"); + ws.close(); + await server.close(); + if (prevToken === undefined) { + delete process.env.CLAWDBOT_GATEWAY_TOKEN; + } else { + process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; + } + }); + test("accepts device token auth for paired device", async () => { const { loadOrCreateDeviceIdentity } = await import("../infra/device-identity.js"); const { approveDevicePairing, getPairedDevice, listDevicePairing } = diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 5ef3f26e7..35265ce63 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -26,7 +26,7 @@ import type { ResolvedGatewayAuth } from "../../auth.js"; import { authorizeGatewayConnect } from "../../auth.js"; import { loadConfig } from "../../../config/config.js"; import { buildDeviceAuthPayload } from "../../device-auth.js"; -import { isLocalGatewayAddress, resolveGatewayClientIp } from "../../net.js"; +import { isLocalGatewayAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js"; import { resolveNodeCommandAllowlist } from "../../node-command-policy.js"; import { type ConnectParams, @@ -177,7 +177,24 @@ export function attachGatewayWsMessageHandler(params: { const configSnapshot = loadConfig(); const trustedProxies = configSnapshot.gateway?.trustedProxies ?? []; const clientIp = resolveGatewayClientIp({ remoteAddr, forwardedFor, realIp, trustedProxies }); - const isLocalClient = isLocalGatewayAddress(clientIp); + + // If proxy headers are present but the remote address isn't trusted, don't treat + // the connection as local. This prevents auth bypass when running behind a reverse + // proxy without proper configuration - the proxy's loopback connection would otherwise + // cause all external requests to be treated as trusted local clients. + const hasProxyHeaders = Boolean(forwardedFor || realIp); + const remoteIsTrustedProxy = isTrustedProxyAddress(remoteAddr, trustedProxies); + const hasUntrustedProxyHeaders = hasProxyHeaders && !remoteIsTrustedProxy; + const isLocalClient = !hasUntrustedProxyHeaders && isLocalGatewayAddress(clientIp); + const reportedClientIp = hasUntrustedProxyHeaders ? undefined : clientIp; + + if (hasUntrustedProxyHeaders) { + logWsControl.warn( + "Proxy headers detected from untrusted address. " + + "Connection will not be treated as local. " + + "Configure gateway.trustedProxies to restore local client detection behind your proxy.", + ); + } const isWebchatConnect = (p: ConnectParams | null | undefined) => isWebchatClient(p?.client); @@ -322,6 +339,31 @@ export function attachGatewayWsMessageHandler(params: { const isControlUi = connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI; const allowInsecureControlUi = isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true; + if (hasUntrustedProxyHeaders && resolvedAuth.mode === "none") { + setHandshakeState("failed"); + setCloseCause("proxy-auth-required", { + client: connectParams.client.id, + clientDisplayName: connectParams.client.displayName, + mode: connectParams.client.mode, + version: connectParams.client.version, + }); + send({ + type: "res", + id: frame.id, + ok: false, + error: errorShape( + ErrorCodes.INVALID_REQUEST, + "gateway auth required behind reverse proxy", + { + details: { + hint: "set gateway.auth or configure gateway.trustedProxies", + }, + }, + ), + }); + close(1008, "gateway auth required"); + return; + } if (!device) { const canSkipDevice = allowInsecureControlUi ? hasSharedAuth : hasTokenAuth; @@ -581,7 +623,7 @@ export function attachGatewayWsMessageHandler(params: { clientMode: connectParams.client.mode, role, scopes, - remoteIp: clientIp, + remoteIp: reportedClientIp, silent: isLocalClient, }); const context = buildRequestContext(); @@ -665,7 +707,7 @@ export function attachGatewayWsMessageHandler(params: { clientMode: connectParams.client.mode, role, scopes, - remoteIp: clientIp, + remoteIp: reportedClientIp, }); } } @@ -714,7 +756,7 @@ export function attachGatewayWsMessageHandler(params: { if (presenceKey) { upsertPresence(presenceKey, { host: connectParams.client.displayName ?? connectParams.client.id ?? os.hostname(), - ip: isLocalClient ? undefined : clientIp, + ip: isLocalClient ? undefined : reportedClientIp, version: connectParams.client.version, platform: connectParams.client.platform, deviceFamily: connectParams.client.deviceFamily, @@ -773,7 +815,9 @@ export function attachGatewayWsMessageHandler(params: { setHandshakeState("connected"); if (role === "node") { const context = buildRequestContext(); - const nodeSession = context.nodeRegistry.register(nextClient, { remoteIp: clientIp }); + const nodeSession = context.nodeRegistry.register(nextClient, { + remoteIp: reportedClientIp, + }); const instanceIdRaw = connectParams.client.instanceId; const instanceId = typeof instanceIdRaw === "string" ? instanceIdRaw.trim() : ""; const nodeIdsForPairing = new Set([nodeSession.nodeId]); diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index cd7df057e..0051b753f 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -53,6 +53,30 @@ describe("security audit", () => { ).toBe(true); }); + it("warns when loopback control UI lacks trusted proxies", async () => { + const cfg: ClawdbotConfig = { + gateway: { + bind: "loopback", + controlUi: { enabled: true }, + }, + }; + + const res = await runSecurityAudit({ + config: cfg, + includeFilesystem: false, + includeChannelSecurity: false, + }); + + expect(res.findings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + checkId: "gateway.trusted_proxies_missing", + severity: "warn", + }), + ]), + ); + }); + it("flags logging.redactSensitive=off", async () => { const cfg: ClawdbotConfig = { logging: { redactSensitive: "off" }, diff --git a/src/security/audit.ts b/src/security/audit.ts index 87e6e3397..db51d576f 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -207,6 +207,10 @@ function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback"; const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off"; const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode }); + const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false; + const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies) + ? cfg.gateway.trustedProxies + : []; if (bind !== "loopback" && auth.mode === "none") { findings.push({ @@ -218,6 +222,20 @@ function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding }); } + if (bind === "loopback" && controlUiEnabled && trustedProxies.length === 0) { + findings.push({ + checkId: "gateway.trusted_proxies_missing", + severity: "warn", + title: "Reverse proxy headers are not trusted", + detail: + "gateway.bind is loopback and gateway.trustedProxies is empty. " + + "If you expose the Control UI through a reverse proxy, configure trusted proxies " + + "so local-client checks cannot be spoofed.", + remediation: + "Set gateway.trustedProxies to your proxy IPs or keep the Control UI local-only.", + }); + } + if (tailscaleMode === "funnel") { findings.push({ checkId: "gateway.tailscale_funnel", From 885167dd58285a59c0db835c977949572cc26a6d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 15:16:40 +0000 Subject: [PATCH 2/4] fix: tighten security audit for loopback auth --- CHANGELOG.md | 6 ++---- docs/platforms/mac/release.md | 14 +++++++------- package.json | 2 +- src/security/audit.test.ts | 25 +++++++++++++++++++++++++ src/security/audit.ts | 12 ++++++++++++ 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d00e7319..4eda32488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,11 @@ Docs: https://docs.clawd.bot -## 2026.1.25 - -### Changes -- TBD. +## 2026.1.24-3 ### Fixes - Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. +- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. ## 2026.1.24-2 diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md index 7f37951cb..d2d267661 100644 --- a/docs/platforms/mac/release.md +++ b/docs/platforms/mac/release.md @@ -30,17 +30,17 @@ Notes: # From repo root; set release IDs so Sparkle feed is enabled. # APP_BUILD must be numeric + monotonic for Sparkle compare. BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.24-1 \ +APP_VERSION=2026.1.24-3 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-app.sh # Zip for distribution (includes resource forks for Sparkle delta support) -ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.24-1.zip +ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.24-3.zip # Optional: also build a styled DMG for humans (drag to /Applications) -scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24-1.dmg +scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24-3.dmg # Recommended: build + notarize/staple zip + DMG # First, create a keychain profile once: @@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24-1.dmg # --apple-id "" --team-id "" --password "" NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.24-1 \ +APP_VERSION=2026.1.24-3 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-dist.sh # Optional: ship dSYM alongside the release -ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.24-1.dSYM.zip +ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.24-3.dSYM.zip ``` ## Appcast entry Use the release note generator so Sparkle renders formatted HTML notes: ```bash -SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.24-1.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml +SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.24-3.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml ``` Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify -- Upload `Clawdbot-2026.1.24-1.zip` (and `Clawdbot-2026.1.24-1.dSYM.zip`) to the GitHub release for tag `v2026.1.24-1`. +- Upload `Clawdbot-2026.1.24-3.zip` (and `Clawdbot-2026.1.24-3.dSYM.zip`) to the GitHub release for tag `v2026.1.24-3`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`. - Sanity checks: - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. diff --git a/package.json b/package.json index 39e7b31d2..5d77e25d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawdbot", - "version": "2026.1.24-2", + "version": "2026.1.24-3", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 0051b753f..2ee7e27ee 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -77,6 +77,31 @@ describe("security audit", () => { ); }); + it("flags loopback control UI without auth as critical", async () => { + const cfg: ClawdbotConfig = { + gateway: { + bind: "loopback", + controlUi: { enabled: true }, + auth: { mode: "none" as any }, + }, + }; + + const res = await runSecurityAudit({ + config: cfg, + includeFilesystem: false, + includeChannelSecurity: false, + }); + + expect(res.findings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + checkId: "gateway.loopback_no_auth", + severity: "critical", + }), + ]), + ); + }); + it("flags logging.redactSensitive=off", async () => { const cfg: ClawdbotConfig = { logging: { redactSensitive: "off" }, diff --git a/src/security/audit.ts b/src/security/audit.ts index db51d576f..3695cf049 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -236,6 +236,18 @@ function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding }); } + if (bind === "loopback" && controlUiEnabled && auth.mode === "none") { + findings.push({ + checkId: "gateway.loopback_no_auth", + severity: "critical", + title: "Gateway auth disabled on loopback", + detail: + "gateway.bind is loopback and gateway.auth is disabled. " + + "If the Control UI is exposed through a reverse proxy, unauthenticated access is possible.", + remediation: "Set gateway.auth (token recommended) or keep the Control UI local-only.", + }); + } + if (tailscaleMode === "funnel") { findings.push({ checkId: "gateway.tailscale_funnel", From 4f82de3dccbab38b367504ce90afd86d70fe0c3e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 15:20:35 +0000 Subject: [PATCH 3/4] docs: add multi agent VPS FAQ --- docs/help/faq.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/help/faq.md b/docs/help/faq.md index 4949f9292..7a5ca6ce8 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -105,6 +105,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, - [How can my agent access my computer if the Gateway is hosted remotely?](#how-can-my-agent-access-my-computer-if-the-gateway-is-hosted-remotely) - [Tailscale is connected but I get no replies. What now?](#tailscale-is-connected-but-i-get-no-replies-what-now) - [Can two Clawdbots talk to each other (local + VPS)?](#can-two-clawdbots-talk-to-each-other-local-vps) + - [Do I need separate VPSes for multiple agents](#do-i-need-separate-vpses-for-multiple-agents) - [Is there a benefit to using a node on my personal laptop instead of SSH from a VPS?](#is-there-a-benefit-to-using-a-node-on-my-personal-laptop-instead-of-ssh-from-a-vps) - [Do nodes run a gateway service?](#do-nodes-run-a-gateway-service) - [Is there an API / RPC way to apply config?](#is-there-an-api-rpc-way-to-apply-config) @@ -1463,6 +1464,16 @@ allowlists, or a "do not reply to bot messages" rule). Docs: [Remote access](/gateway/remote), [Agent CLI](/cli/agent), [Agent send](/tools/agent-send). +### Do I need separate VPSes for multiple agents + +No. One Gateway can host multiple agents, each with its own workspace, model defaults, +and routing. That is the normal setup and it is much cheaper and simpler than running +one VPS per agent. + +Use separate VPSes only when you need hard isolation (security boundaries) or very +different configs that you do not want to share. Otherwise, keep one Gateway and +use multiple agents or sub-agents. + ### Is there a benefit to using a node on my personal laptop instead of SSH from a VPS Yes - nodes are the first‑class way to reach your laptop from a remote Gateway, and they From c8063bdcd83c14177aeff09dec329e3a8534f186 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 15:26:52 +0000 Subject: [PATCH 4/4] fix(ci): pin gradle and normalize gemini cli test paths --- .github/workflows/ci.yml | 2 + .../google-gemini-cli-auth/oauth.test.ts | 109 +++++++++++++++--- 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f944b361..fcd8e457c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -628,6 +628,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: 8.11.1 - name: Install Android SDK packages run: | diff --git a/extensions/google-gemini-cli-auth/oauth.test.ts b/extensions/google-gemini-cli-auth/oauth.test.ts index d776d9be3..a6ee8ee98 100644 --- a/extensions/google-gemini-cli-auth/oauth.test.ts +++ b/extensions/google-gemini-cli-auth/oauth.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import { join } from "node:path"; +import { join, parse } from "node:path"; // Mock fs module before importing the module under test const mockExistsSync = vi.fn(); @@ -19,7 +19,9 @@ vi.mock("node:fs", async (importOriginal) => { }); describe("extractGeminiCliCredentials", () => { - const normalizePath = (value: string) => value.replace(/\\/g, "/"); + const normalizePath = (value: string) => + value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase(); + const rootDir = parse(process.cwd()).root || "/"; const FAKE_CLIENT_ID = "123456789-abcdef.apps.googleusercontent.com"; const FAKE_CLIENT_SECRET = "GOCSPX-FakeSecretValue123"; const FAKE_OAUTH2_CONTENT = ` @@ -49,11 +51,33 @@ describe("extractGeminiCliCredentials", () => { }); it("extracts credentials from oauth2.js in known path", async () => { - const fakeBinDir = "/fake/bin"; + const fakeBinDir = join(rootDir, "fake", "bin"); const fakeGeminiPath = join(fakeBinDir, "gemini"); - const fakeResolvedPath = "/fake/lib/node_modules/@google/gemini-cli/dist/index.js"; - const fakeOauth2Path = - "/fake/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"; + const fakeResolvedPath = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "dist", + "index.js", + ); + const fakeOauth2Path = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "node_modules", + "@google", + "gemini-cli-core", + "dist", + "src", + "code_assist", + "oauth2.js", + ); process.env.PATH = fakeBinDir; @@ -77,9 +101,18 @@ describe("extractGeminiCliCredentials", () => { }); it("returns null when oauth2.js cannot be found", async () => { - const fakeBinDir = "/fake/bin"; + const fakeBinDir = join(rootDir, "fake", "bin"); const fakeGeminiPath = join(fakeBinDir, "gemini"); - const fakeResolvedPath = "/fake/lib/node_modules/@google/gemini-cli/dist/index.js"; + const fakeResolvedPath = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "dist", + "index.js", + ); process.env.PATH = fakeBinDir; @@ -95,11 +128,33 @@ describe("extractGeminiCliCredentials", () => { }); it("returns null when oauth2.js lacks credentials", async () => { - const fakeBinDir = "/fake/bin"; + const fakeBinDir = join(rootDir, "fake", "bin"); const fakeGeminiPath = join(fakeBinDir, "gemini"); - const fakeResolvedPath = "/fake/lib/node_modules/@google/gemini-cli/dist/index.js"; - const fakeOauth2Path = - "/fake/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"; + const fakeResolvedPath = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "dist", + "index.js", + ); + const fakeOauth2Path = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "node_modules", + "@google", + "gemini-cli-core", + "dist", + "src", + "code_assist", + "oauth2.js", + ); process.env.PATH = fakeBinDir; @@ -118,11 +173,33 @@ describe("extractGeminiCliCredentials", () => { }); it("caches credentials after first extraction", async () => { - const fakeBinDir = "/fake/bin"; + const fakeBinDir = join(rootDir, "fake", "bin"); const fakeGeminiPath = join(fakeBinDir, "gemini"); - const fakeResolvedPath = "/fake/lib/node_modules/@google/gemini-cli/dist/index.js"; - const fakeOauth2Path = - "/fake/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"; + const fakeResolvedPath = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "dist", + "index.js", + ); + const fakeOauth2Path = join( + rootDir, + "fake", + "lib", + "node_modules", + "@google", + "gemini-cli", + "node_modules", + "@google", + "gemini-cli-core", + "dist", + "src", + "code_assist", + "oauth2.js", + ); process.env.PATH = fakeBinDir;