From 5cc24181edeed0f8d308d080371048caba134991 Mon Sep 17 00:00:00 2001 From: DevFlex-AI Date: Fri, 7 Aug 2026 19:11:40 +0000 Subject: [PATCH 1/3] feat(bolt): add offline fetch guard --- packages/opencode/src/cli/offline.ts | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/opencode/src/cli/offline.ts diff --git a/packages/opencode/src/cli/offline.ts b/packages/opencode/src/cli/offline.ts new file mode 100644 index 000000000000..8c4277ae7989 --- /dev/null +++ b/packages/opencode/src/cli/offline.ts @@ -0,0 +1,32 @@ +export * as Offline from "./offline" + +// Offline mode (--offline): instead of letting network calls hang until their +// own timeouts, every non-local fetch fails immediately with a clear message. +// Loopback and in-process destinations stay allowed so local servers, the +// daemon, and the internal fetch bridge keep working. + +const LOCAL = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0", "opencode.internal"]) + +export function local(url: URL) { + return LOCAL.has(url.hostname) +} + +export function reject(url: URL) { + return new Error( + `offline mode: refusing network request to ${url.host} (bolt was started with --offline; remove the flag to allow network access)`, + ) +} + +export function enable() { + const original = globalThis.fetch + const guard = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init) + const url = new URL(request.url) + if (local(url)) return original(input as never, init) + throw reject(url) + }) as typeof globalThis.fetch + globalThis.fetch = guard + return () => { + globalThis.fetch = original + } +} From b0b5999e155ea97d417986ccbaddfd0de0ebb3d6 Mon Sep 17 00:00:00 2001 From: DevFlex-AI Date: Fri, 7 Aug 2026 19:11:40 +0000 Subject: [PATCH 2/3] feat(bolt): add global offline flag --- packages/opencode/src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 9e938fe259ee..9ec05a02dd15 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -135,6 +135,10 @@ const cli = yargs(args) describe: "print debug logs to stderr (implies --print-logs and --log-level DEBUG)", type: "boolean", }) + .option("offline", { + describe: "fail fast on network access instead of hanging", + type: "boolean", + }) .middleware(async (opts) => { if (opts.quiet) UI.setQuiet(true) if (opts.verbose) { @@ -147,6 +151,12 @@ const cli = yargs(args) if (opts.pure) { process.env.OPENCODE_PURE = "1" } + if (opts.offline || process.env.OPENCODE_OFFLINE) { + // The env var propagates offline mode to spawned bolt subprocesses. + process.env.OPENCODE_OFFLINE = "1" + const { Offline } = await import("./cli/offline") + Offline.enable() + } Heap.start() From df00357edf99c3173be54b33fdaac0fe1f36c717 Mon Sep 17 00:00:00 2001 From: DevFlex-AI Date: Fri, 7 Aug 2026 19:11:40 +0000 Subject: [PATCH 3/3] test(bolt): cover offline mode guard --- packages/opencode/test/cli/offline.test.ts | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/opencode/test/cli/offline.test.ts diff --git a/packages/opencode/test/cli/offline.test.ts b/packages/opencode/test/cli/offline.test.ts new file mode 100644 index 000000000000..90f2b23b98d7 --- /dev/null +++ b/packages/opencode/test/cli/offline.test.ts @@ -0,0 +1,42 @@ +import { afterAll, describe, expect, test } from "bun:test" +import { Offline } from "../../src/cli/offline" + +describe("offline mode", () => { + test("classifies local destinations", () => { + expect(Offline.local(new URL("http://localhost:4096/health"))).toBe(true) + expect(Offline.local(new URL("http://127.0.0.1:3000"))).toBe(true) + expect(Offline.local(new URL("http://opencode.internal/session"))).toBe(true) + expect(Offline.local(new URL("http://[::1]:8080"))).toBe(true) + expect(Offline.local(new URL("https://api.openai.com/v1/responses"))).toBe(false) + expect(Offline.local(new URL("https://models.opencode.ai"))).toBe(false) + }) + + test("rejection message names the host and the flag", () => { + const error = Offline.reject(new URL("https://api.anthropic.com/v1/messages")) + expect(error.message).toContain("api.anthropic.com") + expect(error.message).toContain("--offline") + }) + + describe("fetch guard", () => { + const disable = Offline.enable() + afterAll(() => disable()) + + test("blocks remote requests immediately", async () => { + const start = performance.now() + expect(fetch("https://example.com")).rejects.toThrow("offline mode") + expect(performance.now() - start).toBeLessThan(100) + }) + + test("allows loopback requests", async () => { + const server = Bun.serve({ + port: 0, + fetch() { + return Response.json({ ok: true }) + }, + }) + const response = await fetch(`http://127.0.0.1:${server.port}`) + expect(await response.json()).toEqual({ ok: true }) + await server.stop(true) + }) + }) +})