mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix: reject disabled agents in CLI validation instead of silently proceeding (#3061)
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: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1cfa9ca1a7
commit
db77121414
5 changed files with 86 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.27.3",
|
||||
"version": "0.27.4",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Manifest } from "../manifest";
|
||||
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { checkEntity } from "../commands/index.js";
|
||||
import { checkEntity, resolveAgentKey } from "../commands/index.js";
|
||||
|
||||
/**
|
||||
* Tests for checkEntity (commands/shared.ts).
|
||||
|
|
@ -383,6 +383,76 @@ describe("checkEntity", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Disabled agents ─────────────────────────────────────────────────────
|
||||
|
||||
describe("disabled agents", () => {
|
||||
let disabledManifest: Manifest;
|
||||
|
||||
beforeEach(() => {
|
||||
disabledManifest = {
|
||||
agents: {
|
||||
claude: {
|
||||
name: "Claude Code",
|
||||
description: "AI coding assistant",
|
||||
url: "https://claude.ai",
|
||||
install: "npm install -g claude",
|
||||
launch: "claude",
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: "test",
|
||||
},
|
||||
},
|
||||
cursor: {
|
||||
name: "Cursor CLI",
|
||||
description: "AI coding agent",
|
||||
url: "https://cursor.com",
|
||||
install: "curl https://cursor.com/install | bash",
|
||||
launch: "agent",
|
||||
env: {},
|
||||
disabled: true,
|
||||
disabled_reason: "Cursor CLI uses a proprietary protocol.",
|
||||
},
|
||||
},
|
||||
clouds: {
|
||||
sprite: {
|
||||
name: "Sprite",
|
||||
description: "Lightweight VMs",
|
||||
price: "test",
|
||||
url: "https://sprite.sh",
|
||||
type: "vm",
|
||||
auth: "SPRITE_TOKEN",
|
||||
provision_method: "api",
|
||||
exec_method: "ssh",
|
||||
interactive_method: "ssh",
|
||||
},
|
||||
},
|
||||
matrix: {
|
||||
"sprite/claude": "implemented",
|
||||
"sprite/cursor": "implemented",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("checkEntity returns false for a disabled agent", () => {
|
||||
expect(checkEntity(disabledManifest, "cursor", "agent")).toBe(false);
|
||||
});
|
||||
|
||||
it("checkEntity returns true for an enabled agent in the same manifest", () => {
|
||||
expect(checkEntity(disabledManifest, "claude", "agent")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolveAgentKey returns null for a disabled agent", () => {
|
||||
expect(resolveAgentKey(disabledManifest, "cursor")).toBeNull();
|
||||
});
|
||||
|
||||
it("resolveAgentKey resolves an enabled agent normally", () => {
|
||||
expect(resolveAgentKey(disabledManifest, "claude")).toBe("claude");
|
||||
});
|
||||
|
||||
it("checkEntity still works for clouds even when agents are disabled", () => {
|
||||
expect(checkEntity(disabledManifest, "sprite", "cloud")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Manifest with overlapping key names ────────────────────────────────
|
||||
|
||||
describe("manifest with overlapping patterns", () => {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ export function findClosestKeyByNameOrKey(
|
|||
function resolveEntityKey(manifest: Manifest, input: string, kind: "agent" | "cloud"): string | null {
|
||||
const collection = getEntityCollection(manifest, kind);
|
||||
if (collection[input]) {
|
||||
if (kind === "agent" && manifest.agents[input].disabled) {
|
||||
return null;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
const keys = getEntityKeys(manifest, kind);
|
||||
|
|
@ -285,6 +288,13 @@ export function checkEntity(manifest: Manifest, value: string, kind: "agent" | "
|
|||
const def = ENTITY_DEFS[kind];
|
||||
const collection = getEntityCollection(manifest, kind);
|
||||
if (collection[value]) {
|
||||
if (kind === "agent" && manifest.agents[value].disabled) {
|
||||
p.log.error(`${pc.bold(manifest.agents[value].name)} is temporarily disabled.`);
|
||||
if (manifest.agents[value].disabled_reason) {
|
||||
p.log.info(manifest.agents[value].disabled_reason);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ async function setupCursorConfig(runner: CloudRunner, _apiKey: string): Promise<
|
|||
'spawn <agent> <cloud> --headless --output json --prompt "task description"',
|
||||
"```",
|
||||
"",
|
||||
"## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie, cursor",
|
||||
"## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie",
|
||||
"## Clouds: hetzner, digitalocean, aws, gcp, sprite",
|
||||
"",
|
||||
"The command returns JSON with connection details. Use this to delegate subtasks",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ You have the \`spawn\` CLI installed. Use it to provision cloud VMs with AI agen
|
|||
spawn <agent> <cloud> --headless --output json --prompt "task description"
|
||||
\`\`\`
|
||||
|
||||
## Agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie
|
||||
## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie
|
||||
## Clouds: hetzner, digitalocean, aws, gcp, sprite
|
||||
|
||||
## Example
|
||||
|
|
@ -51,7 +51,7 @@ ssh -o StrictHostKeyChecking=accept-new root@<ip_address> '~/.local/bin/claude -
|
|||
\`\`\`
|
||||
|
||||
### Known gotchas — do NOT waste time rediscovering these:
|
||||
- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/cursor\`, etc.
|
||||
- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/codex\`, etc.
|
||||
- Claude Code blocks \`--dangerously-skip-permissions\` as root. Use \`--print\` for one-shot prompts instead — it works as root and returns the response directly.
|
||||
- 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\`.
|
||||
- For long-running tasks, use \`spawn delete --name <name> --yes\` to clean up when done.
|
||||
|
|
@ -76,7 +76,7 @@ const HERMES_SNIPPET = `
|
|||
|
||||
You have the \`spawn\` CLI for creating child cloud VMs with AI agents.
|
||||
Use \`spawn <agent> <cloud> --headless --output json --prompt "task"\` to delegate work.
|
||||
Available agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie.
|
||||
Available agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie.
|
||||
Cloud credentials are pre-configured. Run \`spawn list --json\` to see children.
|
||||
Note: --headless only provisions the VM. To run a prompt, SSH in: \`ssh root@<ip> '~/.local/bin/<agent> --print "prompt"'\`.
|
||||
`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue