summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-01-12 15:23:12 -0500
committerDax Raad <[email protected]>2026-01-12 15:23:19 -0500
commit1954c1255e71ab86283e6a99ac82598268fc308d (patch)
tree376c82a88067620e74f9d9e6bba06b8ce114865d
parentb4f33485a7845183272083134618e8f7a15574e0 (diff)
downloadopencode-1954c1255e71ab86283e6a99ac82598268fc308d.tar.gz
opencode-1954c1255e71ab86283e6a99ac82598268fc308d.zip
core: add password authentication and improve server security
- Add OPENCODE_PASSWORD flag for basic auth protection - Show security warnings when password is not set - Remove deprecated spawn command - Improve error handling with HTTPException responses
-rw-r--r--packages/opencode/src/cli/cmd/run.ts10
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts4
-rw-r--r--packages/opencode/src/cli/cmd/tui/spawn.ts48
-rw-r--r--packages/opencode/src/cli/cmd/web.ts4
-rw-r--r--packages/opencode/src/flag/flag.ts1
-rw-r--r--packages/opencode/src/index.ts2
-rw-r--r--packages/opencode/src/server/server.ts9
7 files changed, 23 insertions, 55 deletions
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index a86b435ec..54248f96f 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -339,13 +339,15 @@ export const RunCommand = cmd({
}
await bootstrap(process.cwd(), async () => {
- const server = Server.listen({ port: args.port ?? 0, hostname: "127.0.0.1" })
- const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}` })
+ const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
+ const request = new Request(input, init)
+ return Server.App().fetch(request)
+ }) as typeof globalThis.fetch
+ const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
if (args.command) {
const exists = await Command.get(args.command)
if (!exists) {
- server.stop()
UI.error(`Command "${args.command}" not found`)
process.exit(1)
}
@@ -370,7 +372,6 @@ export const RunCommand = cmd({
})()
if (!sessionID) {
- server.stop()
UI.error("Session not found")
process.exit(1)
}
@@ -389,7 +390,6 @@ export const RunCommand = cmd({
}
await execute(sdk, sessionID)
- server.stop()
})
},
})
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 657f9196c..441240609 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/packages/opencode/src/cli/cmd/serve.ts
@@ -1,12 +1,16 @@
import { Server } from "../../server/server"
import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
+import { Flag } from "../../flag/flag"
export const ServeCommand = cmd({
command: "serve",
builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
handler: async (args) => {
+ if (!Flag.OPENCODE_PASSWORD) {
+ console.log("Warning: OPENCODE_PASSWORD is not set; server is unsecured.")
+ }
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts
deleted file mode 100644
index ef359e6f4..000000000
--- a/packages/opencode/src/cli/cmd/tui/spawn.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { cmd } from "@/cli/cmd/cmd"
-import { Instance } from "@/project/instance"
-import path from "path"
-import { Server } from "@/server/server"
-import { upgrade } from "@/cli/upgrade"
-import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
-
-export const TuiSpawnCommand = cmd({
- command: "spawn [project]",
- builder: (yargs) =>
- withNetworkOptions(yargs).positional("project", {
- type: "string",
- describe: "path to start opencode in",
- }),
- handler: async (args) => {
- upgrade()
- const opts = await resolveNetworkOptions(args)
- const server = Server.listen(opts)
- const bin = process.execPath
- const cmd = []
- let cwd = process.cwd()
- if (bin.endsWith("bun")) {
- cmd.push(
- process.execPath,
- "run",
- "--conditions",
- "browser",
- new URL("../../../index.ts", import.meta.url).pathname,
- )
- cwd = new URL("../../../../", import.meta.url).pathname
- } else cmd.push(process.execPath)
- cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
- const proc = Bun.spawn({
- cmd,
- cwd,
- stdout: "inherit",
- stderr: "inherit",
- stdin: "inherit",
- env: {
- ...process.env,
- BUN_OPTIONS: "",
- },
- })
- await proc.exited
- await Instance.disposeAll()
- await server.stop(true)
- },
-})
diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts
index e2ecc1187..abb347798 100644
--- a/packages/opencode/src/cli/cmd/web.ts
+++ b/packages/opencode/src/cli/cmd/web.ts
@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
+import { Flag } from "../../flag/flag"
import open from "open"
import { networkInterfaces } from "os"
@@ -32,6 +33,9 @@ export const WebCommand = cmd({
builder: (yargs) => withNetworkOptions(yargs),
describe: "start opencode server and open web interface",
handler: async (args) => {
+ if (!Flag.OPENCODE_PASSWORD) {
+ UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_PASSWORD is not set; server is unsecured.")
+ }
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
UI.empty()
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 50e3cd79e..77260a84c 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -20,6 +20,7 @@ export namespace Flag {
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
+ export const OPENCODE_PASSWORD = process.env["OPENCODE_PASSWORD"]
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 6099443e7..3de7735bd 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -21,7 +21,6 @@ import { ExportCommand } from "./cli/cmd/export"
import { ImportCommand } from "./cli/cmd/import"
import { AttachCommand } from "./cli/cmd/tui/attach"
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
-import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
import { AcpCommand } from "./cli/cmd/acp"
import { EOL } from "os"
import { WebCommand } from "./cli/cmd/web"
@@ -81,7 +80,6 @@ const cli = yargs(hideBin(process.argv))
.command(AcpCommand)
.command(McpCommand)
.command(TuiThreadCommand)
- .command(TuiSpawnCommand)
.command(AttachCommand)
.command(RunCommand)
.command(GenerateCommand)
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 32d7a1795..05024acde 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -7,6 +7,7 @@ import { Hono } from "hono"
import { cors } from "hono/cors"
import { stream, streamSSE } from "hono/streaming"
import { proxy } from "hono/proxy"
+import { basicAuth } from "hono/basic-auth"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
@@ -25,6 +26,7 @@ import { Project } from "../project/project"
import { Vcs } from "../project/vcs"
import { Agent } from "../agent/agent"
import { Auth } from "../auth"
+import { Flag } from "../flag/flag"
import { Command } from "../command"
import { ProviderAuth } from "../provider/auth"
import { Global } from "../global"
@@ -45,6 +47,7 @@ import { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
+import { HTTPException } from "hono/http-exception"
import { errors } from "./error"
import { Pty } from "@/pty"
import { PermissionNext } from "@/permission/next"
@@ -80,6 +83,7 @@ export namespace Server {
log.error("failed", {
error: err,
})
+ if (err instanceof HTTPException) return err.getResponse()
if (err instanceof NamedError) {
let status: ContentfulStatusCode
if (err instanceof Storage.NotFoundError) status = 404
@@ -93,6 +97,11 @@ export namespace Server {
status: 500,
})
})
+ .use((c, next) => {
+ const password = Flag.OPENCODE_PASSWORD
+ if (!password) return next()
+ return basicAuth({ username: "opencode", password })(c, next)
+ })
.use(async (c, next) => {
const skipLogging = c.req.path === "/log"
if (!skipLogging) {