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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,24 @@ export AWS_BEARER_TOKEN_BEDROCK="<your-bedrock-api-key>"
export AWS_REGION="us-east-2"
codex-security scan . --provider amazon-bedrock --model openai.gpt-5.6-luna

export AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com"
export AZURE_OPENAI_API_KEY="<your-azure-openai-key>"
codex-security scan . --provider azure --model <your-deployment-name>

export OPENROUTER_API_KEY="<your-openrouter-api-key>"
codex-security scan . --provider openrouter --model anthropic/claude-sonnet-4.5

export FIREWORKS_API_KEY="<your-fireworks-api-key>"
codex-security scan . --provider fireworks --model accounts/fireworks/models/qwen3-235b-a22b
```

For Azure, `--model` is the **deployment** name, not the underlying model name,
and the deployment must serve a reasoning model because scans always request a
reasoning effort. `AZURE_OPENAI_API_KEY` accepts either a resource key or a
Microsoft Entra access token, so
`az account get-access-token --resource https://ai.azure.com` works in place of
a static key.

## Documentation

**👉👉 See the [Codex Security documentation](https://learn.chatgpt.com/docs/security/cli)** for full documentation.
3 changes: 3 additions & 0 deletions plugins/codex-security/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"FIREWORKS_API_KEY",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AWS_BEARER_TOKEN_BEDROCK",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
Expand Down
3 changes: 3 additions & 0 deletions plugins/codex-security/mcp-app/tests/test_mcp_app_smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ assert.deepEqual(
"OPENAI_API_KEY",
"OPENROUTER_API_KEY",
"FIREWORKS_API_KEY",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AWS_BEARER_TOKEN_BEDROCK",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
Expand Down
19 changes: 19 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ Access-token environment variables are not scan API keys.
For other inference providers:

```bash
export AZURE_OPENAI_ENDPOINT="https://<resource>.openai.azure.com"
export AZURE_OPENAI_API_KEY="<your-azure-openai-key>"
npx @openai/codex-security scan . --provider azure --model <your-deployment-name>

export OPENROUTER_API_KEY="<your-openrouter-api-key>"
npx @openai/codex-security scan . --provider openrouter --model anthropic/claude-sonnet-4.5

Expand All @@ -200,6 +204,21 @@ export AWS_REGION="us-east-2"
npx @openai/codex-security scan . --provider amazon-bedrock --model openai.gpt-5.6-luna
```

Azure uses the resource's v1 endpoint, so no `api-version` is needed. Set
`AZURE_OPENAI_ENDPOINT` (or `AZURE_OPENAI_BASE_URL`) to the resource address
such as `https://<resource>.openai.azure.com`; `/openai/v1` is appended when
absent. `--model` is the deployment name. `AZURE_OPENAI_API_KEY` accepts a
resource key or a Microsoft Entra access token from
`az account get-access-token --resource https://ai.azure.com`. Saved scan
recipes keep the resolved endpoint so a replay targets the same resource.

Deploy a reasoning model: scans always send a reasoning effort, so a
non-reasoning deployment fails with `Unsupported parameter: 'reasoning.effort'`.
Give the deployment enough tokens-per-minute for scan-sized prompts; a small
quota surfaces as repeated `Rate limit reached` retries. Deploying a model the
cost table already prices, such as `gpt-5.6-sol`, also keeps `--max-cost` and
cost reporting working; other deployments report no cost estimate.

Bedrock also accepts AWS access keys, profiles, web identity, container
credentials, and the default AWS credential chain. Set `AWS_REGION` and choose
a Bedrock model with `--model`; OpenAI models such as `openai.gpt-5.6-luna`
Expand Down
26 changes: 22 additions & 4 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
} from "./codex-prompt.js";
import {
EXTERNAL_CODEX_PROVIDERS,
hasResolvedBaseUrl,
isExternalModelProvider,
mergedCodexConfig,
scanApprovalPolicy,
Expand Down Expand Up @@ -300,7 +301,8 @@ export type ScanAuthentication =
| "OPENAI_API_KEY"
| "CODEX_API_KEY"
| "OPENROUTER_API_KEY"
| "FIREWORKS_API_KEY";
| "FIREWORKS_API_KEY"
| "AZURE_OPENAI_API_KEY";
verified: false;
}
| {
Expand Down Expand Up @@ -3308,6 +3310,10 @@ async function runtimeScanAuthentication(
return authentication;
}

const EXTERNAL_PROVIDER_KEYS: ReadonlySet<string> = new Set(
Object.values(EXTERNAL_CODEX_PROVIDERS).map((provider) => provider.env_key),
);

/** @internal */
export function selectedScanEnvironment(
environment: ProcessEnvironment,
Expand All @@ -3325,7 +3331,7 @@ export function selectedScanEnvironment(
Object.entries(environment).filter(([name]) => {
const key = name.toUpperCase();
if (key === "OPENAI_API_KEY" || key === "CODEX_API_KEY") return false;
if (key === "OPENROUTER_API_KEY" || key === "FIREWORKS_API_KEY") {
if (EXTERNAL_PROVIDER_KEYS.has(key)) {
return (
!bedrockProvider &&
(selectedProviderKey === null || key === selectedProviderKey)
Expand Down Expand Up @@ -3365,7 +3371,8 @@ function environmentApiKeyEntry(
| "OPENAI_API_KEY"
| "CODEX_API_KEY"
| "OPENROUTER_API_KEY"
| "FIREWORKS_API_KEY";
| "FIREWORKS_API_KEY"
| "AZURE_OPENAI_API_KEY";
value: string;
} | null {
const keys = isExternalModelProvider(modelProvider)
Expand Down Expand Up @@ -3624,8 +3631,19 @@ export function scanPreflightCodexConfig(config: JsonObject): JsonObject {
}
const modelProvider = scanModelProvider(result);
if (isExternalModelProvider(modelProvider)) {
// Per-account endpoints must survive into saved recipes, or replaying a
// recipe would lose the resource the scan actually ran against.
const configured = isRecord(config["model_providers"])
? config["model_providers"][modelProvider]
: undefined;
const baseUrl = isRecord(configured) ? configured["base_url"] : undefined;
result["model_providers"] = {
[modelProvider]: { ...EXTERNAL_CODEX_PROVIDERS[modelProvider] },
[modelProvider]: {
...EXTERNAL_CODEX_PROVIDERS[modelProvider],
...(hasResolvedBaseUrl(modelProvider) && safeString(baseUrl)
? { base_url: baseUrl }
: {}),
},
};
} else if (modelProvider === "amazon-bedrock") {
const providers = config["model_providers"];
Expand Down
44 changes: 39 additions & 5 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
import {
DEFAULT_CODEX_CONFIG,
EXTERNAL_CODEX_PROVIDERS,
externalProviderTable,
isExternalModelProvider,
mergedCodexConfig,
scanModelConfiguration,
Expand Down Expand Up @@ -299,7 +300,7 @@ const VALUE_OPTIONS = new Set([
"--linear-assignee",
]);
const PROVIDER_OPTION = z
.enum(["openai", "openrouter", "fireworks", "amazon-bedrock"])
.enum(["openai", "openrouter", "fireworks", "azure", "amazon-bedrock"])
.default("openai")
.describe("Inference provider for scans.");
const CREATE_PR_OPTION = z
Expand Down Expand Up @@ -6645,9 +6646,12 @@ async function executeScan(
progress?.stage(
`Authentication: API key from ${authentication.source}.`,
);
progress?.stage(
"To use your ChatGPT sign-in, retry with --auth chatgpt.",
);
// A ChatGPT sign-in cannot reach a third-party provider endpoint.
if (externalProviderSource(authentication) === null) {
progress?.stage(
"To use your ChatGPT sign-in, retry with --auth chatgpt.",
);
}
} else if (authentication.method === "aws_credentials") {
progress?.stage(
`Authentication: AWS credentials from ${authentication.source}.`,
Expand Down Expand Up @@ -7142,6 +7146,13 @@ function scanFailureMessage(
"Check your Amazon Bedrock bearer token or AWS credential chain."
);
}
const unauthorizedProvider = externalProviderSource(authentication);
if (unauthorizedProvider !== null) {
return (
`Authentication failed using ${unauthorizedProvider}. ` +
"Check the credential and endpoint for the configured provider."
);
}
return authentication?.method === "api_key"
? `Authentication failed using ${authentication.source}. ` +
"Retry with '--auth chatgpt' or provide a valid API key."
Expand All @@ -7154,6 +7165,13 @@ function scanFailureMessage(
"Check your AWS identity and Bedrock model permissions."
);
}
const forbiddenProvider = externalProviderSource(authentication);
if (forbiddenProvider !== null) {
return (
`The credential from ${forbiddenProvider} cannot access the configured model. ` +
"Confirm the deployment or model name and the credential's permissions."
);
}
return authentication?.method === "api_key"
? `The API key from ${authentication.source} cannot access the configured model. ` +
"Retry with '--auth chatgpt' or use an API key with model access."
Expand All @@ -7168,6 +7186,21 @@ function scanFailureMessage(
}
}

const EXTERNAL_PROVIDER_SOURCES: ReadonlySet<string> = new Set(
Object.values(EXTERNAL_CODEX_PROVIDERS).map((provider) => provider.env_key),
);

// ChatGPT sign-in is not an alternative when the scan runs through a
// third-party provider, so those failures need different guidance.
function externalProviderSource(
authentication: ScanAuthentication | null | undefined,
): string | null {
return authentication?.method === "api_key" &&
EXTERNAL_PROVIDER_SOURCES.has(authentication.source)
? authentication.source
: null;
}

function scanScope(arguments_: ScanArguments): string | null {
if (arguments_.paths.length > 0) {
const displayed = arguments_.paths.slice(0, 3).map((path) => {
Expand Down Expand Up @@ -7447,14 +7480,15 @@ export function parseCodexOverrides(
model?: string,
effort?: ScanReasoningEffort,
provider?: "openai" | "amazon-bedrock" | ExternalModelProvider,
environment: Readonly<Record<string, string | undefined>> = process.env,
): JsonObject {
const result = Object.create(null) as JsonObject;
if (model !== undefined) result["model"] = model;
if (effort !== undefined) result["model_reasoning_effort"] = effort;
if (isExternalModelProvider(provider)) {
result["model_provider"] = provider;
result["model_providers"] = {
[provider]: { ...EXTERNAL_CODEX_PROVIDERS[provider] },
[provider]: externalProviderTable(provider, environment),
};
} else if (provider === "amazon-bedrock") {
result["model_provider"] = provider;
Expand Down
63 changes: 63 additions & 0 deletions sdk/typescript/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,27 @@ export const FIREWORKS_CODEX_PROVIDER = {
wire_api: "responses",
} as const satisfies JsonObject;

// Azure resources each have their own endpoint, so base_url is resolved from
// the environment instead of being a fixed address. The v1 surface accepts the
// resource key or a Microsoft Entra token as a bearer token, so env_key covers
// both without extra header configuration.
export const AZURE_CODEX_PROVIDER = {
name: "Azure OpenAI",
env_key: "AZURE_OPENAI_API_KEY",
wire_api: "responses",
} as const satisfies JsonObject;

export const AZURE_ENDPOINT_ENVIRONMENT_VARIABLES = [
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
] as const;

const AZURE_V1_PATH = "/openai/v1";

export const EXTERNAL_CODEX_PROVIDERS = {
openrouter: OPENROUTER_CODEX_PROVIDER,
fireworks: FIREWORKS_CODEX_PROVIDER,
azure: AZURE_CODEX_PROVIDER,
} as const;

export type ExternalModelProvider = keyof typeof EXTERNAL_CODEX_PROVIDERS;
Expand All @@ -51,6 +69,51 @@ export function isExternalModelProvider(
);
}

/** Providers whose endpoint is per-account rather than a fixed address. */
export function hasResolvedBaseUrl(provider: ExternalModelProvider): boolean {
return provider === "azure";
}

export function azureBaseUrl(
environment: Readonly<Record<string, string | undefined>>,
): string {
const configured = AZURE_ENDPOINT_ENVIRONMENT_VARIABLES.map((name) =>
environment[name]?.trim(),
).find((value) => value !== undefined && value.length > 0);
if (configured === undefined) {
throw new ConfigurationError(
"Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_ENDPOINT to the Azure OpenAI " +
"resource endpoint, such as https://<resource>.openai.azure.com.",
);
}
let endpoint: URL;
try {
endpoint = new URL(configured);
} catch {
throw new ConfigurationError(
`The Azure OpenAI endpoint must be an absolute URL: ${configured}`,
);
}
if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") {
throw new ConfigurationError(
`The Azure OpenAI endpoint must use http or https: ${configured}`,
);
Comment on lines +97 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require TLS for remote Azure endpoints

When AZURE_OPENAI_BASE_URL or AZURE_OPENAI_ENDPOINT names a non-loopback http:// address, this accepts it and Codex subsequently sends AZURE_OPENAI_API_KEY to that endpoint as the provider credential, allowing network observers to recover a resource key or Entra token. Reject plaintext remote endpoints while preserving loopback HTTP only if local-provider testing requires it.

AGENTS.md reference: sdk/typescript/AGENTS.md:L22-L24

Useful? React with 👍 / 👎.

}
// Accept the resource endpoint Azure prints as well as a full v1 base URL.
const path = endpoint.pathname.replace(/\/+$/u, "");
return `${endpoint.origin}${path.endsWith(AZURE_V1_PATH) ? path : `${path}${AZURE_V1_PATH}`}`;
}

/** The complete `model_providers.<id>` table Codex needs for a scan. */
export function externalProviderTable(
provider: ExternalModelProvider,
environment: Readonly<Record<string, string | undefined>>,
): JsonObject {
const table: JsonObject = { ...EXTERNAL_CODEX_PROVIDERS[provider] };
if (provider === "azure") table["base_url"] = azureBaseUrl(environment);
return table;
}

export const DEFAULT_CODEX_CONFIG: Readonly<JsonObject> = {
approval_policy: "on-request",
approvals_reviewer: "auto_review",
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const PLUGIN_HELPER_SECRET_ENVIRONMENT_VARIABLES = new Set([
"CODEX_API_KEY",
"OPENROUTER_API_KEY",
"FIREWORKS_API_KEY",
"AZURE_OPENAI_API_KEY",
]);
const PREPARE_SCAN_ARTIFACT_RESTORER_PROGRAM = `
from pathlib import Path
Expand Down
Loading
Loading