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
20 changes: 10 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# --- AI provider (required) ---
# deepsec dispatches AI agents to investigate candidates. Two backends are
# supported, both routed through Vercel AI Gateway by default.
# --- AI provider (required for full scans) ---
# Recommended: use one Vercel AI Gateway key for both supported backends.
AI_GATEWAY_API_KEY=

# Claude Agent SDK (default backend, --agent claude-agent-sdk)
ANTHROPIC_AUTH_TOKEN=
ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh
# Direct Anthropic fallback for the Claude Agent SDK (--agent claude-agent-sdk).
# Leave these unset when using AI_GATEWAY_API_KEY or local `claude` login.
ANTHROPIC_API_KEY=
ANTHROPIC_BASE_URL=

# OpenAI Codex SDK (--agent codex). Optional; the gateway accepts the
# Anthropic token at /v1/chat/completions too, so you can leave OPENAI_*
# unset and Codex will fall back to ANTHROPIC_AUTH_TOKEN.
# OpenAI Codex SDK (--agent codex). Leave these unset when using
# AI_GATEWAY_API_KEY or local `codex` login.
OPENAI_API_KEY=
OPENAI_BASE_URL=https://ai-gateway.vercel.sh/v1
OPENAI_BASE_URL=
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ AI_GATEWAY_API_KEY=vck_...

See [docs/vercel-setup.md](docs/vercel-setup.md) for getting a key and
for the Vercel Sandbox setup. To bypass the gateway, set
`ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL` (or the OpenAI pair)
explicitly. Explicit values always win over the `AI_GATEWAY_API_KEY`
`ANTHROPIC_API_KEY` (plus `ANTHROPIC_BASE_URL=https://api.anthropic.com`
only when you need an explicit direct Anthropic base URL) or the OpenAI
pair explicitly. Explicit values always win over the `AI_GATEWAY_API_KEY`
expansion.

If a `process` or `revalidate` run halts because the upstream credential
Expand Down
5 changes: 3 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ backend you're using.
| Var | Used by | Purpose |
|---|---|---|
| `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. |
| `ANTHROPIC_AUTH_TOKEN` | `process`, `revalidate`, `triage` (Claude backend) | Gateway-routed token for the Claude Agent SDK. Usually filled from `AI_GATEWAY_API_KEY`; set explicitly only for gateway-compatible proxies that expect this variable. |
| `ANTHROPIC_API_KEY` | same | Direct Anthropic API key. Set this, not `ANTHROPIC_AUTH_TOKEN`, when bypassing the gateway and talking to Anthropic directly. |
| `ANTHROPIC_BASE_URL` | same | Default (when `AI_GATEWAY_API_KEY` is set): `https://ai-gateway.vercel.sh`. Optional for direct Anthropic; set to `https://api.anthropic.com` when you need the base URL to be explicit. |

### Optional

Expand Down
5 changes: 2 additions & 3 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,11 @@ the gateway.

```bash
# Direct Anthropic
ANTHROPIC_AUTH_TOKEN=sk-ant-...
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_BASE_URL=https://api.anthropic.com

# AI Gateway (recommended)
ANTHROPIC_AUTH_TOKEN=vck_...
ANTHROPIC_BASE_URL=https://ai-gateway.vercel.sh
AI_GATEWAY_API_KEY=vck_...
```

If `claude` or `codex` is already logged in on this machine, non-sandbox
Expand Down
5 changes: 3 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ Open `.env.local` and pick one of:
expires after 12 hours; re-pull when you hit auth errors. Convenient
if you're already using Vercel Sandbox (same token unlocks both).

Prefer Anthropic directly? Set `ANTHROPIC_AUTH_TOKEN=sk-ant-…` and
`ANTHROPIC_BASE_URL=https://api.anthropic.com` instead. If `claude` or
Prefer Anthropic directly? Set `ANTHROPIC_API_KEY=sk-ant-…` instead.
Add `ANTHROPIC_BASE_URL=https://api.anthropic.com` only if you need the
base URL to be explicit. If `claude` or
`codex` is already logged in on this machine, non-sandbox runs
(`process` / `revalidate` / `triage`) skip the token and reuse the
subscription. See [vercel-setup.md](vercel-setup.md).
Expand Down
8 changes: 4 additions & 4 deletions docs/vercel-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ If the second command fails with `Missing AI credentials` or a `401`, see [Troub

### How it works

deepsec expands whichever credential it finds (the API key first, the OIDC token as fallback) at startup into the four vars the agent SDKs read (`ANTHROPIC_AUTH_TOKEN`, `OPENAI_API_KEY`, `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`), so a single credential covers both Codex (`--agent codex`, the default) and Claude (`--agent claude`).
deepsec expands whichever gateway credential it finds (the API key first, the OIDC token as fallback) at startup into the four vars the agent SDKs read (`ANTHROPIC_AUTH_TOKEN`, `OPENAI_API_KEY`, `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`), so a single gateway credential covers both Codex (`--agent codex`, the default) and Claude (`--agent claude`). For direct Anthropic, set `ANTHROPIC_API_KEY` instead.

Any of those four vars you set explicitly takes precedence over the expansion — useful for mixing direct Anthropic with gateway-routed OpenAI, etc.

Expand Down Expand Up @@ -113,19 +113,19 @@ If you have your own Anthropic / OpenAI agreement, two options:

**Through the gateway (recommended)** — configure [Bring Your Own Key (BYOK)](https://vercel.com/docs/ai-gateway/authentication-and-byok#bring-your-own-key-byok) at the team level. No gateway markup, with failover and observability on top.

**Bypass the gateway entirely** — set the explicit base URL + token pairs in `.env.local`:
**Bypass the gateway entirely** — set direct provider credentials in `.env.local`:

```bash
# Anthropic direct
ANTHROPIC_AUTH_TOKEN=sk-ant-…
ANTHROPIC_API_KEY=sk-ant-…
ANTHROPIC_BASE_URL=https://api.anthropic.com

# OpenAI direct
OPENAI_API_KEY=sk-…
# (OPENAI_BASE_URL defaults to api.openai.com — only set it for proxies)
```

Mix freely — gateway for Claude, direct for OpenAI, etc. The explicit values always win over the `AI_GATEWAY_API_KEY` expansion.
Mix freely — gateway for Claude, direct Anthropic, direct OpenAI, etc. The explicit values always win over the `AI_GATEWAY_API_KEY` expansion.

---

Expand Down
15 changes: 15 additions & 0 deletions packages/deepsec/src/__tests__/credential-brokering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildSandboxEnv, resolveBrokeredCredentials } from "../sandbox/setup.js

const TOUCHED_KEYS = [
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
Expand Down Expand Up @@ -34,6 +35,20 @@ describe("credential brokering", () => {
expect(c.openaiToken).toBeUndefined();
});

it("captures direct ANTHROPIC_API_KEY from the orchestrator env", () => {
process.env.ANTHROPIC_API_KEY = "sk-ant-real";
const c = resolveBrokeredCredentials("claude-agent-sdk");
expect(c.anthropicToken).toBe("sk-ant-real");
expect(c.openaiToken).toBeUndefined();
});

it("prefers ANTHROPIC_AUTH_TOKEN over direct ANTHROPIC_API_KEY", () => {
process.env.ANTHROPIC_AUTH_TOKEN = "vck_real";
process.env.ANTHROPIC_API_KEY = "sk-ant-real";
const c = resolveBrokeredCredentials("claude-agent-sdk");
expect(c.anthropicToken).toBe("vck_real");
});

it("falls back to ANTHROPIC token for OpenAI on the codex path", () => {
process.env.ANTHROPIC_AUTH_TOKEN = "vck_real";
const c = resolveBrokeredCredentials("codex");
Expand Down
9 changes: 8 additions & 1 deletion 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,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_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.ANTHROPIC_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 @@ -65,9 +67,14 @@ describe("assertAgentCredential", () => {
expect(() => assertAgentCredential("claude-agent-sdk")).not.toThrow();
});

it("passes for claude-agent-sdk when direct Anthropic API key is set", () => {
process.env.ANTHROPIC_API_KEY = "x";
expect(() => assertAgentCredential("claude-agent-sdk")).not.toThrow();
});

it("throws actionable message for claude-agent-sdk when no token and no claude CLI", () => {
expect(() => assertAgentCredential("claude-agent-sdk")).toThrow(/--agent claude/);
expect(() => assertAgentCredential("claude-agent-sdk")).toThrow(/ANTHROPIC_AUTH_TOKEN/);
expect(() => assertAgentCredential("claude-agent-sdk")).toThrow(/ANTHROPIC_API_KEY/);
expect(() => assertAgentCredential("claude-agent-sdk")).toThrow(/AI_GATEWAY_API_KEY/);
expect(() => assertAgentCredential("claude-agent-sdk")).toThrow(
/https:\/\/github\.com\/vercel-labs\/deepsec\/blob\/main\/docs\/vercel-setup\.md/,
Expand Down
5 changes: 3 additions & 2 deletions packages/deepsec/src/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export function assertAgentCredential(
if (agentType !== undefined && !KNOWN_BACKENDS.has(agentType)) return;

const anthropic = process.env.ANTHROPIC_AUTH_TOKEN;
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
const openai = process.env.OPENAI_API_KEY;

if (isCodex(agentType)) {
Expand All @@ -177,14 +178,14 @@ export function assertAgentCredential(
);
}

if (anthropic) return;
if (anthropic || anthropicApiKey) return;
if (!options.inSandbox && hasLocalClaudeAgent()) return;
const displayAgent =
agentType === "claude-agent-sdk" || agentType === undefined ? "claude" : agentType;
throw new Error(
`Missing AI credentials for --agent ${displayAgent}.\n` +
`\n` +
` Add to .env.local: AI_GATEWAY_API_KEY=vck_… (or ANTHROPIC_AUTH_TOKEN=…)\n` +
` Add to .env.local: AI_GATEWAY_API_KEY=vck_… (or ANTHROPIC_API_KEY=… for direct Anthropic)\n` +
` Setup: ${SETUP_DOC_URL}`,
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/deepsec/src/sandbox/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface UploadBundle {
// --- Sandbox env vars ---

// Routing / debug knobs the agent reads inside the sandbox. AI credential
// env vars (ANTHROPIC_AUTH_TOKEN, OPENAI_API_KEY, AI_GATEWAY_API_KEY) are
// env vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, OPENAI_API_KEY, AI_GATEWAY_API_KEY) are
// deliberately absent — they're brokered via firewall header injection
// (see resolveBrokeredCredentials + buildWorkerNetworkPolicy below) so a
// compromised in-VM agent can't read them out of /proc/<pid>/environ.
Expand Down Expand Up @@ -66,11 +66,11 @@ interface BrokeredCredentials {
/**
* Resolve the orchestrator-side AI credentials that will be brokered into
* the sandbox. AI Gateway issues one token per team that authenticates both
* Claude and OpenAI traffic, so when only ANTHROPIC_AUTH_TOKEN is set and
* the worker is going to run codex, fall it back as the OpenAI token.
* Claude and OpenAI traffic, so when only an Anthropic token is set and the
* worker is going to run codex, fall it back as the OpenAI token.
*/
export function resolveBrokeredCredentials(agentType: string | undefined): BrokeredCredentials {
const anthropicToken = process.env.ANTHROPIC_AUTH_TOKEN;
const anthropicToken = process.env.ANTHROPIC_AUTH_TOKEN ?? process.env.ANTHROPIC_API_KEY;
const explicitOpenai = process.env.OPENAI_API_KEY;
// Only borrow ANTHROPIC for OPENAI on the codex path — and only when the
// user hasn't pinned an explicit OpenAI key. Outside codex this fallback
Expand Down