Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0b03c73
fix(security): backport upstream v1.18 hardening for debug config, br…
anandgupta42 Oct 15, 2026
c4a03d0
fix(security): refuse non-http(s) Snowflake SSO URLs; record core `op…
anandgupta42 Oct 15, 2026
4d7e7ce
fix(security): redact credentials in any-scheme URLs and argv in debu…
anandgupta42 Oct 15, 2026
c432a17
fix(security): mask every URL query and fragment value in debug config
anandgupta42 Oct 15, 2026
b1996f2
fix(security): redact every value nested under a secret-named key
anandgupta42 Oct 15, 2026
92aee8d
fix(security): mask MCP environment values and argv arguments in debu…
anandgupta42 Oct 15, 2026
2abadef
fix(markers): do not nest backport markers inside existing marker blocks
anandgupta42 Oct 15, 2026
20e6593
fix(security): keep only the executable in MCP argv; drop server URL …
anandgupta42 Oct 15, 2026
e12b3d4
fix(security): close `debug config` redaction gaps from local review
anandgupta42 Oct 16, 2026
c2d2bc3
fix(security): print only the origin of remote server URLs in `debug …
anandgupta42 Oct 16, 2026
d8b84bc
fix(security): keep redacting scheme-prefixed URLs that contain spaces
anandgupta42 Oct 16, 2026
7743682
fix(security): treat any scheme-prefixed value as a URL in `debug con…
anandgupta42 Oct 16, 2026
55ccdc6
fix(security): print only the origin of provider endpoint URLs in `de…
anandgupta42 Oct 16, 2026
f9ab4d0
fix(security): keep unparseable endpoint values fully masked in `debu…
anandgupta42 Oct 16, 2026
07816fa
fix(security): apply endpoint masking only to address-like values
anandgupta42 Oct 16, 2026
14e5145
fix(security): treat backslash and colon as address characters for en…
anandgupta42 Oct 16, 2026
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
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"open": "10.1.2",
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"semver": "^7.6.3",
"turndown": "7.2.0",
"venice-ai-sdk-provider": "2.0.2",
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import open from "open"

export function openUrl(input: string) {
const url = URL.canParse(input) ? new URL(input) : undefined
if (!url || (url.protocol !== "http:" && url.protocol !== "https:"))
return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${input}`))
Comment on lines +5 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The rejection message echoes the full raw input, which can include userinfo, query tokens, or OAuth state. Include only the parsed protocol, or a fixed phrase, so the error does not reproduce the URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/open.ts, line 5:

<comment>The rejection message echoes the full raw `input`, which can include userinfo, query tokens, or OAuth state. Include only the parsed protocol, or a fixed phrase, so the error does not reproduce the URL.</comment>

<file context>
@@ -0,0 +1,8 @@
+
+export function openUrl(input: string) {
+  const url = URL.canParse(input) ? new URL(input) : undefined
+  if (!url || (url.protocol !== "http:" && url.protocol !== "https:"))
+    return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${input}`))
+  return open(url.href)
</file context>
Suggested change
if (!url || (url.protocol !== "http:" && url.protocol !== "https:"))
return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${input}`))
if (!url || (url.protocol !== "http:" && url.protocol !== "https:"))
return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${url ? url.protocol : "invalid URL"}`))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving as is for the same reason: core/src/open.ts matches upstream exactly. Refused URLs are non-http(s) schemes, not credential-bearing web URLs.

return open(url.href)
}
47 changes: 47 additions & 0 deletions packages/core/test/open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, spyOn, test } from "bun:test"
import type { ChildProcess } from "node:child_process"
import { EventEmitter } from "node:events"
import * as browser from "open"
import { openUrl } from "@opencode-ai/core/open"

describe("openUrl", () => {
test("opens normalized http(s) URLs and preserves the subprocess promise", async () => {
const subprocess = new EventEmitter() as ChildProcess
const promise = Promise.resolve(subprocess)
const opener = spyOn(browser, "default").mockReturnValue(promise)
try {
for (const input of ["HTTP://Example.COM:80/path", "https://example.com/a b"]) {
const result = openUrl(input)
expect(result).toBe(promise)
expect(await result).toBe(subprocess)
expect(opener).toHaveBeenLastCalledWith(new URL(input).href)
}
} finally {
opener.mockRestore()
}
})

test("rejects values that are not URLs", async () => {
const opener = spyOn(browser, "default").mockResolvedValue(new EventEmitter() as ChildProcess)
try {
await expect(openUrl("not a url")).rejects.toThrow("Only http and https links")
await expect(openUrl("")).rejects.toThrow("Only http and https links")
expect(opener).not.toHaveBeenCalled()
} finally {
opener.mockRestore()
}
})

test("rejects non-http schemes", async () => {
const opener = spyOn(browser, "default").mockResolvedValue(new EventEmitter() as ChildProcess)
try {
await expect(openUrl("file:///etc/hosts")).rejects.toThrow("Only http and https links")
await expect(openUrl("javascript:alert(1)")).rejects.toThrow("Only http and https links")
await expect(openUrl("ms-msdt:/id PCWDiagnostic")).rejects.toThrow("Only http and https links")
await expect(openUrl("\\\\server\\share\\file.html")).rejects.toThrow("Only http and https links")
expect(opener).not.toHaveBeenCalled()
} finally {
opener.mockRestore()
}
})
})
8 changes: 8 additions & 0 deletions packages/drivers/src/sign-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export function setBrowserOpenerForTests(fn: (url: string) => void): () => void
}

function launchBrowser(url: string): void {
// The server/IdP supplies this URL. Refuse non-web protocol handlers; the notice still carries it for manual copy.
try {
const protocol = new URL(url).protocol
if (protocol !== "http:" && protocol !== "https:") return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: A refused URL launches nothing, but the later Snowflake timeout error still says 'A sign-in page was opened in the default browser' and asks the user to confirm a tab opened. Make that message conditional on whether a launch happened, or at least log the refusal so the failure is diagnosable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/drivers/src/sign-in.ts, line 63:

<comment>A refused URL launches nothing, but the later Snowflake timeout error still says 'A sign-in page was opened in the default browser' and asks the user to confirm a tab opened. Make that message conditional on whether a launch happened, or at least log the refusal so the failure is diagnosable.</comment>

<file context>
@@ -57,6 +57,14 @@ export function setBrowserOpenerForTests(fn: (url: string) => void): () => void
+  // The server/IdP supplies this URL. Refuse non-web protocol handlers; the notice still carries it for manual copy.
+  try {
+    const protocol = new URL(url).protocol
+    if (protocol !== "http:" && protocol !== "https:") return
+  } catch {
+    return
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point, but P3 and outside this backport. The URL is still in the sign-in notice for manual copy, so the user can recover. I'm noting it for a follow-up on the driver's timeout copy.

} catch {
return
}

const [command, args] =
process.platform === "win32"
? // No shell, so `&` in the SAML query needs no escaping (unlike `cmd /c start`).
Expand Down
56 changes: 52 additions & 4 deletions packages/drivers/test/snowflake-sign-in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
* sign-in that never came back is an actionable error, not the SDK's raw timeout.
* Non-interactive logins get an upper bound instead of the SDK's 300 s retry floor.
*/
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
import * as childProcess from "child_process"
import { EventEmitter } from "node:events"
import { connect, SSO_WAIT_MS } from "../src/snowflake"
import { onBrowserSignIn, redactSignInUrl, setBrowserOpenerForTests, type BrowserSignInNotice } from "../src/sign-in"
import {
onBrowserSignIn,
openInBrowser,
redactSignInUrl,
setBrowserOpenerForTests,
type BrowserSignInNotice,
} from "../src/sign-in"

const SIGN_IN_URL = "https://idp.example.com/sso/saml?SAMLRequest=abc&RelayState=xyz"

/** snowflake-sdk stand-in: `browser` decides what the SSO sign-in does; `connect` what a plain login does. */
function fakeSdk(opts: { browser?: "complete" | "timeout" | "no-prompt"; connect?: "ok" | "never" }) {
function fakeSdk(opts: { browser?: "complete" | "timeout" | "no-prompt"; connect?: "ok" | "never"; url?: string }) {
const created: Array<{ options: any; destroyed: boolean }> = []
const sdk = {
configure() {},
Expand All @@ -20,7 +28,7 @@ function fakeSdk(opts: { browser?: "complete" | "timeout" | "no-prompt"; connect
created.push(entry)
return {
async connectAsync(cb: (err: Error | null) => void) {
if (opts.browser !== "no-prompt") options.openExternalBrowserCallback?.(SIGN_IN_URL)
if (opts.browser !== "no-prompt") options.openExternalBrowserCallback?.(opts.url ?? SIGN_IN_URL)
setTimeout(() => {
if (opts.browser === "timeout") {
cb(new Error("Error while getting SAML token: Browser action timed out after 120000 ms."))
Expand Down Expand Up @@ -63,6 +71,46 @@ afterEach(() => {
})

describe("Snowflake browser sign-in", () => {
test.each(["file:///etc/hosts", "javascript:alert(1)", "ms-msdt:/id PCWDiagnostic", "not a url", "", "https://"])(
"refuses to launch %j but still announces it for manual copy",
async (url) => {
restoreOpener()
const spawn = spyOn(childProcess, "spawn").mockImplementation(() => {
throw new Error("Unexpected browser launch")
})
try {
const { sdk } = fakeSdk({ browser: "complete", url })
const c = await connect(sso, sdk)
await c.connect()

expect(spawn).not.toHaveBeenCalled()
expect(notices).toEqual([
{ warehouse: "snowflake", account: "acme-xy123", phase: "waiting", url, timeoutMs: SSO_WAIT_MS },
{ warehouse: "snowflake", account: "acme-xy123", phase: "completed" },
])
} finally {
spawn.mockRestore()
}
},
)

test.each(["http://idp.example.com/sso", SIGN_IN_URL, "HTTPS://idp.example.com/sso"])(
"launches the browser for %s",
(url) => {
restoreOpener()
const child = Object.assign(new EventEmitter(), { unref() {} }) as childProcess.ChildProcess
const spawn = spyOn(childProcess, "spawn").mockReturnValue(child)
try {
openInBrowser(url)

expect(spawn).toHaveBeenCalledTimes(1)
expect(spawn.mock.calls[0][1]).toContain(url)
} finally {
spawn.mockRestore()
}
},
)

test("the sign-in page is announced with its time limit, opened, and its completion reported", async () => {
const { sdk, created } = fakeSdk({ browser: "complete" })
const c = await connect(sso, sdk)
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/altimate/plugin/altimate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { createServer } from "http"
import { randomBytes } from "crypto"
import open from "open"
import { openUrl } from "@opencode-ai/core/open"
import { AltimateApi } from "../api/client"
// altimate_change — onboarding telemetry for the gateway sign-in funnel
import * as OnboardingTelemetry from "../telemetry/onboarding"
Expand Down Expand Up @@ -444,7 +444,7 @@ export async function AltimateAuthPlugin(_input: PluginInput): Promise<Hooks> {
// to the log file at the top of packages/opencode/src/cli/tui/
// worker.ts — so that write reached the log, never the terminal.
// Removed to stop leaking state-bearing authorize URLs into logs.
await open(authorizeUrl).catch(() => undefined)
await openUrl(authorizeUrl).catch(() => undefined)

// altimate_change start — onboarding funnel: gateway sign-in started.
// Spec name is `gateway_device_code_issued`; this flow is a browser loopback OAuth
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/altimate/workspace/browser-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// See docs `workspace-browser-handoff-plan-v3.md` for the design context.
import { createServer, type Server } from "http"
import { randomBytes } from "crypto"
import open from "open"
import { openUrl } from "@opencode-ai/core/open"

import { AltimateApi } from "@/altimate/api/client"
import { Log } from "@/altimate/util/log"
Expand Down Expand Up @@ -424,7 +424,7 @@ export interface OpenBrowserHandoffInput {
* error is expressed as ``{ok: false, reason}`` so the caller can toast the
* appropriate message. */
export async function openWorkspaceBrowserHandoff(input: OpenBrowserHandoffInput): Promise<HandoffResult> {
return runHandoffWithOpener(input, (url) => open(url).then(() => undefined))
return runHandoffWithOpener(input, (url) => openUrl(url).then(() => undefined))
}

/** Same as ``openWorkspaceBrowserHandoff`` but takes the browser-open callback
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/cli/cmd/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { Account } from "@/account/account"
import { AccountID, OrgID, PollExpired, type PollResult, type AccountError } from "@/account/schema"
import { effectCmd } from "../effect-cmd"
import * as Prompt from "../effect/prompt"
import open from "open"
// altimate_change start — upstream_fix: backport 29f07e0c69 restrict browser URLs to http(s)
import { openUrl } from "@opencode-ai/core/open"
// altimate_change end

const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() => undefined))
// altimate_change start — upstream_fix: backport 29f07e0c69 restrict browser URLs to http(s)
const openBrowser = (url: string) => Effect.promise(() => openUrl(url).catch(() => undefined))
// altimate_change end

const println = (msg: string) => Effect.sync(() => UI.println(msg))

Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/cli/cmd/debug/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { EOL } from "os"
import { Effect } from "effect"
import { effectCmd } from "../../effect-cmd"
// altimate_change start — upstream_fix: backport 82d4c89031 redact debug configuration credentials
import { redactConfig } from "./redact"
// altimate_change end

export const ConfigCommand = effectCmd({
command: "config",
Expand All @@ -9,6 +12,8 @@ export const ConfigCommand = effectCmd({
handler: Effect.fn("Cli.debug.config")(function* () {
const { Config } = yield* Effect.promise(() => import("@/config/config"))
const config = yield* Config.Service.use((cfg) => cfg.get())
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
// altimate_change start — upstream_fix: backport 82d4c89031 redact debug configuration credentials
process.stdout.write(JSON.stringify(redactConfig(config), null, 2) + EOL)
// altimate_change end
}),
})
92 changes: 92 additions & 0 deletions packages/opencode/src/cli/cmd/debug/redact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Fork-only file (not in v1.17.9): extends upstream 82d4c89031.
// Redact only the debug output; the resolved configuration must remain usable by providers.
const secretName =
/(?:api.?key|access.?key|secret|password|passwd|token$|authorization$|cookie$|credential|private.?key|bearer|passphrase|(?:^|[_-])pwd$|(?:^|[_-])auth(?:[_-]|$)|dsn$|connection.?string)/i

function redactURL(value: string): string {
const trimmed = value.trim()
// Prose (prompts, descriptions) is not a URL even when it mentions one. A value that starts with
// `//` or a scheme directly followed by a non-space (`https:/x`, `jdbc:...`) is still a URL, even
// with spaces later; `Review: check {file}` stays prose.
if (/\s/.test(trimmed) && !/^(?:\/\/|[a-z][a-z\d+.-]*:\S)/i.test(trimmed)) return value
// Also covers malformed authorities and opaque schemes such as jdbc:postgresql://.
const authority = /^(?:[^/?#]*:)?\/\/([^/?#]*)/.exec(trimmed)?.[1]
if (authority?.includes("@")) return "***"
// Fail closed for URL-like strings that cannot be safely parsed, regardless of scheme.
if (!URL.canParse(trimmed)) {
return authority !== undefined || /^[a-z][a-z\d+.-]*:/i.test(trimmed) ? "***" : value
}
const url = new URL(trimmed)
if (url.username || url.password) return "***"

const redactParams = (params: string) =>
params
.split("&")
.map((param) => (param ? `${param.split("=", 1)[0]}=***` : param))
.join("&")

// Preserve the original URL spelling and fragments that are only anchors.
return value
.replace(/^([^?#]*\?)([^#]*)/, (_, prefix, params) => prefix + redactParams(params))
.replace(/#(.*)/s, (fragment, params) => (params.includes("=") ? `#${redactParams(params)}` : fragment))
}

// Endpoint URLs (MCP `url`, provider `api`/`baseURL`) can carry credentials in the path
// (`/mcp/u/<token>/...`, `/v1/sk-...`), so print only the origin.
const endpointKey = /^(?:url|baseURL|api|endpoint)$/i

function redactServerURL(value: string): string {
const trimmed = value.trim()
// Explicit endpoint fields fail closed: a schemeless value can still carry a path token.
if (!URL.canParse(trimmed)) return "***"
const url = new URL(trimmed)
if (url.username || url.password || url.origin === "null") return "***"
const rest = url.pathname.replace(/^\/$/, "") + url.search + url.hash
return rest ? `${url.origin}/***` : url.origin
}

// Free-form secret carriers: every environment value is masked, and argv keeps only the
// executable (`env TOKEN=x`, `--header "Authorization: ..."`, `-pSecret`, JDBC strings).
const environmentKey = /^(?:environment|env)$/i
const argvKey = /^(?:command|headersCommand|args)$/i

function redactArgv(argv: unknown[]): unknown[] {
return argv.map((item, index) =>
typeof item !== "string" || index === 0 ? item : "***",
)
}

export function redactConfig(value: unknown, headers = false): unknown {
if (typeof value === "string") return headers ? "***" : redactURL(value)
if (Array.isArray(value))
return value.map((item, index) => {
if (typeof item === "string") {
const flag = /^--?([^=]+)=/.exec(item)
const previous = value[index - 1]
const bare = typeof previous === "string" ? /^--?([^=]+)$/.exec(previous) : null
if ((flag && secretName.test(flag[1])) || (bare && secretName.test(bare[1]))) return "***"
Comment thread
anandgupta42 marked this conversation as resolved.
}
return redactConfig(item, headers)
})
if (value === null || typeof value !== "object") return value

return Object.fromEntries(
Object.entries(value).map(([key, item]) => {
if (typeof item === "string" && (headers || secretName.test(key))) return [key, "***"]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// Only address-like values (`.`, `/`, `\`, `:`): a permission entry for a tool named `api` holds `allow`/`deny`.
if (typeof item === "string" && endpointKey.test(key) && /[./\\:]/.test(item) && !headers)
return [key, redactServerURL(item)]
if (Array.isArray(item) && argvKey.test(key) && !headers) return [key, redactArgv(item)]
Comment thread
anandgupta42 marked this conversation as resolved.
// headersCommand is a record of header name -> argv.
if (item && typeof item === "object" && /^headersCommand$/i.test(key) && !headers)
return [
key,
Object.fromEntries(
Object.entries(item).map(([name, argv]) => [name, Array.isArray(argv) ? redactArgv(argv) : "***"]),
),
]
const secretScope = headers || /^headers$/i.test(key) || environmentKey.test(key) || secretName.test(key)
return [key, redactConfig(item, secretScope)]
Comment on lines +88 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve provider environment-variable names

When a provider declares credential lookup variables via the schema-valid env: string[] field (for example, env: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"]), this key-wide secret scope replaces every variable name with "***". These entries are names rather than resolved environment values, so debug config can no longer show which variables a provider is configured to inspect; distinguish the provider array from value-bearing MCP/LSP environment maps before applying blanket masking.

Useful? React with 👍 / 👎.

}),
)
}
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { cmd } from "./cmd"
import { UI } from "../ui"
import * as prompts from "@clack/prompts"
import open from "open"
import { openUrl } from "@opencode-ai/core/open"
import { AltimateApi } from "@/altimate/api/client"
import {
WorkspaceApi,
Expand Down Expand Up @@ -735,7 +735,7 @@ export async function createThenBindOrRebind(
if (opts.openBrowser === false) {
// printed above; nothing to open
} else if (isSafeHttpUrl(manageUrl)) {
await open(manageUrl).catch(() => undefined)
await openUrl(manageUrl).catch(() => undefined)
} else {
prompts.log.warn(`Skipped auto-open: manage_url is not an http/https URL.`)
}
Expand Down
12 changes: 9 additions & 3 deletions packages/opencode/src/cli/cmd/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { AppRuntime } from "../../effect/app-runtime"
import { Flag } from "../../flag/flag"
import open from "open"
// altimate_change start — upstream_fix: backport 29f07e0c69 restrict browser URLs to http(s)
import { openUrl } from "@opencode-ai/core/open"
// altimate_change end
import { networkInterfaces } from "os"
// altimate_change start — auto-register Altimate Base before the server (and its provider state) starts
import { FreeTier } from "../../altimate/free/client"
Expand Down Expand Up @@ -81,11 +83,15 @@ export const WebCommand = cmd({
}

// Open localhost in browser
open(localhostUrl.toString()).catch(() => {})
// altimate_change start — upstream_fix: backport 29f07e0c69 restrict browser URLs to http(s)
openUrl(localhostUrl.toString()).catch(() => {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Redundant .toString() on localhostUrl

localhostUrl is already a string (const localhostUrl = \http://localhost:${server.port}\`` on line 62), so .toString() can be dropped.

Suggested change
openUrl(localhostUrl.toString()).catch(() => {})
openUrl(localhostUrl).catch(() => {})

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving as is: harmless, and it keeps the line identical to upstream's version of this call site.

// altimate_change end
} else {
const displayUrl = server.url.toString()
UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl)
open(displayUrl).catch(() => {})
// altimate_change start — upstream_fix: backport 29f07e0c69 restrict browser URLs to http(s)
openUrl(displayUrl).catch(() => {})
// altimate_change end
}

await new Promise(() => {})
Expand Down
Loading
Loading