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
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 (`claude-agent-sdk` or `codex`). See [models.md](models.md). |
| `defaultAgent` | `string` | Default `--agent` value (`claude-agent-sdk`, `codex`, or `acp`). See [models.md](models.md). |
| `dataDir` | `string` | Override the `data/` directory. Defaults to `./data`. |

## ProjectDeclaration
Expand Down
10 changes: 9 additions & 1 deletion docs/models.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Models

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

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

Both backends route through [Vercel AI Gateway](https://vercel.com/ai-gateway)
Expand All @@ -28,6 +29,13 @@ 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

# ACP backend, registry agent from https://agentclientprotocol.com:
pnpm deepsec process --project-id my-app --agent acp --acp-registry-agent claude-acp

# ACP backend, fully custom bridge command/args:
pnpm deepsec process --project-id my-app --agent acp --acp-command 'my-acp serve --stdio'
pnpm deepsec process --project-id my-app --agent acp --acp-command my-acp --acp-args '["serve","--stdio"]'

# Triage uses Claude; pass a cheaper model if you want:
pnpm deepsec triage --project-id my-app --model claude-haiku-4-5
```
Expand Down
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"ignore": ["fixtures/vulnerable-app/**", "samples/**"],
"ignoreDependencies": [
"@agentclientprotocol/sdk",
"@anthropic-ai/claude-agent-sdk",
"@openai/codex",
"@openai/codex-sdk",
Expand Down
1 change: 1 addition & 0 deletions packages/deepsec/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const repoRoot = resolve(__dirname, "../..");
// Externalized at runtime: native binaries, heavy SDKs, and jiti (which
// bundles its own esbuild — re-bundling it produces broken output).
const external = [
"@agentclientprotocol/sdk",
"@anthropic-ai/claude-agent-sdk",
"@openai/codex",
"@openai/codex-sdk",
Expand Down
1 change: 1 addition & 0 deletions packages/deepsec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"prepublishOnly": "pnpm -w validate && node -e \"const fs=require('node:fs');for(const f of ['dist/cli.mjs','dist/config.mjs','dist/config.d.ts','dist/sandbox/request-proxy.mjs','dist/docs/getting-started.md','dist/samples/webapp/deepsec.config.ts','SKILL.md','README.md','LICENSE','NOTICE']){if(!fs.existsSync(f)){console.error('Missing: '+f);process.exit(1)}}console.log('Pre-publish checks passed.')\""
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.21.0",
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
"@openai/codex": "^0.125.0",
"@openai/codex-sdk": "^0.125.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/deepsec/src/__tests__/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ describe("assertAgentCredential", () => {
expect(() => assertAgentCredential("codex")).toThrow(/AI_GATEWAY_API_KEY/);
});

it("skips Deepsec-managed credential checks for ACP agents", () => {
// ACP bridges own their auth flow, so missing Claude/OpenAI env vars should not block --agent acp.
expect(() => assertAgentCredential("acp")).not.toThrow();
expect(() => assertAgentCredential("acp", { inSandbox: true })).not.toThrow();
});

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
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 "acp":
return "acp-default";
default:
return "claude-opus-4-7";
}
Expand Down
28 changes: 24 additions & 4 deletions packages/deepsec/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,21 @@ program
.option("--run-id <id>", "Resume a specific processing run")
.option(
"--agent <type>",
"Agent plugin type: claude-agent-sdk or codex (default: defaultAgent in deepsec.config.ts, else claude-agent-sdk)",
"Agent plugin type: claude-agent-sdk, codex, or acp (default: defaultAgent in deepsec.config.ts, else claude-agent-sdk)",
)
.option(
"--model <model>",
"Model to use (default: claude-opus-4-7 for claude-agent-sdk, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-7 for claude-agent-sdk, gpt-5.5 for codex, acp-default for acp)",
)
.option("--acp-registry-agent <id>", "ACP registry agent id, e.g. claude-acp or codex-acp")
.option("--acp-registry-url <url>", "ACP registry URL (default: public latest registry)")
.option(
"--acp-command <cmd>",
"Custom ACP bridge command; may include args when --acp-args is omitted",
)
.option(
"--acp-args <args>",
"Custom ACP bridge args as JSON array or whitespace string; used with --acp-command",
)
.option("--max-turns <n>", "Max conversation turns per batch (default: 150)", parseInt)
.option(
Expand Down Expand Up @@ -182,11 +192,21 @@ program
.option("--run-id <id>", "Resume a specific revalidation run")
.option(
"--agent <type>",
"Agent plugin type: claude-agent-sdk or codex (default: defaultAgent in deepsec.config.ts, else claude-agent-sdk)",
"Agent plugin type: claude-agent-sdk, codex, or acp (default: defaultAgent in deepsec.config.ts, else claude-agent-sdk)",
)
.option(
"--model <model>",
"Model to use (default: claude-opus-4-7 for claude-agent-sdk, gpt-5.5 for codex)",
"Model to use (default: claude-opus-4-7 for claude-agent-sdk, gpt-5.5 for codex, acp-default for acp)",
)
.option("--acp-registry-agent <id>", "ACP registry agent id, e.g. claude-acp or codex-acp")
.option("--acp-registry-url <url>", "ACP registry URL (default: public latest registry)")
.option(
"--acp-command <cmd>",
"Custom ACP bridge command; may include args when --acp-args is omitted",
)
.option(
"--acp-args <args>",
"Custom ACP bridge args as JSON array or whitespace string; used with --acp-command",
)
.option("--max-turns <n>", "Max conversation turns per batch (default: 150)", parseInt)
.option(
Expand Down
43 changes: 42 additions & 1 deletion packages/deepsec/src/commands/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,46 @@ function parseCsv(v: string | undefined): string[] | undefined {
return parts.length > 0 ? parts : undefined;
}

function parseAcpArgs(v: string | undefined): string[] | undefined {
if (!v) return undefined;
const trimmed = v.trim();
if (!trimmed) return undefined;
if (trimmed.startsWith("[")) {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed)) throw new Error("--acp-args JSON must be an array of strings");
return parsed.map(String);
}
return trimmed.split(/\s+/).filter(Boolean);
}

function buildAgentConfig(opts: {
model: string;
maxTurns?: number;
acpRegistryAgent?: string;
acpRegistryUrl?: string;
acpCommand?: string;
acpArgs?: string;
}): Record<string, unknown> {
return {
model: opts.model,
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
...(opts.acpRegistryAgent ? { acpRegistryAgent: opts.acpRegistryAgent } : {}),
...(opts.acpRegistryUrl ? { acpRegistryUrl: opts.acpRegistryUrl } : {}),
...(opts.acpCommand ? { acpCommand: opts.acpCommand } : {}),
...(opts.acpArgs ? { acpArgs: parseAcpArgs(opts.acpArgs) } : {}),
};
}

export async function processCommand(opts: {
projectId?: string;
runId?: string;
agent?: string;
model?: string;
maxTurns?: number;
acpRegistryAgent?: string;
acpRegistryUrl?: string;
acpCommand?: string;
acpArgs?: string;
/** Commander yields `true` when bare; string (unparsed) when an arg is provided */
reinvestigate?: boolean | string;
limit?: number;
Expand Down Expand Up @@ -135,7 +169,14 @@ export async function processCommand(opts: {
projectId,
runId: opts.runId,
agentType,
config: { model, ...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}) },
config: buildAgentConfig({
model,
maxTurns: opts.maxTurns,
acpRegistryAgent: opts.acpRegistryAgent,
acpRegistryUrl: opts.acpRegistryUrl,
acpCommand: opts.acpCommand,
acpArgs: opts.acpArgs,
}),
reinvestigate,
limit: opts.limit,
concurrency: opts.concurrency,
Expand Down
43 changes: 42 additions & 1 deletion packages/deepsec/src/commands/revalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,46 @@ function parseCsv(v: string | undefined): string[] | undefined {
return parts.length > 0 ? parts : undefined;
}

function parseAcpArgs(v: string | undefined): string[] | undefined {
if (!v) return undefined;
const trimmed = v.trim();
if (!trimmed) return undefined;
if (trimmed.startsWith("[")) {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed)) throw new Error("--acp-args JSON must be an array of strings");
return parsed.map(String);
}
return trimmed.split(/\s+/).filter(Boolean);
}

function buildAgentConfig(opts: {
model: string;
maxTurns?: number;
acpRegistryAgent?: string;
acpRegistryUrl?: string;
acpCommand?: string;
acpArgs?: string;
}): Record<string, unknown> {
return {
model: opts.model,
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
...(opts.acpRegistryAgent ? { acpRegistryAgent: opts.acpRegistryAgent } : {}),
...(opts.acpRegistryUrl ? { acpRegistryUrl: opts.acpRegistryUrl } : {}),
...(opts.acpCommand ? { acpCommand: opts.acpCommand } : {}),
...(opts.acpArgs ? { acpArgs: parseAcpArgs(opts.acpArgs) } : {}),
};
}

export async function revalidateCommand(opts: {
projectId?: string;
runId?: string;
agent?: string;
model?: string;
maxTurns?: number;
acpRegistryAgent?: string;
acpRegistryUrl?: string;
acpCommand?: string;
acpArgs?: string;
minSeverity?: string;
force?: boolean;
limit?: number;
Expand Down Expand Up @@ -92,7 +126,14 @@ export async function revalidateCommand(opts: {
projectId,
runId: opts.runId,
agentType,
config: { model, ...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}) },
config: buildAgentConfig({
model,
maxTurns: opts.maxTurns,
acpRegistryAgent: opts.acpRegistryAgent,
acpRegistryUrl: opts.acpRegistryUrl,
acpCommand: opts.acpCommand,
acpArgs: opts.acpArgs,
}),
minSeverity,
force: opts.force,
limit: opts.limit,
Expand Down
9 changes: 8 additions & 1 deletion packages/deepsec/src/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ function isCodex(agentType: string | undefined): boolean {
return agentType === "codex";
}

function isAcp(agentType: string | undefined): boolean {
return agentType === "acp";
}

/**
* 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 @@ -107,7 +111,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", "acp"]);

/**
* Verify the orchestrator has an AI credential the chosen agent can use.
Expand All @@ -129,6 +133,9 @@ export function assertAgentCredential(
options: { inSandbox?: boolean } = {},
): void {
if (agentType !== undefined && !KNOWN_BACKENDS.has(agentType)) return;
// ACP agents own their authentication/credential flow behind the selected ACP bridge.
// Deepsec just connects to the bridge and should not require Claude/OpenAI env vars.
if (isAcp(agentType)) return;

const anthropic = process.env.ANTHROPIC_AUTH_TOKEN;
const openai = process.env.OPENAI_API_KEY;
Expand Down
1 change: 1 addition & 0 deletions packages/processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "tsc"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.21.0",
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
"@openai/codex": "^0.125.0",
"@openai/codex-sdk": "^0.125.0",
Expand Down
Loading