diff --git a/CHANGELOG.md b/CHANGELOG.md index cb962e813d8e..1bb117fc11c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ Code v99.99.999 ## Unreleased +### Added + +- `--agents` starts Code's agent host so agent sessions can run on the server. + Code's dedicated Agents Window is not bundled for the server build yet, so + agents surface in the regular chat UI. See the FAQ for the caveats. + ## [4.132.0](https://github.com/coder/code-server/releases/tag/v4.132.0) - 2026-08-10 Code v1.132.0 diff --git a/docs/FAQ.md b/docs/FAQ.md index 656b45978fe3..183d95d17c5d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -40,6 +40,7 @@ - [How do I hide the coder/coder promotion in Help: Getting Started?](#how-do-i-hide-the-codercoder-promotion-in-help-getting-started) - [How do I disable the proxy?](#how-do-i-disable-the-proxy) - [How do I disable file download?](#how-do-i-disable-file-download) +- [Can I use the Agents Window?](#can-i-use-the-agents-window) - [Why do web views not work?](#why-do-web-views-not-work) @@ -542,6 +543,40 @@ when using the option. You can pass the flag `--disable-file-downloads` to `code-server` +## Can I use the Agents Window? + +Not yet, but you can enable the agent host it runs on. + +VS Code 1.132 added the [Agents +Window](https://code.visualstudio.com/docs/agents/run/agents-window), opened on +the desktop with `code --agents`. It is built from `vs/sessions`, a workbench +that is separate from the regular one, and Code only bundles that workbench into +its `vscode-web` build (the one behind vscode.dev). The server build code-server +ships, `vscode-reh-web`, does not include it, so there is no Agents Window to +serve yet. + +What the server build does include is the agent host, the process that actually +runs agent sessions. Code starts it only when it is told where the host should +listen, and code-server does not tell it by default. Pass `--agents` to change +that: + +```console +code-server --agents +``` + +The agent host then listens on `agent-host.sock` inside your user data directory +(a named pipe on Windows), and Code registers the channel the browser uses to +reach it. Agent sessions show up in the regular chat UI rather than in their own +window. + +Two caveats: + +- Anyone who can read that socket can talk to the agent host, so keep your user + data directory private. It is not exposed over HTTP. +- Code resolves the Claude and Codex SDKs from `product.agentSdks`, which its + build pipeline only writes into the desktop server build. Those agents are + unavailable unless you point Code at a local SDK root yourself. + ## Why do web views not work? Web views rely on service workers, and service workers are only available in a diff --git a/src/node/cli.ts b/src/node/cli.ts index 0946c8e04344..543e8a921e7f 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,4 +1,5 @@ import { field, Level, logger } from "@coder/logger" +import * as crypto from "crypto" import { promises as fs } from "fs" import { load } from "js-yaml" import * as path from "path" @@ -91,6 +92,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "reuse-window"?: boolean "new-window"?: boolean "ignore-last-opened"?: boolean + agents?: boolean verbose?: boolean "app-name"?: string "welcome-text"?: string @@ -288,6 +290,16 @@ export const options: Options> = { short: "r", description: "Force to open a file or folder in an already opened window.", }, + // Named after Code's own --agents so the flags line up once we can serve the + // Agents Window itself. That needs Code's `vs/sessions` bundle, which its + // build only adds to the vscode-web entry points, not to the server build we + // package (see build/gulpfile.reh.ts in the submodule). + agents: { + type: "boolean", + description: + "Start Code's agent host so agent sessions can run on the server. Code's dedicated Agents Window is not \n" + + "bundled for the server yet, so agents surface in the regular chat UI rather than in their own window.", + }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, @@ -907,6 +919,22 @@ export interface CodeArgs extends UserProvidedCodeArgs { "without-browser-env-var"?: boolean compatibility?: string log?: string[] + "agent-host-path"?: string +} + +/** + * Where Code's agent host should listen when --agents is set. + * + * Windows has no Unix sockets so use a named pipe there instead. The path is + * derived from the user data directory since that is what separates concurrent + * instances from each other. + */ +export function agentHostSocketPath(userDataDir: string): string { + if (process.platform === "win32") { + const hash = crypto.createHash("sha1").update(userDataDir).digest("hex").substring(0, 16) + return `\\\\.\\pipe\\code-server-agent-host-${hash}` + } + return path.join(userDataDir, "agent-host.sock") } /** @@ -921,5 +949,10 @@ export const toCodeArgs = async (args: DefaultedArgs): Promise => { version: !!args.version, port: args.port?.toString(), log: args.log ? [args.log] : undefined, + // Telling Code where the agent host should listen is what makes it spawn + // one. It also registers the channel the workbench uses to reach the agent + // host over the remote connection; without a path that channel is + // registered as unavailable and agent sessions cannot connect. + "agent-host-path": args.agents ? agentHostSocketPath(args["user-data-dir"]) : undefined, } } diff --git a/src/node/main.ts b/src/node/main.ts index c2d3bd57852b..1b14f7c6ac62 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -1,11 +1,12 @@ import { field, logger } from "@coder/logger" +import { promises as fs } from "fs" import http from "http" import * as os from "os" import * as path from "path" import { Disposable } from "../common/emitter" import { plural } from "../common/util" import { createApp, ensureAddress } from "./app" -import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" +import { agentHostSocketPath, AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" import { commit, version, vsRootPath } from "./constants" import { loadCustomStrings } from "./i18n" import { register } from "./routes" @@ -119,6 +120,27 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st vscode.end() } +/** + * Remove a leftover agent host socket. + * + * Code binds the socket but never unlinks it, so a socket left behind by a + * killed instance makes the agent host fail to listen on the next start. + * Named pipes on Windows are not files and disappear on their own. + */ +const removeStaleAgentHostSocket = async (socketPath: string): Promise => { + if (process.platform === "win32") { + return + } + try { + await fs.unlink(socketPath) + logger.debug(`Removed stale agent host socket ${socketPath}`) + } catch (error: any) { + if (error.code !== "ENOENT") { + logger.warn(`Could not remove agent host socket ${socketPath}: ${error.message}`) + } + } +} + export const runCodeServer = async ( args: DefaultedArgs, ): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => { @@ -139,6 +161,11 @@ export const runCodeServer = async ( ) } + // Must happen before Code can be loaded, which is on the first request. + if (args.agents) { + await removeStaleAgentHostSocket(agentHostSocketPath(args["user-data-dir"])) + } + const app = await createApp(args) const protocol = args.cert ? "https" : "http" const serverAddress = ensureAddress(app.server, protocol) @@ -197,6 +224,10 @@ export const runCodeServer = async ( if (args["skip-auth-preflight"]) { logger.info(" - Skipping authentication for preflight requests") } + if (args.agents) { + logger.info(` - Agent host enabled on ${agentHostSocketPath(args["user-data-dir"])}`) + logger.info(" - Code's Agents Window is not bundled for the server; agents appear in the regular chat UI") + } if (process.env.VSCODE_PROXY_URI) { logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`) } diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 85e16f38ca99..f5e02fa7dea9 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -10,6 +10,7 @@ import { setDefaults, shouldOpenInExistingInstance, toCodeArgs, + agentHostSocketPath, optionDescriptions, options, Options, @@ -114,6 +115,8 @@ describe("parser", () => { "--skip-auth-preflight", + "--agents", + ["--session-socket", "/tmp/override-code-server-ipc-socket"], ["--reconnection-grace-time", "86400"], @@ -157,6 +160,7 @@ describe("parser", () => { "reconnection-grace-time": "86400", "abs-proxy-base-path": "/codeserver/app1", "skip-auth-preflight": true, + agents: true, }) }) @@ -984,6 +988,7 @@ describe("toCodeArgs", () => { port: "8080", version: false, log: undefined, + "agent-host-path": undefined, } const testName = "vscode-args" @@ -1006,6 +1011,25 @@ describe("toCodeArgs", () => { _: [file], }) }) + + it("should tell Code where to run the agent host", async () => { + expect(await toCodeArgs(await setDefaults(parse(["--agents"])))).toStrictEqual({ + ...vscodeDefaults, + agents: true, + "agent-host-path": agentHostSocketPath(paths.data), + }) + }) + + it("should derive the agent host socket from the user data directory", async () => { + if (process.platform === "win32") { + expect(agentHostSocketPath("C:\\one")).toMatch(/^\\\\\.\\pipe\\code-server-agent-host-[0-9a-f]{16}$/) + } else { + expect(agentHostSocketPath("/one")).toBe(path.join("/one", "agent-host.sock")) + } + // Concurrent instances have separate user data directories, so their agent + // hosts must not land on the same socket. + expect(agentHostSocketPath("/one")).not.toBe(agentHostSocketPath("/two")) + }) }) describe("optionDescriptions", () => {