Skip to content

Commit 01e367f

Browse files
louisgvclaude
andcommitted
fix: reject disabled agents in CLI validation instead of silently proceeding
resolveEntityKey() and checkEntity() checked manifest.agents[input] directly, bypassing the disabled filter in agentKeys(). This let users run `spawn cursor <cloud>` even though cursor is disabled, wasting time provisioning a VM for an agent that can't route through OpenRouter. Now both functions check the disabled flag and show the disabled_reason to the user. Also removes stale cursor references from spawn skill templates injected into child VMs. Agent: code-health Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1cfa9ca commit 01e367f

5 files changed

Lines changed: 86 additions & 6 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/spawn",
3-
"version": "0.27.3",
3+
"version": "0.27.4",
44
"type": "module",
55
"bin": {
66
"spawn": "cli.js"

packages/cli/src/__tests__/check-entity.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Manifest } from "../manifest";
22

33
import { beforeEach, describe, expect, it } from "bun:test";
4-
import { checkEntity } from "../commands/index.js";
4+
import { checkEntity, resolveAgentKey } from "../commands/index.js";
55

66
/**
77
* Tests for checkEntity (commands/shared.ts).
@@ -383,6 +383,76 @@ describe("checkEntity", () => {
383383
});
384384
});
385385

386+
// ── Disabled agents ─────────────────────────────────────────────────────
387+
388+
describe("disabled agents", () => {
389+
let disabledManifest: Manifest;
390+
391+
beforeEach(() => {
392+
disabledManifest = {
393+
agents: {
394+
claude: {
395+
name: "Claude Code",
396+
description: "AI coding assistant",
397+
url: "https://claude.ai",
398+
install: "npm install -g claude",
399+
launch: "claude",
400+
env: {
401+
ANTHROPIC_API_KEY: "test",
402+
},
403+
},
404+
cursor: {
405+
name: "Cursor CLI",
406+
description: "AI coding agent",
407+
url: "https://cursor.com",
408+
install: "curl https://cursor.com/install | bash",
409+
launch: "agent",
410+
env: {},
411+
disabled: true,
412+
disabled_reason: "Cursor CLI uses a proprietary protocol.",
413+
},
414+
},
415+
clouds: {
416+
sprite: {
417+
name: "Sprite",
418+
description: "Lightweight VMs",
419+
price: "test",
420+
url: "https://sprite.sh",
421+
type: "vm",
422+
auth: "SPRITE_TOKEN",
423+
provision_method: "api",
424+
exec_method: "ssh",
425+
interactive_method: "ssh",
426+
},
427+
},
428+
matrix: {
429+
"sprite/claude": "implemented",
430+
"sprite/cursor": "implemented",
431+
},
432+
};
433+
});
434+
435+
it("checkEntity returns false for a disabled agent", () => {
436+
expect(checkEntity(disabledManifest, "cursor", "agent")).toBe(false);
437+
});
438+
439+
it("checkEntity returns true for an enabled agent in the same manifest", () => {
440+
expect(checkEntity(disabledManifest, "claude", "agent")).toBe(true);
441+
});
442+
443+
it("resolveAgentKey returns null for a disabled agent", () => {
444+
expect(resolveAgentKey(disabledManifest, "cursor")).toBeNull();
445+
});
446+
447+
it("resolveAgentKey resolves an enabled agent normally", () => {
448+
expect(resolveAgentKey(disabledManifest, "claude")).toBe("claude");
449+
});
450+
451+
it("checkEntity still works for clouds even when agents are disabled", () => {
452+
expect(checkEntity(disabledManifest, "sprite", "cloud")).toBe(true);
453+
});
454+
});
455+
386456
// ── Manifest with overlapping key names ────────────────────────────────
387457

388458
describe("manifest with overlapping patterns", () => {

packages/cli/src/commands/shared.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ export function findClosestKeyByNameOrKey(
162162
function resolveEntityKey(manifest: Manifest, input: string, kind: "agent" | "cloud"): string | null {
163163
const collection = getEntityCollection(manifest, kind);
164164
if (collection[input]) {
165+
if (kind === "agent" && manifest.agents[input].disabled) {
166+
return null;
167+
}
165168
return input;
166169
}
167170
const keys = getEntityKeys(manifest, kind);
@@ -285,6 +288,13 @@ export function checkEntity(manifest: Manifest, value: string, kind: "agent" | "
285288
const def = ENTITY_DEFS[kind];
286289
const collection = getEntityCollection(manifest, kind);
287290
if (collection[value]) {
291+
if (kind === "agent" && manifest.agents[value].disabled) {
292+
p.log.error(`${pc.bold(manifest.agents[value].name)} is temporarily disabled.`);
293+
if (manifest.agents[value].disabled_reason) {
294+
p.log.info(manifest.agents[value].disabled_reason);
295+
}
296+
return false;
297+
}
288298
return true;
289299
}
290300

packages/cli/src/shared/agent-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ async function setupCursorConfig(runner: CloudRunner, _apiKey: string): Promise<
193193
'spawn <agent> <cloud> --headless --output json --prompt "task description"',
194194
"```",
195195
"",
196-
"## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie, cursor",
196+
"## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie",
197197
"## Clouds: hetzner, digitalocean, aws, gcp, sprite",
198198
"",
199199
"The command returns JSON with connection details. Use this to delegate subtasks",

packages/cli/src/shared/spawn-skill.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ You have the \`spawn\` CLI installed. Use it to provision cloud VMs with AI agen
3030
spawn <agent> <cloud> --headless --output json --prompt "task description"
3131
\`\`\`
3232
33-
## Agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie
33+
## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie
3434
## Clouds: hetzner, digitalocean, aws, gcp, sprite
3535
3636
## Example
@@ -51,7 +51,7 @@ ssh -o StrictHostKeyChecking=accept-new root@<ip_address> '~/.local/bin/claude -
5151
\`\`\`
5252
5353
### Known gotchas — do NOT waste time rediscovering these:
54-
- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/cursor\`, etc.
54+
- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/codex\`, etc.
5555
- Claude Code blocks \`--dangerously-skip-permissions\` as root. Use \`--print\` for one-shot prompts instead — it works as root and returns the response directly.
5656
- Do NOT try to create a non-root user to work around root restrictions — it requires copying config files (\`.claude.json\`, \`.claude/settings.json\`, env vars) and is fragile. Just use \`--print\`.
5757
- For long-running tasks, use \`spawn delete --name <name> --yes\` to clean up when done.
@@ -76,7 +76,7 @@ const HERMES_SNIPPET = `
7676
7777
You have the \`spawn\` CLI for creating child cloud VMs with AI agents.
7878
Use \`spawn <agent> <cloud> --headless --output json --prompt "task"\` to delegate work.
79-
Available agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie.
79+
Available agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie.
8080
Cloud credentials are pre-configured. Run \`spawn list --json\` to see children.
8181
Note: --headless only provisions the VM. To run a prompt, SSH in: \`ssh root@<ip> '~/.local/bin/<agent> --print "prompt"'\`.
8282
`;

0 commit comments

Comments
 (0)