-
Notifications
You must be signed in to change notification settings - Fork 2
feat(bolt): add --offline flag that fails fast without network #393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
fiRepository: bolt-builder/bolt-cli Length of output: 2068 🌐 Web query:
💡 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
fiRepository: 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 -120Repository: bolt-builder/bolt-cli Length of output: 50378 Permit local Fetch resource schemes explicitly.
🤖 Prompt for AI Agents |
||
| globalThis.fetch = guard | ||
| return () => { | ||
| globalThis.fetch = original | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -120Repository: 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 -160Repository: bolt-builder/bolt-cli Length of output: 19918 🌐 Web query:
💡 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| }) | ||||||||||||||||||
|
|
||||||||||||||||||
| 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) | ||||||||||||||||||
| }) | ||||||||||||||||||
| }) | ||||||||||||||||||
| }) | ||||||||||||||||||
There was a problem hiding this comment.
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.1only.127.0.0.2and other addresses in127.0.0.0/8are loopback addresses, but this guard rejects them. Allow the full range and add a test for127.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