Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 6 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,16 @@ built-ins by reusing the same slug.
- **Outputs:** FileRecord `findings[]` populated, `status: "analyzed"`,
`analysisHistory[]` appended.

Two agent backends are supported, both routed through Vercel AI Gateway
by default:
Three agent backends are supported:

| `--agent` | SDK | Default model |
| `--agent` | Runtime | Default model |
|---|---|---|
| `codex` (default) | `@openai/codex-sdk` | `gpt-5.5` |
| `claude` | `@anthropic-ai/claude-agent-sdk` | `claude-opus-4-7` |
| `cursor` | Cursor Agent CLI (`agent`) | `composer-2.5` |

`codex` and `claude` route through Vercel AI Gateway by default; `cursor`
uses Cursor credentials (`CURSOR_API_KEY` or `agent login`).

Same prompt, same JSON output schema. You can mix backends within a
project — re-process a file with a different agent and the second run's
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ see [`samples/webapp/deepsec.config.ts`](../samples/webapp/deepsec.config.ts).
| `projects` | `ProjectDeclaration[]` | The codebases deepsec knows about. |
| `plugins` | `DeepsecPlugin[]` | Loaded in order; later plugins override single-slot capabilities. |
| `matchers` | `{ only?: string[]; exclude?: string[] }` | Filter the matcher set used by `scan`. |
| `defaultAgent` | `string` | Default `--agent` value (`codex` or `claude`). See [models.md](models.md). |
| `defaultAgent` | `string` | Default `--agent` value (`codex`, `claude`, or `cursor`). See [models.md](models.md). |
| `dataDir` | `string` | Override the `data/` directory. Defaults to `./data`. |

## ProjectDeclaration
Expand Down
13 changes: 11 additions & 2 deletions docs/models.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# Models

deepsec talks to LLMs through two interchangeable backends:
deepsec talks to LLMs through interchangeable agent backends:

| Backend | Default model | Used by |
|-----------------------------|-----------------------|------------------------------|
| `codex` (default) | `gpt-5.5` | `process`, `revalidate` |
| `claude` | `claude-opus-4-7` | `process`, `revalidate` |
| `cursor` | `composer-2.5` | `process`, `revalidate` |
| `claude` (triage) | `claude-sonnet-4-6` | `triage` (Claude-only) |

Both backends route through [Vercel AI Gateway](https://vercel.com/ai-gateway)
`codex` and `claude` route through [Vercel AI Gateway](https://vercel.com/ai-gateway)
by default, so a single token covers Claude **and** Codex. To use
Anthropic or OpenAI directly, point `ANTHROPIC_BASE_URL` /
`OPENAI_BASE_URL` at the provider.

`cursor` uses the [Cursor Agent CLI](https://cursor.com/docs) (`agent` on
PATH). Set `CURSOR_API_KEY` in `.env.local`, or run `agent login` on the
host. List models with `agent --list-models`.

## CLI selection

```bash
Expand All @@ -28,6 +33,10 @@ pnpm deepsec process --project-id my-app --agent codex
# Codex backend, specific model:
pnpm deepsec process --project-id my-app --agent codex --model gpt-5.4

# Cursor Agent CLI (local `agent` binary or CURSOR_API_KEY):
pnpm deepsec process --project-id my-app --agent cursor
pnpm deepsec process --project-id my-app --agent cursor --model gpt-5.3-codex-high

# Triage uses Claude; pass a cheaper model if you want:
pnpm deepsec triage --project-id my-app --model claude-haiku-4-5
```
Expand Down
23 changes: 23 additions & 0 deletions packages/deepsec/src/__tests__/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ describe("assertAgentCredential", () => {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
CLAUDE_HOME: process.env.CLAUDE_HOME,
CODEX_HOME: process.env.CODEX_HOME,
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
PATH: process.env.PATH,
};
delete process.env.ANTHROPIC_AUTH_TOKEN;
delete process.env.OPENAI_API_KEY;
delete process.env.CURSOR_API_KEY;
// Point CLAUDE_HOME / CODEX_HOME and PATH at empty tmp dirs so the
// suite is hermetic — the dev running tests may have a real
// ~/.codex/auth.json or `claude` on $PATH, which would cause
Expand Down Expand Up @@ -115,6 +117,27 @@ describe("assertAgentCredential", () => {
expect(() => assertAgentCredential("codex")).toThrow(/AI_GATEWAY_API_KEY/);
});

it("passes for cursor when CURSOR_API_KEY is set", () => {
process.env.CURSOR_API_KEY = "x";
expect(() => assertAgentCredential("cursor")).not.toThrow();
});

it("passes for cursor when `agent` is on PATH (login mode)", () => {
writeFileSync(join(emptyPathDir, "agent"), "#!/bin/sh\n", { mode: 0o755 });
expect(() => assertAgentCredential("cursor")).not.toThrow();
});

it("throws actionable message for cursor when no key and no agent CLI", () => {
expect(() => assertAgentCredential("cursor")).toThrow(/--agent cursor/);
expect(() => assertAgentCredential("cursor")).toThrow(/CURSOR_API_KEY/);
expect(() => assertAgentCredential("cursor")).toThrow(/agent login/);
});

it("ignores cursor login auth in sandbox mode", () => {
writeFileSync(join(emptyPathDir, "agent"), "#!/bin/sh\n", { mode: 0o755 });
expect(() => assertAgentCredential("cursor", { inSandbox: true })).toThrow(/CURSOR_API_KEY/);
});

it("skips the credential check for custom plugin agents", () => {
// Tests use this so a stub agent registered via plugins[] doesn't
// require fake ANTHROPIC_AUTH_TOKEN env vars.
Expand Down
4 changes: 4 additions & 0 deletions packages/deepsec/src/__tests__/resolve-agent-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ describe("resolveAgentType", () => {
setLoadedConfig(defineConfig({ projects: [] }));
expect(resolveAgentType(undefined)).toBe("codex");
});

it("passes cursor through unchanged", () => {
expect(resolveAgentType("cursor")).toBe("cursor");
});
});
2 changes: 2 additions & 0 deletions packages/deepsec/src/agent-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export function defaultModelForAgent(agentType: string): string {
switch (agentType) {
case "codex":
return "gpt-5.5";
case "cursor":
return "composer-2.5";
default:
return "claude-opus-4-7";
}
Expand Down
8 changes: 4 additions & 4 deletions packages/deepsec/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ program
.option("--run-id <id>", "Resume a specific processing run")
.option(
"--agent <type>",
"Agent plugin type: codex or claude (default: defaultAgent in deepsec.config.ts, else codex)",
"Agent plugin type: codex, claude, or cursor (default: defaultAgent in deepsec.config.ts, else codex)",
)
.option(
"--model <model>",
"Model to use (default: claude-opus-4-7 for claude, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-7 for claude, gpt-5.5 for codex, composer-2.5 for cursor)",
)
.option("--max-turns <n>", "Max conversation turns per batch (default: 150)", parseInt)
.option(
Expand Down Expand Up @@ -192,11 +192,11 @@ program
.option("--run-id <id>", "Resume a specific revalidation run")
.option(
"--agent <type>",
"Agent plugin type: codex or claude (default: defaultAgent in deepsec.config.ts, else codex)",
"Agent plugin type: codex, claude, or cursor (default: defaultAgent in deepsec.config.ts, else codex)",
)
.option(
"--model <model>",
"Model to use (default: claude-opus-4-7 for claude, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-7 for claude, gpt-5.5 for codex, composer-2.5 for cursor)",
)
.option("--max-turns <n>", "Max conversation turns per batch (default: 150)", parseInt)
.option(
Expand Down
22 changes: 21 additions & 1 deletion packages/deepsec/src/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ function isCodex(agentType: string | undefined): boolean {
return agentType === "codex";
}

function isCursor(agentType: string | undefined): boolean {
return agentType === "cursor";
}

function hasLocalCursorAgent(): boolean {
return whichSync("agent");
}

/**
* Walk `$PATH` looking for a binary. Used as a positive signal that an
* agent CLI (`claude`, `codex`) is set up on this host — if it's
Expand Down Expand Up @@ -138,7 +146,7 @@ function hasLocalCodexAgent(): boolean {
// via plugins (deepsec.config.ts → plugins: [{ agents: [...] }]) handle
// their own credential resolution, so we skip the check for anything
// other than these.
const KNOWN_BACKENDS = new Set<string>(["claude-agent-sdk", "codex"]);
const KNOWN_BACKENDS = new Set<string>(["claude-agent-sdk", "codex", "cursor"]);

/**
* Verify the orchestrator has an AI credential the chosen agent can use.
Expand Down Expand Up @@ -177,6 +185,18 @@ export function assertAgentCredential(
);
}

if (isCursor(agentType)) {
if (process.env.CURSOR_API_KEY) return;
if (!options.inSandbox && hasLocalCursorAgent()) return;
throw new Error(
`Missing AI credentials for --agent cursor.\n` +
`\n` +
` Add to .env.local: CURSOR_API_KEY=…\n` +
` Or install the Cursor CLI and run \`agent login\` on this host.\n` +
` Setup: ${SETUP_DOC_URL}`,
);
}

if (anthropic) return;
if (!options.inSandbox && hasLocalClaudeAgent()) return;
const displayAgent =
Expand Down
5 changes: 3 additions & 2 deletions packages/processor/src/__tests__/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ describe("AgentRegistry", () => {
});

describe("createDefaultAgentRegistry", () => {
it("registers both backends", () => {
it("registers built-in backends", () => {
const registry = createDefaultAgentRegistry();
expect(registry.get("claude-agent-sdk")).toBeDefined();
expect(registry.get("codex")).toBeDefined();
expect(registry.types().sort()).toEqual(["claude-agent-sdk", "codex"]);
expect(registry.get("cursor")).toBeDefined();
expect(registry.types().sort()).toEqual(["claude-agent-sdk", "codex", "cursor"]);
});
});
Loading