Skip to content
Open
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
10 changes: 5 additions & 5 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ to one of these built-in driver integrations:
- `opencode`: the host-installed OpenCode CLI; DevSpace probes its major version
and uses the matching 1.x or 2.x protocol internally
- `pi`: the installed Pi coding-agent SDK, one in-process session per DevSpace agent
- `cursor`: ACP
- `copilot`: ACP
- `grok`: Grok Build ACP (`grok agent stdio`)
- `acp`: generic Agent Client Protocol integration. Cursor, Copilot, and Grok
are default provider instances using this driver; other ACP agents can be
configured without adding a DevSpace driver.

Codex and OpenCode are resolved from the user's environment rather than bundled
as executables. Run their normal login/setup flows before using them; set
Expand Down Expand Up @@ -124,8 +124,8 @@ DevSpace passes this through to providers that expose a matching control:
- `codex`: app-server model reasoning effort.
- `pi`: the AgentSession thinking-level control.
- `opencode`: model variant.
- `cursor` and `copilot`: ACP thought-level config when supported.
- `grok`: `--reasoning-effort` on startup and xAI's ACP model metadata for resumed sessions.
- ACP: standard model/thought-level config when advertised by the agent. The
Grok flavor also understands xAI's typed model metadata and startup effort.

### `disabled`

Expand Down
26 changes: 20 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ multiple independently configured instances of one integration:
```

Profiles can then select that instance with `provider: codex-work`. The driver
is the implementation (`codex`, `claude`, `opencode`, `pi`, `cursor`,
`copilot`, or `grok`); provider-native session ids remain internal to DevSpace.
is the implementation (`codex`, `claude`, `opencode`, `pi`, or `acp`);
provider-native session ids remain internal to DevSpace. Cursor, Copilot, and
Grok are default ACP provider instances rather than separate drivers.

`subagents.instructions` controls when ChatGPT receives the managed workflow:

Expand Down Expand Up @@ -198,10 +199,23 @@ runtime and automatically selects the OpenCode 1.x or 2.x protocol. Both major
versions use the same provider instance and profile configuration; `opencode2`
is not a separate driver.

Codex, Claude, Cursor, Copilot, and Grok also accept `command`. OpenCode and Pi
do not expose a command override. For providers that support it, an explicit
`command` wins over both the inherited command override and a command override
placed in `env`.
Codex, Claude, and ACP instances also accept `command`. OpenCode and Pi do not
expose a command override. Cursor, Copilot, and Grok retain their built-in ACP
commands when `command` is omitted. An arbitrary ACP instance declares its
executable and may provide fixed startup arguments under `config.args`:

```jsonc
{
"id": "kiro",
"driver": "acp",
"enabled": true,
"command": "kiro",
"config": { "args": ["acp"] }
}
```

`config.flavor` is reserved for the small built-in protocol quirks currently
needed by `cursor`, `copilot`, and `grok`; generic ACP agents normally omit it.

Existing process-level overrides remain supported: `CODEX_COMMAND`,
`CODEX_HOME`, `CLAUDE_COMMAND`, `CURSOR_COMMAND`, `COPILOT_COMMAND`,
Expand Down
21 changes: 21 additions & 0 deletions schema/v1/devspace.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
"claude",
"opencode",
"pi",
"acp",
"cursor",
"copilot",
"grok"
Expand Down Expand Up @@ -220,6 +221,26 @@
"type": "string",
"minLength": 1,
"pattern": "\\S"
},
"config": {
"type": "object",
Comment on lines +225 to +226

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Schema accepts unusable ACP config

The published schema accepts an enabled ACP provider such as kiro without a command, but runtime configuration parsing rejects it. Editor validation can therefore approve a configuration that DevSpace cannot load. This validation mismatch is a non-blocking concern.

Knowledge Base Used: Configuration and onboarding flow

Artifacts

Executable schema and runtime comparison script

  • The authored script validates a full config document with installed Ajv and parses that document with the runtime schema, providing the executed source for both conditions.

Validation output with command missing

  • Running the script without a command produced Ajv acceptance and runtime rejection at the provider command path, confirming the mismatch.

Validation output with command supplied

  • Running the same script with `command: "kiro"` produced acceptance by both validators, isolating the missing command as the difference.

View artifacts

T-Rex Ran code and verified through T-Rex

"properties": {
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"flavor": {
"type": "string",
"enum": [
"cursor",
"copilot",
"grok"
]
}
},
"additionalProperties": false
}
},
"required": [
Expand Down
4 changes: 2 additions & 2 deletions src/config-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type DevspaceConfig,
} from "./config-schema.js";
import { storedSubagentsConfigSchema } from "./local-agent-config.js";
import { LOCAL_AGENT_DRIVER_KINDS } from "./local-agent-provider.js";
import { LOCAL_AGENT_DEFAULT_PROVIDER_IDS } from "./local-agent-provider.js";

const legacyConfigSchema = z.object({
host: z.string().optional(),
Expand Down Expand Up @@ -92,7 +92,7 @@ function migrateLegacySubagents(
return {
enabled: value,
providers: value
? LOCAL_AGENT_DRIVER_KINDS.map((id) => ({ id, enabled: true }))
? LOCAL_AGENT_DEFAULT_PROVIDER_IDS.map((id) => ({ id, enabled: true }))
: [],
};
}
13 changes: 13 additions & 0 deletions src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const migrations: Migration[] = [
name: "local-agent-provider-instances",
up: migrateLocalAgentProviderInstances,
},
{
version: 10,
name: "local-agent-generic-acp-driver",
up: migrateLocalAgentGenericAcpDriver,
},
];

export function migrateDatabase(sqlite: Database.Database): void {
Expand Down Expand Up @@ -310,6 +315,14 @@ function migrateLocalAgentProviderInstances(sqlite: Database.Database): void {
`);
}

function migrateLocalAgentGenericAcpDriver(sqlite: Database.Database): void {
sqlite.exec(`
update local_agent_sessions
set driver = 'acp'
where driver in ('cursor', 'copilot', 'grok')
`);
}

function addColumnIfMissing(
sqlite: Database.Database,
table: "workspace_sessions" | "local_agent_sessions",
Expand Down
32 changes: 16 additions & 16 deletions src/local-agent-acp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const connection = {

const sessionIds: string[] = [];
const runtime = new AcpRuntime({
provider: "cursor",
flavor: "cursor",
command: "cursor-agent",
args: ["acp"],
env: {},
Expand Down Expand Up @@ -106,7 +106,7 @@ assert.equal(requests.filter(({ method }) => method === "session/close").length,
assert.equal(runtime.isAlive(), true);

const resumedRuntime = new AcpRuntime({
provider: "cursor",
flavor: "cursor",
command: "cursor-agent",
args: ["acp"],
env: {},
Expand Down Expand Up @@ -145,7 +145,7 @@ assert.equal(queues.has("cursor_session_1"), false);
assert.equal(requests.filter(({ method }) => method === "session/close").length, 1);

const closeOnlyRuntime = new AcpRuntime({
provider: "cursor",
flavor: "cursor",
command: "cursor-agent",
args: ["acp"],
env: {},
Expand Down Expand Up @@ -229,7 +229,7 @@ const overlapConnection = {
closed: new Promise<void>(() => undefined),
};
const overlapRuntime = new AcpRuntime({
provider: "cursor",
flavor: "cursor",
command: "cursor-agent",
args: ["acp"],
env: {},
Expand Down Expand Up @@ -259,7 +259,7 @@ await overlapRuntime.close();
const cachedContext = {
agentId: "agt_acp",
providerInstanceId: "cursor",
provider: "cursor" as const,
provider: "acp" as const,
workspaceRoot: "/tmp/project",
writeMode: "allowed" as const,
};
Expand All @@ -270,13 +270,13 @@ assert.deepEqual(acpCommandArgs("cursor", cachedContext), [
assert.deepEqual(acpCommandArgs("grok", {
...cachedContext,
providerInstanceId: "grok",
provider: "grok",
provider: "acp",
effort: "low",
}), ["agent", "--reasoning-effort", "low", "stdio"]);
assert.deepEqual(acpCommandArgs("grok", {
...cachedContext,
providerInstanceId: "grok",
provider: "grok",
provider: "acp",
effort: "low",
}, { GROK_AGENT_PROFILE: " /tmp/grok-coding-only.md " }), [
"agent", "--agent-profile", "/tmp/grok-coding-only.md", "--reasoning-effort", "low", "stdio",
Expand All @@ -291,11 +291,11 @@ assert.deepEqual(acpCommandArgs("copilot", { ...cachedContext, writeMode: "full_
"--acp", "--no-sandbox", "--allow-all", "-C", resolvedProject,
]);

const missingCommandDriver = new AcpLocalAgentDriver(
"cursor",
process.env,
() => join(tmpdir(), "devspace-definitely-missing-acp-command"),
);
const missingCommandDriver = new AcpLocalAgentDriver({
flavor: "cursor",
env: process.env,
commandResolver: () => join(tmpdir(), "devspace-definitely-missing-acp-command"),
});
const missingCommand = await missingCommandDriver.createRuntime(cachedContext);
assert.equal(missingCommand.isErr(), true);
if (missingCommand.isErr()) {
Expand All @@ -318,11 +318,11 @@ if (process.platform === "win32") {
`require("node:fs").writeFileSync(${JSON.stringify(marker)}, JSON.stringify(process.argv.slice(2)));\n`,
);
await writeFile(command, `@ECHO OFF\r\n"${process.execPath}" "${recorder}" %*\r\n`);
const shimDriver = new AcpLocalAgentDriver("copilot", process.env, () => command);
const shimDriver = new AcpLocalAgentDriver({ flavor: "copilot", env: process.env, commandResolver: () => command });
const shimStartup = await shimDriver.createRuntime({
...cachedContext,
providerInstanceId: "copilot",
provider: "copilot",
provider: "acp",
workspaceRoot,
});
assert.equal(shimStartup.isErr(), true);
Expand Down Expand Up @@ -397,7 +397,7 @@ const grokConnection = {
closed: new Promise<void>(() => undefined),
};
const grokRuntime = new AcpRuntime({
provider: "grok",
flavor: "grok",
command: "grok",
args: ["agent", "--reasoning-effort", "low", "stdio"],
env: {},
Expand Down Expand Up @@ -445,7 +445,7 @@ const grokConfigurationConnection = {
closed: new Promise<void>(() => undefined),
};
const grokConfigurationRuntime = new AcpRuntime({
provider: "grok",
flavor: "grok",
command: "grok",
args: ["agent", "stdio"],
env: {},
Expand Down
Loading
Loading