Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/runtime-config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
47 changes: 45 additions & 2 deletions src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ export type HarnessConfig =

export type LegacyToolMode = "minimal" | "full" | "codex";

export type HarnessToolGroup =
| "write-edit"
| "dedicated-inspection"
| "bash"
| "apply-patch"
| "process-session";

export interface CompiledHarness {
toolGroups: readonly HarnessToolGroup[];
instructions: string;
bashDescription?: string;
}

export function harnessFromLegacyToolMode(mode: LegacyToolMode): HarnessConfig {
switch (mode) {
case "minimal":
Expand All @@ -20,6 +33,36 @@ export function harnessFromLegacyToolMode(mode: LegacyToolMode): HarnessConfig {
}
}

export function usesDedicatedInspection(harness: HarnessConfig): boolean {
return harness.kind === "claude-code" && harness.inspection === "dedicated";
export function compileHarness(
harness: HarnessConfig,
options: { skillsEnabled: boolean },
): CompiledHarness {
if (harness.kind === "codex") {
return {
toolGroups: ["apply-patch", "process-session"],
instructions:
"Use DevSpace for coding work. Call open_workspace 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 open_workspace again. Open another workspace only when changing projects, switching checkout/worktree mode, creating another isolated worktree, or when the current workspaceId is rejected. Use 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 open_workspace; read applicable instruction and skill files before working in their scope.",
};
}

const dedicatedInspection = harness.inspection === "dedicated";
const inspectionInstruction = dedicatedInspection
? "Prefer read, grep, glob, and ls for file inspection. "
: "In shell inspection mode, grep, glob, and ls are disabled; use bash with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection. ";
const skillsInstruction = options.skillsEnabled
? "When open_workspace returns available skills and a task matches a skill, use read to read that skill's path before proceeding. Skill paths may be outside the workspace, but read only permits advertised SKILL.md files and files under already-loaded skill directories. "
: "";
const commonInstruction =
"Use DevSpace for coding work. Call open_workspace 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 open_workspace again. Open another workspace only when changing projects, switching checkout/worktree mode, creating another isolated worktree, or when the current workspaceId is rejected. Follow instructions returned by open_workspace. Before working under a path listed in availableAgentsFiles, use read to inspect that instruction file and follow it. ";

return {
toolGroups: dedicatedInspection
? ["write-edit", "dedicated-inspection", "bash"]
: ["write-edit", "bash"],
instructions:
`${commonInstruction}${skillsInstruction}${inspectionInstruction}Prefer edit for targeted modifications, write only for new files or complete rewrites, and bash for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with bash; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.`,
bashDescription: dedicatedInspection
? "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 bash 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 edit for targeted changes and write for new files or full rewrites. Prefer read, grep, glob, and ls for file inspection. 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, search, file discovery, and directory inspection. In shell inspection mode, grep, glob, and ls are disabled; use command-line tools such as grep, rg, find, ls, and tree for those read-only inspection actions. Do not use bash 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 edit for targeted changes and write for new files or full rewrites. Prefer read for direct file reads. This is powerful execution and should only be exposed behind strong authentication.",
};
}
55 changes: 55 additions & 0 deletions src/runtime-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadConfig } from "./config.js";
import { compileRuntime } from "./runtime-config.js";

const configDir = mkdtempSync(join(tmpdir(), "devspace-runtime-config-test-"));
const baseEnv = {
DEVSPACE_CONFIG_DIR: configDir,
DEVSPACE_ALLOWED_ROOTS: process.cwd(),
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
};

const minimal = compileRuntime(loadConfig(baseEnv), { artifactDownloadSupported: true });
assert.deepEqual(minimal.runtimeHarness.toolGroups, ["write-edit", "bash"]);

const full = compileRuntime(
loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "full" }),
{ artifactDownloadSupported: true },
);
assert.deepEqual(full.runtimeHarness.toolGroups, ["write-edit", "dedicated-inspection", "bash"]);

const codex = compileRuntime(
loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "codex" }),
{ artifactDownloadSupported: true },
);
assert.deepEqual(codex.runtimeHarness.toolGroups, ["apply-patch", "process-session"]);

assert.deepEqual(minimal.artifactCapability, {
status: "unavailable",
reason: "disabled",
});

const unsupportedArtifacts = compileRuntime(
loadConfig({ ...baseEnv, DEVSPACE_ARTIFACTS: "1" }),
{ artifactDownloadSupported: false },
);
assert.deepEqual(unsupportedArtifacts.artifactCapability, {
status: "unavailable",
reason: "unsupported-platform",
});

const availableArtifacts = compileRuntime(
loadConfig({
...baseEnv,
DEVSPACE_ARTIFACTS: "1",
DEVSPACE_ARTIFACT_MAX_FILE_BYTES: "123",
}),
{ artifactDownloadSupported: true },
);
assert.deepEqual(availableArtifacts.artifactCapability, {
status: "available",
maxFileBytes: 123,
});
32 changes: 32 additions & 0 deletions src/runtime-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ServerConfig } from "./config.js";
import { compileHarness, type CompiledHarness } from "./harness.js";

export type ArtifactCapability =
| {
status: "available";
maxFileBytes: number;
}
| {
status: "unavailable";
reason: "disabled" | "unsupported-platform";
};

export interface RuntimeConfig extends ServerConfig {
runtimeHarness: CompiledHarness;
artifactCapability: ArtifactCapability;
}

export function compileRuntime(
config: ServerConfig,
environment: { artifactDownloadSupported: boolean },
): RuntimeConfig {
return {
...config,
runtimeHarness: compileHarness(config.harness, { skillsEnabled: config.skillsEnabled }),
artifactCapability: !config.artifactsEnabled
? { status: "unavailable", reason: "disabled" }
: environment.artifactDownloadSupported
? { status: "available", maxFileBytes: config.artifactMaxFileBytes }
: { status: "unavailable", reason: "unsupported-platform" },
};
}
5 changes: 3 additions & 2 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { SubagentsConfig } from "./local-agent-config.js";
import { createReviewCheckpointManager } from "./review-checkpoints.js";
import { ProcessSessionManager } from "./process-sessions.js";
import { createMcpServer } from "./server.js";
import { compileRuntime } from "./runtime-config.js";
import { SqliteWorkspaceStore } from "./workspace-store.js";
import { WorkspaceRegistry } from "./workspaces.js";

Expand Down Expand Up @@ -224,7 +225,7 @@ test("checkout reuse and context suppression survive a registry restart", async

const restoredStore = new SqliteWorkspaceStore(context.stateDir);
const restoredServer = createMcpServer(
context.config,
compileRuntime(context.config, { artifactDownloadSupported: true }),
new WorkspaceRegistry(context.config, restoredStore),
createReviewCheckpointManager(),
new ProcessSessionManager(),
Expand Down Expand Up @@ -338,7 +339,7 @@ async function fixture(
const store = new SqliteWorkspaceStore(stateDir);
const workspaces = new WorkspaceRegistry(config, store);
const server = createMcpServer(
config,
compileRuntime(config, { artifactDownloadSupported: true }),
workspaces,
createReviewCheckpointManager(),
new ProcessSessionManager(),
Expand Down
Loading
Loading