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
15 changes: 15 additions & 0 deletions github-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,21 @@ The Action uses the CLI’s validated JSON result for findings and scan status.
If the CLI exits without a usable result, the Action reports failure and leaves
report paths empty; it does not recover unvalidated files from disk.

### Request attribution

The Action adds `codex_security_integration=github_action` and
`codex_security_provider` (the configured provider family) through the CLI's existing
`responses_api_metadata` setting. These accompany the CLI's request attribution
on model requests supported by the provider. This attribution contains no
repository, workflow, run, actor, credential, or finding data.
Native managed configuration may override the provider selection before inference.

The Action continues to set `analytics.enabled=false`. Request metadata is
separate from native analytics and does not create an additional telemetry
request or publish scan results to Codex Security Cloud. Requests sent to other
providers do not establish OpenAI-side usage reporting. Summaries, annotations,
and artifact uploads retain their existing controls.

## Troubleshooting

- **Checkout or history errors:** use the triggering revision, `fetch-depth: 0`
Expand Down
9 changes: 8 additions & 1 deletion github-action/dist/index.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion github-action/src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export function scanArguments(inputs: Inputs, target: {repository: string; diffB
'--output-dir', resultsDirectory, '--format', 'json'];
// Preserve the pinned CLI's sandbox and automatic approval-review defaults.
// Forcing approval_policy="never" prevents recovery from hosted Linux sandbox errors.
args.push('--codex', 'analytics.enabled=false');
args.push('--codex', 'analytics.enabled=false',
'--codex', 'responses_api_metadata.codex_security_integration="github_action"',
'--codex', `responses_api_metadata.codex_security_provider=${JSON.stringify(inputs.provider)}`);
// Bind the option value and keep it explicitly relative so the CLI treats
// leading '-' and '~' as literal repository filenames.
for (const path of inputs.paths) args.push(`--path=./${path}`);
Expand Down
12 changes: 12 additions & 0 deletions github-action/tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ import { copyCompletedScan } from './fixtures.js';
type Scenario = 'schedule' | 'pr' | 'policy-pr';
const inputKey = (name: string): string => `INPUT_${name.toUpperCase()}`;

function assertAttribution(args: readonly string[], provider: string): void {
assert.deepEqual(args.flatMap((arg, index) => arg === '--codex' ? [args[index + 1]] : []), [
'analytics.enabled=false',
'responses_api_metadata.codex_security_integration="github_action"',
`responses_api_metadata.codex_security_provider="${provider}"`,
]);
}

function outputValues(text: string): Record<string, string> {
const values: Record<string, string> = {};
const lines = text.split(/\r?\n/u);
Expand Down Expand Up @@ -186,6 +194,7 @@ test('PR severity failure retains SARIF outputs despite unrelated scope notes',

test('scheduled complete report-only scan succeeds', async (t) => {
const app = await harness(t); const result = await app.run();
assertAttribution(result.args, 'openai');
assert.equal(result.exitCode, 0); assert.equal(result.outputs['scan-status'], 'completed');
assert.equal(result.outputs['policy-status'], 'passed'); assert.equal(result.outputs['sarif-upload-ready'], 'true');
assert.equal(result.outputs['analysis-ref'], 'refs/heads/main');
Expand All @@ -212,6 +221,7 @@ test('Deep scans forward their budget and publish completed reports with cleanup
const app = await harness(t);
app.setInput('mode', 'deep'); app.setInput('max-time-hours', '1.5');
const result = await app.run();
assertAttribution(result.args, 'openai');
assert.equal(result.args[result.args.indexOf('--mode') + 1], 'deep');
assert.equal(result.args[result.args.indexOf('--max-time-hours') + 1], '1.5');
assert.equal(result.exitCode, 0); assert.equal(result.cleanups, 1);
Expand Down Expand Up @@ -254,6 +264,7 @@ for (const mode of ['standard', 'deep']) {
assert.equal(result.exitCode, 0); assert.equal(result.outputs['scan-status'], 'completed');
assert.equal(result.outputs['sarif-upload-ready'], 'true');
assert.equal(result.args[result.args.indexOf('--provider') + 1], 'amazon-bedrock');
assertAttribution(result.args, 'amazon-bedrock');
assert.equal(result.args[result.args.indexOf('--auth') + 1], 'auto');
assert.equal(result.environment.AWS_ACCESS_KEY_ID, 'synthetic-access-key');
assert.equal(result.environment.AWS_SECRET_ACCESS_KEY, 'synthetic-secret-key');
Expand All @@ -274,6 +285,7 @@ for (const [provider, key] of [['openrouter', 'OPENROUTER_API_KEY'], ['fireworks
const result = await app.run();
assert.equal(result.exitCode, 0); assert.equal(result.outputs['scan-status'], 'completed');
assert.equal(result.args[result.args.indexOf('--provider') + 1], provider);
assertAttribution(result.args, provider);
assert.equal(result.args[result.args.indexOf('--auth') + 1], 'api-key');
assert.equal(result.environment[key], expectedKey);
for (const other of ['OPENAI_API_KEY', 'CODEX_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'AWS_SESSION_TOKEN', 'GITHUB_TOKEN'])
Expand Down
42 changes: 39 additions & 3 deletions plugins/codex-security/mcp-app/src/deep-scan/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ export class CodexSdkWorkerExecutor implements CodexWorkerExecutor {
// Worker-owned tool and permission settings take precedence over inheritance.
const configOverrides = profileConfigOverrides({
...runtimeSettings.config,
responses_api_metadata: {
codex_security_surface: "plugin",
codex_security_command: "scan",
codex_security_scan_mode: "deep",
// Keep the host's integration/provider context as this coordinator
// launches or resumes workers with a different role.
...(isRecord(runtimeSettings.config.responses_api_metadata)
? runtimeSettings.config.responses_api_metadata
: {}),
codex_security_worker_role: request.kind,
},
...(this.modelSettings.reasoningEffort
? { model_reasoning_effort: this.modelSettings.reasoningEffort }
: {}),
Expand Down Expand Up @@ -516,6 +527,27 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

function mergeConfigTables(
base: Record<string, unknown>,
override: Record<string, unknown>,
): Record<string, unknown> {
return Object.fromEntries([
...Object.entries(base),
...Object.entries(override).map(([key, value]) => {
const inherited = base[key];
return [
key,
isRecord(inherited) &&
!Array.isArray(inherited) &&
isRecord(value) &&
!Array.isArray(value)
? mergeConfigTables(inherited, value)
: value,
];
}),
]);
}

async function workerRuntimeSettings(
environment: Record<string, string>,
): Promise<CodexSdkWorkerRuntimeSettings> {
Expand All @@ -531,17 +563,21 @@ async function workerRuntimeSettings(
typeof config.profile === "string" && isRecord(profiles)
? profiles[config.profile]
: undefined;
const selected = { ...config, ...(isRecord(profile) ? profile : {}) };
for (const key of ["analytics", "responses_api_metadata"]) {
const selected: Record<string, unknown> = {
...config,
...(isRecord(profile) ? profile : {}),
};
for (const key of ["analytics", "otel", "responses_api_metadata"]) {
if (isRecord(config[key]) && isRecord(profile) && isRecord(profile[key])) {
selected[key] = { ...config[key], ...profile[key] };
selected[key] = mergeConfigTables(config[key], profile[key]);
}
}
const inherited = Object.fromEntries(
[
"model_reasoning_summary",
"service_tier",
"analytics",
"otel",
"responses_api_metadata",
"skills",
].map((key) => [key, selected[key]]),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/** Root, selected-profile, and SDK snapshot telemetry used by launch tests. */
export function workerTelemetryFixture(
index: number,
hasConfiguration: boolean,
) {
// Cover missing settings, native config inheritance, and snapshot overrides.
const mergeProfileTelemetry = index === 0 && hasConfiguration;
const telemetry =
index === 0 && !hasConfiguration
? {}
: {
analytics: { enabled: index % 2 === 0 },
otel: {
environment: `synthetic-scan-${index}`,
exporter: {
"otlp-http": {
endpoint: `http://localhost:${4318 + index}/v1/logs`,
headers: { "X-Scan": `synthetic-scan-${index}` },
},
},
log_user_prompt: false,
},
responses_api_metadata: {
codex_security_surface: index % 2 === 0 ? "cli" : "sdk",
codex_security_command: "scan",
codex_security_package_version: `0.2.${index}`,
codex_security_plugin_version: `0.1.${index}`,
codex_security_integration: "github_action",
codex_security_provider:
index % 2 === 0 ? "openai" : "amazon-bedrock",
custom_tag: `synthetic-scan-${index}`,
"team.tag": `literal-key-${index}`,
...(mergeProfileTelemetry
? {
root_only: "native-root",
profile_only: "selected-profile",
}
: {}),
},
};
const overrideTelemetry = index === 1 || index === 2;
const inheritedTelemetry =
overrideTelemetry || index === 4
? {
analytics: { enabled: index % 2 !== 0 },
otel: { environment: "preflight-default", exporter: "none" },
responses_api_metadata: { custom_tag: "preflight-default" },
}
: mergeProfileTelemetry
? {
analytics: { enabled: true },
otel: {
environment: "native-root",
exporter: {
"otlp-http": {
endpoint: "http://localhost:4317/v1/logs",
headers: { "X-Scan": "synthetic-scan-0" },
},
},
log_user_prompt: false,
},
responses_api_metadata: {
codex_security_surface: "cli",
codex_security_command: "scan",
codex_security_package_version: "0.2.0",
codex_security_plugin_version: "0.1.0",
codex_security_integration: "github_action",
codex_security_provider: "openai",
custom_tag: "preflight-default",
"team.tag": "literal-key-0",
root_only: "native-root",
},
}
: telemetry;
return {
mergeProfileTelemetry,
telemetry,
inheritedTelemetry,
workerTelemetry: overrideTelemetry ? telemetry : {},
};
}
72 changes: 28 additions & 44 deletions plugins/codex-security/mcp-app/tests/test_deep_scan_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
DeepScanWorkerKind,
} from "../src/deep-scan/types.js";
import { testDeepScanContextSnapshots } from "./deep_scan_context_snapshot_cases.ts";
import { workerTelemetryFixture } from "./deep_scan_worker_telemetry_fixture.ts";

const executorSource = new URL("../src/deep-scan/executor.ts", import.meta.url);
const { bundledCodexSdkEnvironment } = await importModule({
Expand Down Expand Up @@ -1515,47 +1516,12 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`,
},
],
};
// Cover missing settings, native config inheritance, and snapshot overrides.
const mergeProfileTelemetry = index === 0 && configuration !== "";
const telemetry =
index === 0 && configuration === ""
? {}
: {
analytics: { enabled: index % 2 === 0 },
responses_api_metadata: {
codex_security_surface: index % 2 === 0 ? "cli" : "sdk",
codex_security_command: "scan",
codex_security_package_version: `0.2.${index}`,
codex_security_plugin_version: `0.1.${index}`,
custom_tag: `synthetic-scan-${index}`,
...(mergeProfileTelemetry
? {
root_only: "native-root",
profile_only: "selected-profile",
}
: {}),
},
};
const overrideTelemetry = index === 1 || index === 2;
const inheritedTelemetry =
overrideTelemetry || index === 4
? {
analytics: { enabled: index % 2 !== 0 },
responses_api_metadata: { custom_tag: "preflight-default" },
}
: mergeProfileTelemetry
? {
analytics: { enabled: true },
responses_api_metadata: {
codex_security_surface: "cli",
codex_security_command: "scan",
codex_security_package_version: "0.2.0",
codex_security_plugin_version: "0.1.0",
custom_tag: "preflight-default",
root_only: "native-root",
},
}
: telemetry;
const {
mergeProfileTelemetry,
telemetry,
inheritedTelemetry,
workerTelemetry,
} = workerTelemetryFixture(index, configuration !== "");
const features =
index === 0
? undefined
Expand Down Expand Up @@ -1674,7 +1640,7 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`,
skills,
features,
telemetry,
workerTelemetry: overrideTelemetry ? telemetry : {},
workerTelemetry,
configuration: {
...parsedConfiguration,
...inheritedTelemetry,
Expand Down Expand Up @@ -1718,6 +1684,14 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`,
...(mergeProfileTelemetry
? {
analytics: {},
otel: {
environment: "synthetic-scan-0",
exporter: {
"otlp-http": {
endpoint: "http://localhost:4318/v1/logs",
},
},
},
responses_api_metadata: {
custom_tag: "synthetic-scan-0",
profile_only: "selected-profile",
Expand Down Expand Up @@ -2054,15 +2028,25 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`,
parseToml(
nativeConfigOverrides(invocation.argv)
.filter((override) =>
/^(analytics|responses_api_metadata)[.=]/.test(
/^(analytics|otel|responses_api_metadata)[.=]/.test(
override,
),
)
.join("\n"),
),
),
),
workerConfigurations[index].telemetry,
{
...workerConfigurations[index].telemetry,
responses_api_metadata: {
codex_security_surface: "plugin",
codex_security_command: "scan",
codex_security_scan_mode: "deep",
...workerConfigurations[index].telemetry
.responses_api_metadata,
codex_security_worker_role: kind,
},
},
);
assert.equal(
invocation.cacheDirectory,
Expand Down
Loading
Loading