Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/opencode/src/cli/offline.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +10 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Classify the complete IPv4 loopback range.

Line 11 allows 127.0.0.1 only. 127.0.0.2 and other addresses in 127.0.0.0/8 are loopback addresses, but this guard rejects them. Allow the full range and add a test for 127.0.0.2.

Proposed fix
 export function local(url: URL) {
-  return LOCAL.has(url.hostname)
+  return LOCAL.has(url.hostname) || url.hostname.startsWith("127.")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/cli/offline.ts` around lines 10 - 12, Update local to
classify every IPv4 address in the 127.0.0.0/8 loopback range as local, not only
the hostname currently covered by LOCAL; add a test verifying that 127.0.0.2 is
accepted while preserving existing classifications.


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
Comment on lines +20 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
cat -n packages/opencode/src/cli/offline.ts
rg -n -C 4 'globalThis\.fetch|local\(|offline' packages/opencode --glob '*.ts' --glob '*.tsx'

Repository: bolt-builder/bolt-cli

Length of output: 43988


🏁 Script executed:

#!/bin/bash
set -eu

cat -n packages/opencode/test/cli/offline.test.ts
command -v bun || true
bun --version 2>/dev/null || true

if command -v bun >/dev/null 2>&1; then
  bun - <<'JS'
const blob = new Blob(["blob-body"], { type: "text/plain" })
const blobURL = URL.createObjectURL(blob)

for (const url of ["file:///etc/hosts", "data:text/plain,hello", blobURL, "s3://bucket/key"]) {
  try {
    const response = await fetch(url)
    console.log(JSON.stringify({ url, status: response.status, text: (await response.text()).slice(0, 40) }))
  } catch (error) {
    console.log(JSON.stringify({ url, error: String(error) }))
  }
}

URL.revokeObjectURL(blobURL)
JS
fi

Repository: bolt-builder/bolt-cli

Length of output: 2068


🌐 Web query:

site:bun.sh/docs fetch file:// data: blob: URL Bun fetch supported protocols

💡 Result:

Bun's fetch implementation supports file://, data:, and blob: URL protocols in addition to standard HTTP and HTTPS requests [1]. 1. file:// URLs: You can use the fetch API to retrieve local files from the filesystem [1]. 2. data: URLs: Bun supports the data: URL scheme, allowing you to fetch data encoded directly within the URL string [1]. 3. blob: URLs: Bun supports fetching blobs, which are commonly created using URL.createObjectURL [1]. These are frequently used to retrieve data from in-memory objects or as a source for Web Workers [1][2]. Additionally, Bun's fetch supports the s3:// protocol for interacting with Amazon S3 buckets [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

url='https://bun.sh/docs/runtime/networking/fetch'
if command -v curl >/dev/null 2>&1; then
  curl -LfsS "$url" | rg -in -C 3 'file:|data:|blob:|protocol|URL' | head -120 || true
fi

Repository: bolt-builder/bolt-cli

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -eu

curl -LfsS https://bun.sh/docs/runtime/networking/fetch |
  rg -in -C 3 'file:|data:|blob:|protocol|URL' | head -120

Repository: bolt-builder/bolt-cli

Length of output: 50378


Permit local Fetch resource schemes explicitly.

local() checks only url.hostname, so file:, data:, and blob: requests fail in offline mode. Permit these schemes by protocol, keep s3: blocked, and add coverage for all three schemes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/cli/offline.ts` around lines 20 - 27, Update the fetch
guard in enable and its local URL validation to explicitly allow file:, data:,
and blob: protocols, while continuing to reject s3: and other non-local
requests. Add coverage verifying offline fetch permits each of the three
resource schemes and keeps s3: blocked.

globalThis.fetch = guard
return () => {
globalThis.fetch = original
}
}
10 changes: 10 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()

Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/test/cli/offline.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +24 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

file=$(git ls-files 'packages/opencode/test/cli/offline.test.ts')
printf '%s\n' "$file"
cat -n "$file"
printf '\n--- package/test configuration references ---\n'
rg -n --glob 'package.json' --glob 'bunfig.toml' --glob 'vitest.config.*' --glob 'jest.config.*' \
  '"test"|bun test|vitest|jest' packages/opencode . 2>/dev/null | head -120

Repository: bolt-builder/bolt-cli

Length of output: 4503


🏁 Script executed:

#!/bin/sh
set -eu

printf '%s\n' '--- Offline implementation ---'
file=$(git ls-files 'packages/opencode/src/cli/offline.ts')
printf '%s\n' "$file"
cat -n "$file"

printf '%s\n' '--- Bun version declarations ---'
rg -n --hidden --glob 'package.json' --glob 'bun.lock' --glob 'bun.lockb' \
  'bun|`@types/bun`' packages/opencode package.json bun.lock bun.lockb 2>/dev/null | head -160

Repository: bolt-builder/bolt-cli

Length of output: 19918


🌐 Web query:

Bun test expect rejects await promise matcher documentation

💡 Result:

In the Bun test runner (bun:test), the.rejects matcher is used to assert that a promise or a function returning a promise rejects [1][2][3]. Usage with Promises and Functions: You can use.rejects to verify that a promise rejects with a specific value or to check if an asynchronous function throws an error using.toThrow [1][4]. Example with a promise: expect(Promise.reject("error")).rejects.toBe("error"); Example with an async function (using.toThrow): await expect(async => { await fetchUser("invalid-id"); }).rejects.toThrow("User not found"); Key Details: - Async execution: When testing functions that throw, ensure you await the expect call [1][4]. - Input types: The.rejects matcher accepts both promise objects and functions that return a promise (or thenable) [5]. - Asymmetric Matchers: Bun also provides asymmetric matchers like expect.rejectsTo, which can be used within.toEqual assertions for more complex matching needs [3][6]. - Jest Compatibility: Bun's test runner is designed for Jest compatibility, so these patterns will be familiar if you have used Jest previously [1][7]. Documentation for these matchers can be found in the official Bun API reference under the bun:test module [8][9].

Citations:


Await the rejected-fetch assertion.

The asynchronous matcher is not awaited, so the test can finish before it validates the rejection.

Proposed fix
-      expect(fetch("https://example.com")).rejects.toThrow("offline mode")
+      await expect(fetch("https://example.com")).rejects.toThrow("offline mode")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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("blocks remote requests immediately", async () => {
const start = performance.now()
await expect(fetch("https://example.com")).rejects.toThrow("offline mode")
expect(performance.now() - start).toBeLessThan(100)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/cli/offline.test.ts` around lines 24 - 27, Await the
rejected-fetch assertion in the “blocks remote requests immediately” test so the
test waits for and validates the asynchronous rejection. Keep the existing
offline-mode error expectation and timing assertion unchanged.

})

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)
})
})
})
Loading