Skip to content
Open
9 changes: 9 additions & 0 deletions packages/opencode/src/cli/cmd/tui/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cmd } from "../cmd"
import { UI } from "@/cli/ui"
import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { getAuthorizationHeader } from "@/flag/auth"

export const AttachCommand = cmd({
command: "attach <url>",
Expand Down Expand Up @@ -35,6 +36,11 @@ export const AttachCommand = cmd({
alias: ["p"],
type: "string",
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
})
.option("username", {
alias: ["u"],
type: "string",
describe: "basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')",
}),
handler: async (args) => {
const unguard = win32InstallCtrlCGuard()
Expand All @@ -58,6 +64,9 @@ export const AttachCommand = cmd({
}
})()
const headers = (() => {
const Authorization = getAuthorizationHeader({ passwordFromCli: args.password })
if (!Authorization) return undefined
return { Authorization }
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import type { Event } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { getAuthorizationHeader } from "@/flag/auth"

declare global {
const OPENCODE_WORKER_PATH: string
Expand All @@ -21,6 +22,7 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const request = new Request(input, init)
const body = request.body ? await request.text() : undefined

const result = await client.call("fetch", {
url: request.url,
method: request.method,
Expand Down Expand Up @@ -152,6 +154,16 @@ export const TuiThreadCommand = cmd({
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url

// if server is started with password protection, we need to provide it
const authHeader = getAuthorizationHeader()
if (authHeader) {
customFetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const request = new Request(input, init)
request.headers.set("Authorization", authHeader)
return fetch(request)
}) as typeof fetch
}
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
Expand Down
9 changes: 1 addition & 8 deletions packages/opencode/src/cli/cmd/tui/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Config } from "@/config/config"
import { GlobalBus } from "@/bus/global"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import type { BunWebSocketData } from "hono/bun"
import { Flag } from "@/flag/flag"
import { getAuthorizationHeader } from "@/flag/auth"

await Log.init({
print: process.argv.includes("--print-logs"),
Expand Down Expand Up @@ -143,10 +143,3 @@ export const rpc = {
}

Rpc.listen(rpc)

function getAuthorizationHeader(): string | undefined {
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
return `Basic ${btoa(`${username}:${password}`)}`
}
11 changes: 11 additions & 0 deletions packages/opencode/src/flag/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Flag } from "@/flag/flag"

export function getAuthorizationHeader(options?: {
passwordFromCli?: string
usernameFromCli?: string
}): string | undefined {
const password = options?.passwordFromCli ?? Flag.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
const username = options?.usernameFromCli ?? Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
return `Basic ${btoa(`${username}:${password}`)}`
}
10 changes: 8 additions & 2 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Server } from "../server/server"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { getAuthorizationHeader } from "../flag/auth"
import { CodexAuthPlugin } from "./codex"
import { Session } from "../session"
import { NamedError } from "@opencode-ai/util/error"
Expand All @@ -22,11 +23,16 @@ export namespace Plugin {
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]

const state = Instance.state(async () => {
const authHeader = getAuthorizationHeader()

const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
directory: Instance.directory,
// @ts-ignore - fetch type incompatibility
fetch: async (...args) => Server.App().fetch(...args),
fetch: (async (input, init) => {
const request = new Request(input, init)
if (authHeader) request.headers.set("Authorization", authHeader)
return Server.App().fetch(request)
}) as typeof fetch,
})
const config = await Config.get()
const hooks: Hooks[] = []
Expand Down