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
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,17 @@ If you feel like the `deepsec` should look at more parts of the code, give it [t

## AI provider

When running locally, `deepsec` falls back to your existing `claude` /
`codex` subscription if you've logged in on this machine. Subscriptions
(Claude Pro/Max, ChatGPT Plus) are useful for evaluating deepsec but
generally don't have enough headroom for full repo scans.
When running locally, `deepsec` supports three built-in AI backends:

- `claude` / `claude-agent-sdk`
- `codex`
- `cursor`

`claude` and `codex` can route through Vercel AI Gateway, and both can
also reuse a local CLI login on your machine (`claude`, `codex`) for
evaluation workflows. Subscriptions (Claude Pro/Max, ChatGPT Plus) are
useful for trying deepsec but generally don't have enough headroom for
full repo scans.

For real scans, use Vercel AI Gateway. One key covers both Claude and
Codex, and the gateway's default quotas are sized for highly concurrent
Expand All @@ -86,6 +93,24 @@ for the Vercel Sandbox setup. To bypass the gateway, set
explicitly. Explicit values always win over the `AI_GATEWAY_API_KEY`
expansion.

Cursor uses the Cursor SDK directly in local mode:

```
CURSOR_API_KEY=cursor_...
```

Its built-in default model is `composer-2.5` with Cursor's `fast`
variant disabled. For the Cursor backend, `--model` accepts raw Cursor
model ids, raw aliases, and friendly suffix slugs like
`gpt-5.4-high` or `gpt-5.4-high-1m`, which deepsec resolves against your
account's `Cursor.models.list()` catalog. Reasoning slugs do not
implicitly opt you into large context tiers; use an explicit context
option like `1m`, either alone (`gpt-5.4-1m`) or combined with other
options (`gpt-5.4-high-1m`), when you want that. Because that catalog is account-specific,
a slug that works for one user may not exist for another. Cursor support
is local-only in this release; `sandbox ... --agent cursor` is not
supported yet.

If a `process` or `revalidate` run halts because the upstream credential
ran out of quota or credits, deepsec stops gracefully and tells you
where to top up. Re-run the same command afterward and it picks up
Expand Down
3 changes: 2 additions & 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 Expand Up @@ -107,6 +107,7 @@ backend you're using.
| `AI_GATEWAY_API_KEY` | all AI commands | Shortcut. Expands at CLI startup into `ANTHROPIC_AUTH_TOKEN` / `OPENAI_API_KEY` / `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` — one key covers both Claude and Codex through Vercel AI Gateway. Any of those four vars set explicitly takes precedence. Falls back to `VERCEL_OIDC_TOKEN` (from `vercel env pull`) when unset, so users already authenticated for Sandbox don't need a separate gateway key. |
| `ANTHROPIC_AUTH_TOKEN` | `process`, `revalidate`, `triage` (Claude backend) | API token for the Claude Agent SDK. AI Gateway-issued or Anthropic-issued. Set this if you don't use `AI_GATEWAY_API_KEY`. |
| `ANTHROPIC_BASE_URL` | same | Default (when `AI_GATEWAY_API_KEY` is set): `https://ai-gateway.vercel.sh`. Set to `https://api.anthropic.com` for direct Anthropic. |
| `CURSOR_API_KEY` | `--agent cursor` | Cursor SDK API key. Required for the built-in Cursor backend. Local runs only; sandbox is not supported. |

### Optional

Expand Down
26 changes: 18 additions & 8 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,28 @@ calibrate before committing to a full pass.

`triage` is ~1¢/finding. `revalidate` is comparable to `process`.

## Should I use Claude or Codex?
## Should I use Claude, Codex, or Cursor?

Both work. Different strengths:

- **Claude (Opus):** strong at reasoning about authorization shapes and
cross-file flows. The default. Most expensive.
- **Codex (gpt-5.5):** runs in a strict sandbox (read-only, no network).
Fast at grep-heavy investigations. Cheaper.
- **Cursor (`composer-2.5`):** runs through the Cursor SDK in local
read-only mode. Good if your team already uses Cursor models and wants
to pass Cursor-supported slugs directly with `--model`.

Mix them. Run Claude first, then re-process unconvincing findings with
`--agent codex --reinvestigate` for a second opinion. Findings dedupe
across agents.
`--agent codex --reinvestigate` or `--agent cursor --reinvestigate` for
a second opinion. Findings dedupe across agents.

## Should I use Vercel AI Gateway or Anthropic directly?
## Should I use Vercel AI Gateway, Cursor, or a direct provider?

Either works. The gateway gives you provider failover, observability,
and zero data retention. One token covers Claude and Codex. For a quick
evaluation, use Anthropic directly. For ongoing production scanning, use
the gateway.
For `claude` and `codex`, the gateway gives you provider failover,
observability, and zero data retention. One token covers both backends.
For a quick evaluation, use Anthropic directly. For ongoing production
scanning, use the gateway.

```bash
# Direct Anthropic
Expand All @@ -76,11 +79,18 @@ ANTHROPIC_BASE_URL=https://api.anthropic.com
# AI Gateway (recommended)
ANTHROPIC_AUTH_TOKEN=vck_...
ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh

# Cursor SDK (local only)
CURSOR_API_KEY=cursor_...
```

If `claude` or `codex` is already logged in on this machine, non-sandbox
runs reuse that subscription — no API key needed.

Cursor is separate from AI Gateway: it uses the Cursor SDK directly,
stays local-only for now, and does not support `deepsec sandbox ...`
yet.

See [vercel-setup.md](vercel-setup.md) for how to get a gateway key
and how to wire up Vercel Sandbox auth (OIDC or access token).

Expand Down
67 changes: 60 additions & 7 deletions docs/models.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
# Models

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

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

Both backends 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 is different: it uses the Cursor SDK directly in local mode and
authenticates with `CURSOR_API_KEY`. It does **not** route through
Vercel AI Gateway today, and sandbox execution is not supported yet.
When deepsec selects its default `composer-2.5` model, it pins Cursor's
`fast=false` variant so the default stays on the standard non-fast tier.

## CLI selection

```bash
Expand All @@ -22,14 +30,26 @@ pnpm deepsec process --project-id my-app
# Claude with a specific model:
pnpm deepsec process --project-id my-app --agent claude --model claude-sonnet-4-6

# Cursor backend, default model:
pnpm deepsec process --project-id my-app --agent cursor

# Cursor backend, raw model id from your Cursor account:
pnpm deepsec process --project-id my-app --agent cursor --model claude-4-sonnet

# Cursor backend, friendly variant slug resolved via Cursor metadata:
pnpm deepsec process --project-id my-app --agent cursor --model gpt-5.4-high

# Codex backend, default model:
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

# Triage uses Claude; pass a cheaper model if you want:
# Triage defaults to Claude; pass a cheaper model if you want:
pnpm deepsec triage --project-id my-app --model claude-haiku-4-5

# Or use Cursor explicitly for triage:
pnpm deepsec triage --project-id my-app --agent cursor --model composer-2.5
```

`--agent` and `--model` are also accepted on `revalidate`. Set the
Expand All @@ -38,7 +58,7 @@ default backend project-wide via `defaultAgent` in

## Why these defaults

### `claude-opus-4-7` for `process` and `revalidate`
### `claude-opus-4-8` for `process` and `revalidate`

Investigating a candidate site is a multi-step reasoning task: trace
control flow, recognize an auth boundary, decide whether input is
Expand All @@ -58,11 +78,44 @@ and cost for that loop. `gpt-5.5-pro` is the most careful Codex
option at significantly higher cost; `gpt-5.4` and below are fine for
follow-up reinvestigation passes.

### `composer-2.5` for the Cursor backend

Cursor runs through the Cursor SDK in local read-only mode. For deepsec's
workflow, `composer-2.5` is the right default: strong reasoning, broad
availability on Cursor accounts, and a stable default path. deepsec also
disables Cursor's `fast` variant for this default, so bare
`composer-2.5` means the standard non-fast tier unless you explicitly
choose another slug.

For Cursor specifically, deepsec accepts three `--model` forms:

1. raw Cursor model ids, like `claude-4-sonnet`
2. raw Cursor aliases, like `gpt`
3. friendly suffix slugs, like `gpt-5.4-high` or `gpt-5.4-high-1m`

Friendly suffix slugs are resolved against your account's live
`Cursor.models.list()` catalog. deepsec first checks exact ids and exact
aliases, then resolves known suffix slugs to a `{ id, params }`
selection. For example, `gpt-5.4-high` becomes the `gpt-5.4` model with
the matching Cursor parameters for the `high` reasoning preset, while
keeping large context windows as explicit opt-ins. If you want the large
context tier, pass a dedicated slug such as `gpt-5.4-1m`, or combine
options directly as `gpt-5.4-high-1m`. deepsec splits the suffix on `-`
and matches each option token against Cursor's discovered parameter
options, so `gpt-5.4-1m-high` also resolves correctly.

Because deepsec does not maintain a hardcoded allowlist here, the source
of truth is your account itself. A slug that works for one user may not
exist for another if their Cursor account lacks that model or variant.
If you're unsure which ids, aliases, or suffix slugs you have, inspect
the SDK's `Cursor.models.list()` output outside of deepsec.

### `claude-sonnet-4-6` for `triage`

Triage buckets findings into P0/P1/P2/skip without re-reading the code
— it just looks at the finding text. That's a cheap task; Opus is
overkill. Sonnet keeps `triage` at ~1¢/finding.
overkill. Sonnet keeps `triage` at ~1¢/finding, so it stays the default
when you don't pass `--agent`.

## Refusals

Expand Down Expand Up @@ -109,7 +162,7 @@ Two small integration points:

1. **The model identifier** — whatever string the provider's SDK
accepts. deepsec passes it through unchanged. No code change needed
to *use* a new model on either backend.
to *use* a new model on Claude, Codex, or Cursor.
2. **Pricing for the cost-per-batch readout.** The Claude Agent SDK
reports cost natively, so new Claude-family models drop in with
zero code changes. Codex doesn't, so add a line to
Expand Down
1 change: 1 addition & 0 deletions packages/deepsec/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const repoRoot = resolve(__dirname, "../..");
// bundles its own esbuild — re-bundling it produces broken output).
const external = [
"@anthropic-ai/claude-agent-sdk",
"@cursor/sdk",
"@openai/codex",
"@openai/codex-sdk",
"@vercel/sandbox",
Expand Down
1 change: 1 addition & 0 deletions packages/deepsec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.158",
"@cursor/sdk": "^1.0.18",
"@openai/codex": "^0.125.0",
"@openai/codex-sdk": "^0.125.0",
"@vercel/oidc": "^3.4.0",
Expand Down
18 changes: 18 additions & 0 deletions packages/deepsec/src/__tests__/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ describe("assertAgentCredential", () => {
beforeEach(() => {
saved = {
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
CLAUDE_HOME: process.env.CLAUDE_HOME,
CODEX_HOME: process.env.CODEX_HOME,
PATH: process.env.PATH,
};
delete process.env.ANTHROPIC_AUTH_TOKEN;
delete process.env.CURSOR_API_KEY;
delete process.env.OPENAI_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
Expand Down Expand Up @@ -91,6 +93,22 @@ describe("assertAgentCredential", () => {
expect(() => assertAgentCredential("codex")).not.toThrow();
});

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

it("throws actionable message for cursor when CURSOR_API_KEY is missing", () => {
expect(() => assertAgentCredential("cursor")).toThrow(/--agent cursor/);
expect(() => assertAgentCredential("cursor")).toThrow(/CURSOR_API_KEY/);
expect(() => assertAgentCredential("cursor")).toThrow(/cursor\.com\/dashboard\/integrations/);
});

it("rejects cursor in sandbox mode even when CURSOR_API_KEY is set", () => {
process.env.CURSOR_API_KEY = "cursor_x";
expect(() => assertAgentCredential("cursor", { inSandbox: true })).toThrow(/local runs only/);
});

it("passes for codex when only ANTHROPIC token is set (gateway fallback)", () => {
process.env.ANTHROPIC_AUTH_TOKEN = "x";
expect(() => assertAgentCredential("codex")).not.toThrow();
Expand Down
9 changes: 9 additions & 0 deletions packages/deepsec/src/__tests__/resolve-agent-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ describe("resolveAgentType", () => {
expect(resolveAgentType(undefined)).toBe("claude-agent-sdk");
});

it("passes cursor through unchanged", () => {
expect(resolveAgentType("cursor")).toBe("cursor");
});

it("falls back to defaultAgent from config when not provided", () => {
setLoadedConfig(defineConfig({ projects: [], defaultAgent: "codex" }));
expect(resolveAgentType(undefined)).toBe("codex");
});

it("falls back to cursor from config when set", () => {
setLoadedConfig(defineConfig({ projects: [], defaultAgent: "cursor" }));
expect(resolveAgentType(undefined)).toBe("cursor");
});

it("falls back to codex when neither is set", () => {
setLoadedConfig(defineConfig({ projects: [] }));
expect(resolveAgentType(undefined)).toBe("codex");
Expand Down
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-8";
}
Expand Down
14 changes: 9 additions & 5 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-8 for claude, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)",
)
.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-8 for claude, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-8 for claude, gpt-5.5 for codex, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)",
)
.option("--max-turns <n>", "Max conversation turns per batch (default: 150)", parseInt)
.option(
Expand Down Expand Up @@ -238,7 +238,11 @@ program
"Project identifier (default: the only project in deepsec.config.ts; required if there are multiple)",
)
.option("--severity <sev>", "Severity to triage (default: MEDIUM)", "MEDIUM")
.option("--model <model>", "Model to use (default: claude-sonnet-4-6 — cheaper)")
.option("--agent <type>", "Triage backend: claude or cursor (default: claude)")
.option(
"--model <model>",
"Model to use (default: claude-sonnet-4-6 for claude, composer-2.5 for cursor; Cursor also accepts aliases and combined option slugs like gpt-5.4-high-1m)",
)
.option("--force", "Re-triage already-triaged findings")
.option("--limit <n>", "Max findings to triage", parseInt)
.option("--concurrency <n>", "Parallel triage batches (default: cores - 1)", parseInt)
Expand Down
Loading