From 0d266b4547a8d67f770efbdbe0f41a6fecf322ac Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:00:49 +0530 Subject: [PATCH 1/2] refactor(harness): model coding harness configuration --- src/config.test.ts | 29 +++++++++++++++++++++++------ src/config.ts | 12 ++++++++---- src/harness.ts | 25 +++++++++++++++++++++++++ src/server.ts | 17 +++++++++-------- 4 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 src/harness.ts diff --git a/src/config.test.ts b/src/config.test.ts index 9cbc261b..fd8ed336 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -15,12 +15,29 @@ assert.equal(loadConfig(baseEnv).widgets, "full"); assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "changes" }).widgets, "changes"); assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "full" }).widgets, "full"); assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "off" }).widgets, "off"); -assert.equal(loadConfig(baseEnv).toolMode, "minimal"); -assert.equal(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "minimal" }).toolMode, "minimal"); -assert.equal(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "full" }).toolMode, "full"); -assert.equal(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "codex" }).toolMode, "codex"); -assert.equal(loadConfig({ ...baseEnv, DEVSPACE_MINIMAL_TOOLS: "0" }).toolMode, "full"); -assert.equal(loadConfig({ ...baseEnv, DEVSPACE_MINIMAL_TOOLS: "1" }).toolMode, "minimal"); +assert.deepEqual(loadConfig(baseEnv).harness, { + kind: "claude-code", + inspection: "shell", +}); +assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "minimal" }).harness, { + kind: "claude-code", + inspection: "shell", +}); +assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "full" }).harness, { + kind: "claude-code", + inspection: "dedicated", +}); +assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "codex" }).harness, { + kind: "codex", +}); +assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_MINIMAL_TOOLS: "0" }).harness, { + kind: "claude-code", + inspection: "dedicated", +}); +assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_MINIMAL_TOOLS: "1" }).harness, { + kind: "claude-code", + inspection: "shell", +}); assert.throws( () => loadConfig({ ...baseEnv, DEVSPACE_MINIMAL_TOOLS: "maybe" }), /Invalid DEVSPACE_MINIMAL_TOOLS: maybe/, diff --git a/src/config.ts b/src/config.ts index aa82a66e..99723cf1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,8 +5,12 @@ import type { LoggingConfig, LogFormat, LogLevel } from "./logger.js"; import type { OAuthConfig } from "./oauth-provider.js"; import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js"; import { resolveSubagentsConfig, type SubagentsConfig } from "./local-agent-config.js"; +import { + harnessFromLegacyToolMode, + type HarnessConfig, + type LegacyToolMode, +} from "./harness.js"; -export type ToolMode = "minimal" | "full" | "codex"; export type WidgetMode = "off" | "changes" | "full"; const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60; const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60; @@ -19,7 +23,7 @@ export interface ServerConfig { allowedRoots: string[]; allowedHosts: string[]; publicBaseUrl: string; - toolMode: ToolMode; + harness: HarnessConfig; widgets: WidgetMode; stateDir: string; worktreeRoot: string; @@ -90,7 +94,7 @@ function parseBoolean(value: string | undefined, name: string): boolean { throw new Error(`Invalid ${name}: ${value}`); } -function parseToolMode(env: NodeJS.ProcessEnv): ToolMode { +function parseLegacyToolMode(env: NodeJS.ProcessEnv): LegacyToolMode { const mode = env.DEVSPACE_TOOL_MODE; if (mode === "minimal" || mode === "full" || mode === "codex") return mode; if (mode) throw new Error(`Invalid DEVSPACE_TOOL_MODE: ${mode}`); @@ -240,7 +244,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig { allowedRoots: parseAllowedRoots(env.DEVSPACE_ALLOWED_ROOTS ?? files.config.allowedRoots), allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts), publicBaseUrl, - toolMode: parseToolMode(env), + harness: harnessFromLegacyToolMode(parseLegacyToolMode(env)), widgets: parseWidgetMode(env.DEVSPACE_WIDGETS), stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())), worktreeRoot: resolve(expandHomePath(env.DEVSPACE_WORKTREE_ROOT ?? files.config.worktreeRoot ?? defaultWorktreeRoot())), diff --git a/src/harness.ts b/src/harness.ts new file mode 100644 index 00000000..ab7e5eb7 --- /dev/null +++ b/src/harness.ts @@ -0,0 +1,25 @@ +export type HarnessConfig = + | { + kind: "claude-code"; + inspection: "shell" | "dedicated"; + } + | { + kind: "codex"; + }; + +export type LegacyToolMode = "minimal" | "full" | "codex"; + +export function harnessFromLegacyToolMode(mode: LegacyToolMode): HarnessConfig { + switch (mode) { + case "minimal": + return { kind: "claude-code", inspection: "shell" }; + case "full": + return { kind: "claude-code", inspection: "dedicated" }; + case "codex": + return { kind: "codex" }; + } +} + +export function usesDedicatedInspection(harness: HarnessConfig): boolean { + return harness.kind === "claude-code" && harness.inspection === "dedicated"; +} diff --git a/src/server.ts b/src/server.ts index 16c2010d..df338733 100644 --- a/src/server.ts +++ b/src/server.ts @@ -23,6 +23,7 @@ import { registerArtifactTools, } from "./artifact-tools.js"; import { loadConfig, type ServerConfig, type WidgetMode } from "./config.js"; +import { usesDedicatedInspection } from "./harness.js"; import { createOpenAIIncomingArtifactAdapter, type IncomingArtifactAdapter, @@ -202,11 +203,11 @@ function serverInstructions(config: ServerConfig): string { ? " If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs." : ""; - if (config.toolMode === "codex") { + if (config.harness.kind === "codex") { return `Use DevSpace for coding work. Call ${toolNames.openWorkspace} once for each project folder or isolated worktree, then keep using its workspaceId. During continued work in the same project or worktree, do not call ${toolNames.openWorkspace} again. Open another workspace only when changing projects, switching checkout/worktree mode, creating another isolated worktree, or when the current workspaceId is rejected. Use ${toolNames.read} for direct file reads, apply_patch for all file modifications, exec_command for inspection, tests, builds, and other commands, and write_stdin to poll or interact with running processes. Follow instructions returned by ${toolNames.openWorkspace}; read applicable instruction and skill files before working in their scope.${artifactInstruction}${showChangesInstruction}`; } - const inspection = config.toolMode !== "full" + const inspection = !usesDedicatedInspection(config.harness) ? `In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled; use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection. ` : `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. `; @@ -1055,7 +1056,7 @@ export function createMcpServer( }, ); - if (config.toolMode !== "codex") { + if (config.harness.kind === "claude-code") { registerAppTool( server, toolNames.write, @@ -1221,7 +1222,7 @@ export function createMcpServer( ); } - if (config.toolMode === "codex") { + if (config.harness.kind === "codex") { registerAppTool( server, "apply_patch", @@ -1351,7 +1352,7 @@ export function createMcpServer( ); } - if (config.toolMode === "full") { + if (usesDedicatedInspection(config.harness)) { registerAppTool( server, toolNames.grep, @@ -1562,13 +1563,13 @@ export function createMcpServer( ); } - if (config.toolMode !== "codex") { + if (config.harness.kind === "claude-code") { registerAppTool( server, toolNames.shell, { title: "Bash", - description: config.toolMode !== "full" + description: !usesDedicatedInspection(config.harness) ? `Run a shell command in a workspace. Use only for tests, builds, git inspection, package scripts, search, file discovery, and directory inspection. In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled; use command-line tools such as grep, rg, find, ls, and tree for those read-only inspection actions. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read} for direct file reads. This is powerful execution and should only be exposed behind strong authentication.` : `Run a shell command in a workspace. Use only for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. This is powerful execution and should only be exposed behind strong authentication.`, inputSchema: { @@ -1654,7 +1655,7 @@ export function createMcpServer( ); } - if (config.toolMode === "codex") { + if (config.harness.kind === "codex") { registerCodexProcessTools(server, config, workspaces, processSessions); } From 812f613a4e27611eb5c15fdc3ac718a65c11db34 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 24 Aug 2026 00:00:49 +0530 Subject: [PATCH 2/2] test(harness): lock tool contracts --- src/server.test.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/server.test.ts b/src/server.test.ts index cb29d11c..b35de516 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -80,6 +80,30 @@ test("open_workspace keeps lifecycle flags out of model output and preserves com assert.ok(Array.isArray(card.agents)); }); +test("legacy tool modes resolve to the intended coding harness tool contracts", async (t) => { + const cases = [ + { + mode: "minimal" as const, + tools: ["open_workspace", "read", "write", "edit", "bash"], + }, + { + mode: "full" as const, + tools: ["open_workspace", "read", "write", "edit", "grep", "glob", "ls", "bash"], + }, + { + mode: "codex" as const, + tools: ["open_workspace", "read", "apply_patch", "exec_command", "write_stdin"], + }, + ]; + + for (const { mode, tools } of cases) { + const context = await fixture(t, { toolMode: mode }); + const listed = await context.client.listTools(); + assert.deepEqual(listed.tools.map((tool) => tool.name), tools); + await context.close(); + } +}); + test("open_workspace refreshes provider availability for each catalog", async (t) => { let available = false; const context = await fixture(t, { @@ -247,6 +271,7 @@ async function fixture( git?: boolean; localAgentProviders?: LocalAgentProviderAvailability[] | (() => LocalAgentProviderAvailability[]); subagents?: SubagentsConfig; + toolMode?: "minimal" | "full" | "codex"; } = {}, ): Promise { const root = await mkdtemp(join(tmpdir(), "devspace-server-test-")); @@ -285,7 +310,7 @@ async function fixture( DEVSPACE_WORKTREE_ROOT: join(root, ".worktrees"), DEVSPACE_AGENT_DIR: agentDir, DEVSPACE_WIDGETS: "full", - DEVSPACE_TOOL_MODE: "full", + DEVSPACE_TOOL_MODE: options.toolMode ?? "full", DEVSPACE_SUBAGENTS: options.localAgentProviders ? "1" : "0", DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", PORT: "1",