diff --git a/.env.example b/.env.example index a6768c5..664e97b 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/README.md b/README.md index 659e489..8403b3d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/configuration.md b/docs/configuration.md index e0cb6ba..b8ef651 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 diff --git a/docs/faq.md b/docs/faq.md index bfd88ec..5db9e6e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md index bf27969..0b4c7e0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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). diff --git a/docs/vercel-setup.md b/docs/vercel-setup.md index d3b8fd1..05d0c8e 100644 --- a/docs/vercel-setup.md +++ b/docs/vercel-setup.md @@ -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. @@ -113,11 +113,11 @@ 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 @@ -125,7 +125,7 @@ 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. --- diff --git a/packages/deepsec/src/__tests__/credential-brokering.test.ts b/packages/deepsec/src/__tests__/credential-brokering.test.ts index 5f08dcc..62b32d5 100644 --- a/packages/deepsec/src/__tests__/credential-brokering.test.ts +++ b/packages/deepsec/src/__tests__/credential-brokering.test.ts @@ -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", @@ -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"); diff --git a/packages/deepsec/src/__tests__/preflight.test.ts b/packages/deepsec/src/__tests__/preflight.test.ts index 02915f1..48c537b 100644 --- a/packages/deepsec/src/__tests__/preflight.test.ts +++ b/packages/deepsec/src/__tests__/preflight.test.ts @@ -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 @@ -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/, diff --git a/packages/deepsec/src/preflight.ts b/packages/deepsec/src/preflight.ts index 30ef742..c27e7e7 100644 --- a/packages/deepsec/src/preflight.ts +++ b/packages/deepsec/src/preflight.ts @@ -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)) { @@ -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}`, ); } diff --git a/packages/deepsec/src/sandbox/setup.ts b/packages/deepsec/src/sandbox/setup.ts index ac2f3f4..1108f43 100644 --- a/packages/deepsec/src/sandbox/setup.ts +++ b/packages/deepsec/src/sandbox/setup.ts @@ -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//environ. @@ -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