From 0ba7351e349256a4cc97a51861a7b5a12bd0f1fb Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Fri, 31 Jul 2026 05:00:02 -0400 Subject: [PATCH 1/4] Allow scan options during bulk discovery --- sdk/typescript/src/cli.ts | 23 ++--------------------- sdk/typescript/tests-ts/cli.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 30dc6f4..7161517 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -1292,28 +1292,9 @@ export async function main( let outputDir: string; let githubHost: string | undefined; if (args.input === undefined) { - let optionIndex = 1; - while (optionIndex < argv.length) { - const argument = argv[optionIndex]!; - if ( - argument === "--model" || - argument === "--effort" || - argument === "--codex" - ) { - optionIndex += 2; - } else if ( - argument.startsWith("--model=") || - argument.startsWith("--effort=") || - argument.startsWith("--codex=") - ) { - optionIndex += 1; - } else { - break; - } - } - if (argv[0] !== "bulk-scan" || optionIndex !== argv.length) { + if (options.outputDir !== undefined) { throw new Error( - "Run 'codex-security bulk-scan [--model MODEL] [--effort EFFORT] [--codex KEY=VALUE]' to discover repositories, or provide a CSV and --output-dir.", + "--output-dir can only be used with a repository CSV; omit it to choose an output directory interactively.", ); } const wizard = await runBulkScanWizard( diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index d16b6db..656c7af 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -756,6 +756,12 @@ describe("CLI", () => { ["bulk-scan", "--codex", 'model_reasoning_effort="high"'], ["bulk-scan", '--codex=model_reasoning_effort="high"'], ["bulk-scan", "--model", "gpt-5.6-terra", "--effort", "high"], + ["bulk-scan", "--workers", "8"], + ["bulk-scan", "--workers=8"], + ["bulk-scan", "--mode", "deep"], + ["bulk-scan", "--max-attempts=3"], + ["bulk-scan", "--plugin-path", "./plugin"], + ["bulk-scan", "--python=python3"], ] as const) { const stdout = capture(); const stderr = capture(); @@ -791,6 +797,24 @@ describe("CLI", () => { expect(stdout.text()).toBe(""); }); + test("lets the discovery wizard choose its output directory", async () => { + const stdout = capture(); + const stderr = capture(); + + expect( + await main( + ["bulk-scan", "--output-dir", "results"], + stdout.stream, + stderr.stream, + dependencies(), + ), + ).toBe(2); + expect(stderr.text()).toContain( + "--output-dir can only be used with a repository CSV", + ); + expect(stdout.text()).toBe(""); + }); + test("exposes only typed, read-only SDK metadata over MCP", () => { const child = spawnSync( process.execPath, From e96d424a08be180f2bd6765ae463abb09cc06d96 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 1 Aug 2026 06:55:37 -0700 Subject: [PATCH 2/4] fix: preserve container safeguards for flexible bulk scans --- docker/entrypoint.sh | 60 +++++- sdk/typescript/README.md | 10 + sdk/typescript/tests-ts/cli.test.ts | 174 ++++++++++++++++-- .../tests-ts/container-entrypoint.test.ts | 169 ++++++++++++++++- 4 files changed, 391 insertions(+), 22 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 83dc3be..0e6b8e6 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,11 +2,63 @@ set -eu -if [ "${1:-}" = bulk-scan ]; then - case "${2:-}" in - --help|-h) +# Global options may precede the command, and scan options may precede its CSV. +# Classify positional input without consuming arguments or mistaking option values for a CSV. +bulk_scan_command= +bulk_scan_input= +bulk_scan_metadata= +expects_option_value= + +for argument do + if [ "$expects_option_value" = yes ]; then + expects_option_value= + continue + fi + + case "$argument" in + --help|-h|--llms|--llms-full|--schema|--version) + bulk_scan_metadata=yes + continue + ;; + esac + + if [ "$bulk_scan_command" != yes ]; then + case "$argument" in + bulk-scan) + bulk_scan_command=yes + ;; + --filter-output|--format|--token-limit|--token-offset) + expects_option_value=yes + ;; + --filter-output=*|--format=*|--token-limit=*|--token-offset=*|\ + --full-output|--json|--token-count|--) + ;; + *) + break + ;; + esac + continue + fi + + case "$argument" in + --output-dir|--workers|--mode|--model|--effort|--max-attempts|\ + --plugin-path|--python|--codex|--filter-output|--format|\ + --token-limit|--token-offset) + expects_option_value=yes ;; - ""|-*) + -*) + ;; + *) + if [ -z "$bulk_scan_input" ]; then + bulk_scan_input=$argument + fi + ;; + esac +done + +if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_metadata" != yes ]; then + case "$bulk_scan_input" in + "") printf '%s\n' 'codex-security: bulk-scan requires a repository CSV; interactive discovery is not supported in this image.' >&2 exit 2 ;; diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 21f02ce..f26b8cc 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -186,6 +186,7 @@ npx @openai/codex-security scan /path/to/repository --max-cost 5 npx @openai/codex-security install-hook npx @openai/codex-security bulk-scan npx @openai/codex-security bulk-scan --model gpt-5.6-terra --effort high +npx @openai/codex-security bulk-scan --workers 4 --mode deep --max-attempts 3 npx @openai/codex-security bulk-scan repositories.csv --output-dir /path/outside/repositories/security-scans --workers 4 npx @openai/codex-security scans list /path/to/repository npx @openai/codex-security scans list --scan-root /path/outside/repository/results @@ -395,6 +396,11 @@ Private checkouts reuse your GitHub CLI sign-in without changing your global Git configuration. The selected repositories are saved to `/repositories.csv` for review or resumption. +Interactive discovery accepts the same `--workers`, `--mode`, `--max-attempts`, +`--model`, `--effort`, `--plugin-path`, `--python`, and `--codex` settings as +CSV-driven scans. It prompts for the output directory; `--output-dir` is only +valid when a repository CSV is supplied. + To use an existing repository list or run in CI, pass a CSV with required `id`, `repository`, and `revision` columns. Revisions must be full commit hashes; optional `scope` and `mode` columns narrow individual scans: @@ -544,6 +550,10 @@ device login remains in `state/`. For unattended scans, set `OPENAI_API_KEY` or `CODEX_API_KEY` instead. Set `GH_TOKEN` or `GITHUB_TOKEN` for private GitHub repositories. +The container accepts the repository CSV before or after bulk-scan options. +Interactive repository discovery remains disabled, including when global CLI +options appear before `bulk-scan`. + On Ubuntu hosts that restrict unprivileged user namespaces, an administrator can install the optional, narrowly scoped AppArmor profile once: diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 656c7af..7115d0e 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -15,6 +15,7 @@ import { delimiter, join, normalize } from "node:path"; import { Writable } from "node:stream"; import { fileURLToPath, pathToFileURL } from "node:url"; import { stripVTControlCharacters } from "node:util"; +import { Octokit } from "@octokit/core"; import { describe, expect, test } from "bun:test"; import type { CodexSecurityConfig, @@ -746,6 +747,127 @@ describe("CLI", () => { } }); + test("forwards interactive bulk-scan options through repository discovery", async () => { + const root = await mkdtemp(join(tmpdir(), "codex-security-cli-discovery-")); + const configurations: CodexSecurityConfig[] = []; + let githubClients = 0; + + try { + const stdout = capture(); + const stderr = capture(); + expect( + await main( + [ + "bulk-scan", + "--workers=2", + "--mode", + "deep", + "--max-attempts=3", + "--plugin-path", + "./plugin", + "--python=python3", + "--model", + "gpt-5.6-terra", + "--effort", + "high", + "--codex", + "features.goals=true", + ], + stdout.stream, + stderr.stream, + dependencies({ + currentDirectory: root, + bulkScan: { + prompt: { + isInteractive: () => true, + write: () => {}, + confirm: async () => true, + input: async () => "results", + select: async ( + _question: string, + options: readonly { label: string; value: Value }[], + ) => options[0]!.value, + }, + now: () => Date.parse("2026-08-01T00:00:00.000Z"), + currentDirectory: () => root, + createGitHub: async () => { + githubClients += 1; + return new Octokit({ + auth: "test-token", + request: { + fetch: async (resource: Parameters[0]) => { + const path = new URL( + resource instanceof Request ? resource.url : resource, + ).pathname; + if (path === "/user/orgs") return Response.json([]); + if (path === "/user") { + return Response.json({ login: "acme" }); + } + if (path !== "/graphql") { + throw new Error(`Unexpected GitHub request: ${path}`); + } + return Response.json({ + data: { + repositoryOwner: { + repositories: { + nodes: ["one", "two", "three"].map( + (name, index) => ({ + nameWithOwner: `acme/${name}`, + pushedAt: "2026-07-30T00:00:00.000Z", + defaultBranchRef: { + target: { + oid: String(index + 1).repeat(40), + }, + }, + }), + ), + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + }, + }, + }, + }); + }, + }, + }); + }, + }, + onConfig: (config) => { + configurations.push(config); + throw new Error("Stop before checking out repositories."); + }, + }), + ), + ).toBe(2); + + expect(githubClients).toBe(1); + expect(configurations).toHaveLength(2); + for (const configuration of configurations) { + expect(configuration).toMatchObject({ + pluginPath: "./plugin", + pythonPath: "python3", + codexOverrides: { + features: { goals: true }, + model: "gpt-5.6-terra", + model_reasoning_effort: "high", + }, + }); + } + + const manifest = JSON.parse( + await readFile(join(root, "results", "manifest.json"), "utf8"), + ) as { tasks: { mode: string }[] }; + expect(manifest.tasks).toHaveLength(3); + expect(manifest.tasks.every(({ mode }) => mode === "deep")).toBe(true); + expect(stdout.text()).toBe(""); + expect(stderr.text()).toContain("Stop before checking out repositories."); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + test("requires a terminal for interactive bulk scans", async () => { for (const argv of [ ["bulk-scan"], @@ -759,9 +881,32 @@ describe("CLI", () => { ["bulk-scan", "--workers", "8"], ["bulk-scan", "--workers=8"], ["bulk-scan", "--mode", "deep"], + ["bulk-scan", "--mode=deep"], + ["bulk-scan", "--max-attempts", "3"], ["bulk-scan", "--max-attempts=3"], ["bulk-scan", "--plugin-path", "./plugin"], + ["bulk-scan", "--plugin-path=./plugin"], + ["bulk-scan", "--python", "python3"], ["bulk-scan", "--python=python3"], + [ + "bulk-scan", + "--workers", + "8", + "--mode", + "deep", + "--max-attempts", + "3", + "--plugin-path", + "./plugin", + "--python", + "python3", + "--model", + "gpt-5.6-terra", + "--effort", + "high", + ], + ["--format", "toon", "bulk-scan", "--workers", "8"], + ["--format=toon", "bulk-scan", "--workers=8"], ] as const) { const stdout = capture(); const stderr = capture(); @@ -798,21 +943,22 @@ describe("CLI", () => { }); test("lets the discovery wizard choose its output directory", async () => { - const stdout = capture(); - const stderr = capture(); + for (const argv of [ + ["bulk-scan", "--output-dir", "results"], + ["bulk-scan", "--output-dir=results"], + ["--format", "toon", "bulk-scan", "--output-dir=results"], + ] as const) { + const stdout = capture(); + const stderr = capture(); - expect( - await main( - ["bulk-scan", "--output-dir", "results"], - stdout.stream, - stderr.stream, - dependencies(), - ), - ).toBe(2); - expect(stderr.text()).toContain( - "--output-dir can only be used with a repository CSV", - ); - expect(stdout.text()).toBe(""); + expect( + await main(argv, stdout.stream, stderr.stream, dependencies()), + ).toBe(2); + expect(stderr.text()).toContain( + "--output-dir can only be used with a repository CSV", + ); + expect(stdout.text()).toBe(""); + } }); test("exposes only typed, read-only SDK metadata over MCP", () => { diff --git a/sdk/typescript/tests-ts/container-entrypoint.test.ts b/sdk/typescript/tests-ts/container-entrypoint.test.ts index 990655f..73162d9 100644 --- a/sdk/typescript/tests-ts/container-entrypoint.test.ts +++ b/sdk/typescript/tests-ts/container-entrypoint.test.ts @@ -166,6 +166,60 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "accepts repository CSVs after global options and bulk-scan flags", + async () => { + for (const arguments_ of [ + [ + "bulk-scan", + "--workers", + "2", + "/input/repositories.csv", + "--output-dir", + "/output", + ], + [ + "bulk-scan", + "--workers=2", + "--output-dir=/output", + "/input/repositories.csv", + ], + [ + "--format", + "toon", + "bulk-scan", + "--mode", + "deep", + "/input/repositories.csv", + "--output-dir=/output", + ], + [ + "--format=toon", + "--token-limit=4", + "bulk-scan", + "--output-dir", + "/output", + "/input/repositories.csv", + ], + ] as const) { + const result = await runEntrypoint(arguments_); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe( + [ + ...arguments_, + ...(appArmorRestrictsUserNamespaces && + !usesCodexSecurityAppArmorProfile + ? ["--codex", "features.use_legacy_landlock=true"] + : []), + "", + ].join("\n"), + ); + } + }, + ); + testPosix( "preserves bulk-scan help without injecting scan configuration", async () => { @@ -177,6 +231,29 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "preserves prefixed help and metadata without injecting scan configuration", + async () => { + for (const arguments_ of [ + ["--format", "toon", "bulk-scan", "--help"], + ["--format=toon", "bulk-scan", "-h"], + ["--help", "bulk-scan"], + ["-h", "bulk-scan"], + ["--schema", "bulk-scan"], + ["--llms", "bulk-scan"], + ["--llms-full", "bulk-scan"], + ["bulk-scan", "--workers", "8", "--help"], + ["bulk-scan", "--schema"], + ] as const) { + const result = await runEntrypoint(arguments_); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + } + }, + ); + testPosix( "preserves an explicit Landlock override without duplicating it", async () => { @@ -226,6 +303,40 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "keeps prefixed and option-first CSV scans behind the Landlock guard", + async () => { + const arguments_ = [ + "--format", + "toon", + "bulk-scan", + "--workers", + "2", + "/input/repositories.csv", + "--output-dir", + "/output", + "--codex", + "features.use_legacy_landlock=false", + ]; + const result = await runEntrypoint(arguments_); + + if ( + appArmorRestrictsUserNamespaces && + !usesCodexSecurityAppArmorProfile + ) { + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe( + "codex-security: restricted Ubuntu hosts require --codex features.use_legacy_landlock=true.\n", + ); + } else { + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + } + }, + ); + testPosix( "rejects interactive discovery before starting the CLI", async () => { @@ -239,12 +350,62 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "rejects prefixed discovery without mistaking option values for a CSV", + async () => { + for (const arguments_ of [ + ["bulk-scan", "--workers", "8"], + ["bulk-scan", "--mode", "deep", "--output-dir", "/output"], + [ + "bulk-scan", + "--model", + "gpt-5.6-terra", + "--effort", + "high", + "--max-attempts", + "3", + "--plugin-path", + "./plugin", + "--python", + "python3", + "--codex", + "features.goals=true", + ], + ["bulk-scan", "--workers=8", "--output-dir=/output"], + ["--format", "toon", "bulk-scan", "--workers", "8"], + ["--format=toon", "--token-limit", "5", "bulk-scan", "--mode", "deep"], + [ + "--filter-output", + "total", + "--token-offset=1", + "bulk-scan", + "--python=python3", + ], + ["--json", "bulk-scan", "--max-attempts", "2"], + ["--", "bulk-scan", "--workers", "8"], + ] as const) { + const result = await runEntrypoint(arguments_); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe( + "codex-security: bulk-scan requires a repository CSV; interactive discovery is not supported in this image.\n", + ); + } + }, + ); + testPosix("does not change non-scan commands", async () => { - const result = await runEntrypoint(["--version"]); + for (const arguments_ of [ + ["--version"], + ["--filter-output", "bulk-scan", "info"], + ] as const) { + const result = await runEntrypoint(arguments_); - expect(result.status).toBe(0); - expect(result.stderr).toBe(""); - expect(result.stdout).toBe("--version\n"); + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + } }); }); From fe12bfd011ba75030012ae62375c5aca6d8e2004 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 1 Aug 2026 07:05:57 -0700 Subject: [PATCH 3/4] fix: preserve container sandboxing after option terminators --- docker/entrypoint.sh | 33 +++- .../tests-ts/container-entrypoint.test.ts | 149 ++++++++++++++---- 2 files changed, 147 insertions(+), 35 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0e6b8e6..8c52c2a 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -7,6 +7,7 @@ set -eu bulk_scan_command= bulk_scan_input= bulk_scan_metadata= +bulk_scan_positional_only= expects_option_value= for argument do @@ -15,6 +16,13 @@ for argument do continue fi + if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_positional_only" = yes ]; then + if [ -z "$bulk_scan_input" ]; then + bulk_scan_input=$argument + fi + continue + fi + case "$argument" in --help|-h|--llms|--llms-full|--schema|--version) bulk_scan_metadata=yes @@ -41,6 +49,9 @@ for argument do fi case "$argument" in + --) + bulk_scan_positional_only=yes + ;; --output-dir|--workers|--mode|--model|--effort|--max-attempts|\ --plugin-path|--python|--codex|--filter-output|--format|\ --token-limit|--token-offset) @@ -76,7 +87,9 @@ if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_metadata" != yes ]; then landlock_override= expects_codex_override= for argument do - if [ "$expects_codex_override" = yes ]; then + if [ "$argument" = -- ]; then + break + elif [ "$expects_codex_override" = yes ]; then case "$argument" in features.use_legacy_landlock=true) landlock_override=enabled @@ -103,7 +116,23 @@ if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_metadata" != yes ]; then done if [ "$landlock_override" != enabled ]; then - set -- "$@" --codex features.use_legacy_landlock=true + if [ "$bulk_scan_positional_only" = yes ]; then + arguments_remaining=$# + landlock_inserted= + while [ "$arguments_remaining" -gt 0 ]; do + argument=$1 + shift + if [ "$argument" = -- ] && [ "$landlock_inserted" != yes ]; then + set -- "$@" --codex features.use_legacy_landlock=true "$argument" + landlock_inserted=yes + else + set -- "$@" "$argument" + fi + arguments_remaining=$((arguments_remaining - 1)) + done + else + set -- "$@" --codex features.use_legacy_landlock=true + fi fi fi fi diff --git a/sdk/typescript/tests-ts/container-entrypoint.test.ts b/sdk/typescript/tests-ts/container-entrypoint.test.ts index 73162d9..9f24046 100644 --- a/sdk/typescript/tests-ts/container-entrypoint.test.ts +++ b/sdk/typescript/tests-ts/container-entrypoint.test.ts @@ -255,47 +255,79 @@ describe("customer container entrypoint", () => { ); testPosix( - "preserves an explicit Landlock override without duplicating it", + "keeps metadata-shaped CSV filenames behind the option terminator", async () => { - const arguments_ = [ - "bulk-scan", - "/input/repositories.csv", - "--output-dir", - "/output", - "--codex", - "features.use_legacy_landlock=true", - ]; - const result = await runEntrypoint(arguments_); + for (const filename of [ + "--help", + "-h", + "--schema", + "--llms", + "--llms-full", + "--version", + "--codex=features.use_legacy_landlock=true", + "--codex=features.use_legacy_landlock=false", + ] as const) { + const arguments_ = [ + "--format", + "toon", + "bulk-scan", + "--output-dir", + "/output", + "--", + filename, + ]; + const result = await runEntrypoint(arguments_); - expect(result.status).toBe(0); - expect(result.stderr).toBe(""); - expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe( + [ + ...arguments_.slice(0, -2), + ...(appArmorRestrictsUserNamespaces && + !usesCodexSecurityAppArmorProfile + ? ["--codex", "features.use_legacy_landlock=true"] + : []), + "--", + filename, + "", + ].join("\n"), + ); + } }, ); testPosix( - "rejects incompatible Landlock overrides only on restricted hosts", + "preserves an explicit Landlock override without duplicating it", async () => { - const arguments_ = [ - "bulk-scan", - "/input/repositories.csv", - "--output-dir", - "/output", - "--codex", - "features.use_legacy_landlock=false", - ]; - const result = await runEntrypoint(arguments_); + for (const arguments_ of [ + [ + "bulk-scan", + "/input/repositories.csv", + "--output-dir", + "/output", + "--codex", + "features.use_legacy_landlock=true", + ], + [ + "bulk-scan", + "--output-dir", + "/output", + "--codex", + "features.use_legacy_landlock=true", + "--", + "--help", + ], + [ + "bulk-scan", + "--output-dir", + "/output", + "--codex=features.use_legacy_landlock=true", + "--", + "--schema", + ], + ] as const) { + const result = await runEntrypoint(arguments_); - if ( - appArmorRestrictsUserNamespaces && - !usesCodexSecurityAppArmorProfile - ) { - expect(result.status).toBe(2); - expect(result.stdout).toBe(""); - expect(result.stderr).toBe( - "codex-security: restricted Ubuntu hosts require --codex features.use_legacy_landlock=true.\n", - ); - } else { expect(result.status).toBe(0); expect(result.stderr).toBe(""); expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); @@ -303,6 +335,56 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "rejects incompatible Landlock overrides only on restricted hosts", + async () => { + for (const arguments_ of [ + [ + "bulk-scan", + "/input/repositories.csv", + "--output-dir", + "/output", + "--codex", + "features.use_legacy_landlock=false", + ], + [ + "bulk-scan", + "--output-dir", + "/output", + "--codex", + "features.use_legacy_landlock=false", + "--", + "--help", + ], + [ + "bulk-scan", + "--output-dir", + "/output", + "--codex=features.use_legacy_landlock=false", + "--", + "--schema", + ], + ] as const) { + const result = await runEntrypoint(arguments_); + + if ( + appArmorRestrictsUserNamespaces && + !usesCodexSecurityAppArmorProfile + ) { + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe( + "codex-security: restricted Ubuntu hosts require --codex features.use_legacy_landlock=true.\n", + ); + } else { + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + } + } + }, + ); + testPosix( "keeps prefixed and option-first CSV scans behind the Landlock guard", async () => { @@ -372,6 +454,7 @@ describe("customer container entrypoint", () => { "features.goals=true", ], ["bulk-scan", "--workers=8", "--output-dir=/output"], + ["bulk-scan", "--output-dir", "/output", "--"], ["--format", "toon", "bulk-scan", "--workers", "8"], ["--format=toon", "--token-limit", "5", "bulk-scan", "--mode", "deep"], [ From 49d2172013f987401cdacc3845f5ae5efe7c7f25 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 1 Aug 2026 07:14:24 -0700 Subject: [PATCH 4/4] fix: preserve metadata flags after incomplete scan options --- docker/entrypoint.sh | 10 ++++---- .../tests-ts/container-entrypoint.test.ts | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 8c52c2a..e29e265 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -11,11 +11,6 @@ bulk_scan_positional_only= expects_option_value= for argument do - if [ "$expects_option_value" = yes ]; then - expects_option_value= - continue - fi - if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_positional_only" = yes ]; then if [ -z "$bulk_scan_input" ]; then bulk_scan_input=$argument @@ -30,6 +25,11 @@ for argument do ;; esac + if [ "$expects_option_value" = yes ]; then + expects_option_value= + continue + fi + if [ "$bulk_scan_command" != yes ]; then case "$argument" in bulk-scan) diff --git a/sdk/typescript/tests-ts/container-entrypoint.test.ts b/sdk/typescript/tests-ts/container-entrypoint.test.ts index 9f24046..a742be6 100644 --- a/sdk/typescript/tests-ts/container-entrypoint.test.ts +++ b/sdk/typescript/tests-ts/container-entrypoint.test.ts @@ -254,6 +254,30 @@ describe("customer container entrypoint", () => { }, ); + testPosix( + "forwards metadata after options with missing values to the CLI", + async () => { + for (const arguments_ of [ + ["bulk-scan", "--workers", "--help"], + ["bulk-scan", "--output-dir", "-h"], + ["bulk-scan", "--workers", "--schema"], + ["bulk-scan", "--codex", "--llms"], + ["bulk-scan", "--model", "--llms-full"], + ["bulk-scan", "--python", "--version"], + ["--format", "--help", "bulk-scan"], + ["--filter-output", "-h", "bulk-scan"], + ["--token-limit", "--schema", "bulk-scan"], + ["--token-offset", "--llms", "bulk-scan"], + ] as const) { + const result = await runEntrypoint(arguments_); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe(`${arguments_.join("\n")}\n`); + } + }, + ); + testPosix( "keeps metadata-shaped CSV filenames behind the option terminator", async () => {