diff --git a/README.md b/README.md index b6ea1de1..e79089a7 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ npx @openai/codex-security scan . --provider openrouter --model anthropic/claude export FIREWORKS_API_KEY="" npx @openai/codex-security scan . --provider fireworks --model accounts/fireworks/models/qwen3-235b-a22b + +export HF_TOKEN="" +npx @openai/codex-security scan . --provider huggingface --model openai/gpt-oss-120b ``` Local sign-in honors Codex's configured credential backend, including a system diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 6177b754..f46d792b 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -128,6 +128,9 @@ npx @openai/codex-security scan . --provider openrouter --model anthropic/claude export FIREWORKS_API_KEY="" npx @openai/codex-security scan . --provider fireworks --model accounts/fireworks/models/qwen3-235b-a22b + +export HF_TOKEN="" +npx @openai/codex-security scan . --provider huggingface --model openai/gpt-oss-120b ``` On Windows, set the API key in PowerShell: @@ -418,6 +421,13 @@ Use `--provider openrouter` to send inference through OpenRouter. Set Use `--provider fireworks` to send inference through Fireworks AI. Set `FIREWORKS_API_KEY` and specify a supported model with `--model`. +Use `--provider huggingface` to send inference through +[Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers/en/index). +Set `HF_TOKEN` (fine-grained token with Inference Providers permission) and +specify a supported model with `--model`. Append a provider or policy suffix +when needed (for example `openai/gpt-oss-120b:groq` or +`openai/gpt-oss-120b:cheapest`). + Scan progress identifies the requested paths and reports actual ranking, file-review, validation, and attack-path phases as they become available. Completion summarizes findings, severity, coverage, elapsed time, available diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index f4033d68..8497e0c5 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -26,11 +26,13 @@ import { type AccountStatus, } from "./auth.js"; import { + EXTERNAL_CODEX_PROVIDER_ENV_KEYS, EXTERNAL_CODEX_PROVIDERS, isExternalModelProvider, mergedCodexConfig, scanModelConfiguration, type CodexSecurityConfig, + type ExternalProviderEnvKey, type JsonObject, writeCodexConfig, } from "./config.js"; @@ -173,11 +175,7 @@ export type ScanAuthMode = "auto" | "chatgpt" | "api-key"; export type ScanAuthentication = | { method: "api_key"; - source: - | "OPENAI_API_KEY" - | "CODEX_API_KEY" - | "OPENROUTER_API_KEY" - | "FIREWORKS_API_KEY"; + source: "OPENAI_API_KEY" | "CODEX_API_KEY" | ExternalProviderEnvKey; verified: false; } | { @@ -1903,7 +1901,7 @@ function selectedScanEnvironment( const key = name.toUpperCase(); if (key === "OPENAI_API_KEY" || key === "CODEX_API_KEY") return false; if (selectedProviderKey === null) return true; - if (key === "OPENROUTER_API_KEY" || key === "FIREWORKS_API_KEY") { + if (EXTERNAL_CODEX_PROVIDER_ENV_KEYS.has(key)) { return key === selectedProviderKey; } return true; @@ -1936,11 +1934,7 @@ function environmentApiKeyEntry( environment: ProcessEnvironment, modelProvider?: unknown, ): { - source: - | "OPENAI_API_KEY" - | "CODEX_API_KEY" - | "OPENROUTER_API_KEY" - | "FIREWORKS_API_KEY"; + source: "OPENAI_API_KEY" | "CODEX_API_KEY" | ExternalProviderEnvKey; value: string; } | null { const keys = isExternalModelProvider(modelProvider) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 1311875d..4882da47 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -194,7 +194,7 @@ const VALUE_OPTIONS = new Set([ "--reason", ]); const PROVIDER_OPTION = z - .enum(["openai", "openrouter", "fireworks"]) + .enum(["openai", "openrouter", "fireworks", "huggingface"]) .default("openai") .describe("Inference provider for scans."); diff --git a/sdk/typescript/src/config.ts b/sdk/typescript/src/config.ts index 8ce7033d..ca8e7e91 100644 --- a/sdk/typescript/src/config.ts +++ b/sdk/typescript/src/config.ts @@ -35,13 +35,28 @@ export const FIREWORKS_CODEX_PROVIDER = { wire_api: "responses", } as const satisfies JsonObject; +export const HUGGINGFACE_CODEX_PROVIDER = { + name: "Hugging Face", + base_url: "https://router.huggingface.co/v1", + env_key: "HF_TOKEN", + wire_api: "responses", +} as const satisfies JsonObject; + export const EXTERNAL_CODEX_PROVIDERS = { openrouter: OPENROUTER_CODEX_PROVIDER, fireworks: FIREWORKS_CODEX_PROVIDER, + huggingface: HUGGINGFACE_CODEX_PROVIDER, } as const; export type ExternalModelProvider = keyof typeof EXTERNAL_CODEX_PROVIDERS; +export type ExternalProviderEnvKey = + (typeof EXTERNAL_CODEX_PROVIDERS)[ExternalModelProvider]["env_key"]; + +export const EXTERNAL_CODEX_PROVIDER_ENV_KEYS = new Set( + Object.values(EXTERNAL_CODEX_PROVIDERS).map((provider) => provider.env_key), +); + export function isExternalModelProvider( provider: unknown, ): provider is ExternalModelProvider { diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 16283140..d1e2b476 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -43,7 +43,10 @@ import { PluginBootstrapError, PluginPythonUnavailableError, } from "./errors.js"; -import type { JsonObject } from "./config.js"; +import { + EXTERNAL_CODEX_PROVIDER_ENV_KEYS, + type JsonObject, +} from "./config.js"; import { resolveTrustedExecutable } from "./trusted-executable.js"; const execFile = promisify(execFileCallback); @@ -606,13 +609,14 @@ export async function runWorkbench( ], { env: Object.fromEntries( - Object.entries(options.environment).filter( - ([name]) => - name.toUpperCase() !== "OPENAI_API_KEY" && - name.toUpperCase() !== "CODEX_API_KEY" && - name.toUpperCase() !== "OPENROUTER_API_KEY" && - name.toUpperCase() !== "FIREWORKS_API_KEY", - ), + Object.entries(options.environment).filter(([name]) => { + const key = name.toUpperCase(); + return ( + key !== "OPENAI_API_KEY" && + key !== "CODEX_API_KEY" && + !EXTERNAL_CODEX_PROVIDER_ENV_KEYS.has(key) + ); + }), ), encoding: "utf8", maxBuffer: 4 * 1024 * 1024, diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 4b7f6882..22d390d7 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -41,6 +41,7 @@ import { } from "../src/api.js"; import { FIREWORKS_CODEX_PROVIDER, + HUGGINGFACE_CODEX_PROVIDER, OPENROUTER_CODEX_PROVIDER, writeCodexConfig, type JsonObject, @@ -77,6 +78,13 @@ const EXTERNAL_PROVIDER_CASES = [ "accounts/fireworks/models/qwen3-235b-a22b", FIREWORKS_CODEX_PROVIDER, ], + [ + "Hugging Face", + "huggingface", + "HF_TOKEN", + "openai/gpt-oss-120b", + HUGGINGFACE_CODEX_PROVIDER, + ], ] as const; const TestClientBase = CodexSecurity as unknown as new ( config: Record, diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 0d6ca9fb..4f971942 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -39,6 +39,7 @@ import { main, parseCodexOverrides, Progress } from "../src/cli.js"; import { DEFAULT_CODEX_CONFIG, FIREWORKS_CODEX_PROVIDER, + HUGGINGFACE_CODEX_PROVIDER, OPENROUTER_CODEX_PROVIDER, scanModelConfiguration, } from "../src/config.js"; @@ -121,7 +122,9 @@ describe("CLI", () => { model: { type: "string" }, verbose: { type: "boolean" }, effort: { enum: ["minimal", "low", "medium", "high", "xhigh"] }, - provider: { enum: ["openai", "openrouter", "fireworks"] }, + provider: { + enum: ["openai", "openrouter", "fireworks", "huggingface"], + }, failOnSeverity: { enum: ["critical", "high", "medium", "low"] }, }, }, @@ -759,6 +762,12 @@ describe("CLI", () => { "accounts/fireworks/models/qwen3-235b-a22b", FIREWORKS_CODEX_PROVIDER, ], + [ + "Hugging Face", + "huggingface", + "openai/gpt-oss-120b", + HUGGINGFACE_CODEX_PROVIDER, + ], ] as const)( "routes bulk scans through %s", async (_name, provider, model, providerConfig) => { @@ -1683,7 +1692,9 @@ describe("CLI", () => { expect(help.text()).toContain("--stop-after-no-new "); expect(help.text()).toContain("--max-discovery-runs "); expect(help.text()).toContain("--model "); - expect(help.text()).toContain("--provider "); + expect(help.text()).toContain( + "--provider ", + ); expect(help.text()).toContain( `OpenAI model to use (default: ${DEFAULT_SCAN_MODEL_CONFIGURATION.model}).`, ); @@ -1749,7 +1760,9 @@ describe("CLI", () => { ); expect(help.text()).not.toContain("--outputDir"); expect(help.text()).not.toContain("--maxAttempts"); - expect(help.text()).toContain("--provider "); + expect(help.text()).toContain( + "--provider ", + ); expect(stderr.text()).toBe(""); }); @@ -1810,6 +1823,13 @@ describe("CLI", () => { "accounts/fireworks/models/llama-v3p3-70b-instruct", FIREWORKS_CODEX_PROVIDER, ], + [ + "Hugging Face", + "huggingface", + "openai/gpt-oss-120b", + "moonshotai/Kimi-K2-Instruct-0905:groq", + HUGGINGFACE_CODEX_PROVIDER, + ], ] as const)( "routes scans through %s", async (_name, provider, selectedModel, codexModel, providerConfig) => { @@ -1953,7 +1973,11 @@ describe("CLI", () => { "high", ), ).toThrow("--effort conflicts with --codex model_reasoning_effort"); - for (const provider of ["openrouter", "fireworks"] as const) { + for (const provider of [ + "openrouter", + "fireworks", + "huggingface", + ] as const) { expect(() => parseCodexOverrides([], undefined, undefined, provider), ).toThrow(`--model is required when using --provider ${provider}`); @@ -2047,6 +2071,10 @@ describe("CLI", () => { ["scan", ".", "--provider", "fireworks"], "--model is required when using --provider fireworks", ], + [ + ["scan", ".", "--provider", "huggingface"], + "--model is required when using --provider huggingface", + ], [ ["scan", ".", "--effort", "ultra"], "--effort must be minimal, low, medium, high, or xhigh", diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index d3c2fa3f..e144070b 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -1841,6 +1841,7 @@ describe("runtime directories and plugin Python boundary", () => { "assert os.environ.get('CODEX_API_KEY') is None", "assert os.environ.get('OPENROUTER_API_KEY') is None", "assert os.environ.get('FIREWORKS_API_KEY') is None", + "assert os.environ.get('HF_TOKEN') is None", "print(json.dumps({'ok': True}))", ].join("\n"), ); @@ -1856,6 +1857,7 @@ describe("runtime directories and plugin Python boundary", () => { CODEX_API_KEY: "also-must-not-reach-python", OPENROUTER_API_KEY: "openrouter-must-not-reach-python", FIREWORKS_API_KEY: "fireworks-must-not-reach-python", + HF_TOKEN: "huggingface-must-not-reach-python", }, }, ["test-command"],