From 916c807a4f23158b5f457e7312e4ced5f03b5289 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 00:31:21 +0000 Subject: [PATCH 1/8] build(plugin): assemble native payloads for package builds --- .github/actions/download-native/action.yml | 9 +++++ .github/workflows/container-ci.yml | 10 ++++++ .github/workflows/container-release.yml | 11 ++++++ .github/workflows/native-artifacts.yml | 34 +++++++++++++++++++ .github/workflows/native-unix.yml | 14 +++----- .github/workflows/native-windows.yml | 14 +++----- .github/workflows/node-ci.yml | 15 +++++++- .github/workflows/node-release.yml | 7 ++++ .github/workflows/test-quality.yml | 6 ++++ .../mcp-app/scripts/build_mcp_app.mjs | 15 +++++++- plugins/codex-security/native/.gitignore | 1 + plugins/codex-security/plugin-files.json | 8 +++++ sdk/typescript/scripts/smoke-package.mjs | 33 ++++++++++++++++++ sdk/typescript/tests-ts/build-plugin.test.ts | 16 ++++++++- 14 files changed, 170 insertions(+), 23 deletions(-) create mode 100644 .github/actions/download-native/action.yml create mode 100644 .github/workflows/native-artifacts.yml diff --git a/.github/actions/download-native/action.yml b/.github/actions/download-native/action.yml new file mode 100644 index 000000000..7fd2e7d75 --- /dev/null +++ b/.github/actions/download-native/action.yml @@ -0,0 +1,9 @@ +name: Download native runtime +description: Prepare the verified universal native payload for package builds. +runs: + using: composite + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-universal-${{ github.sha }} + path: plugins/codex-security/native/prebuilt diff --git a/.github/workflows/container-ci.yml b/.github/workflows/container-ci.yml index d55e90871..cd5499b6c 100644 --- a/.github/workflows/container-ci.yml +++ b/.github/workflows/container-ci.yml @@ -5,6 +5,8 @@ on: branches: [main] paths: - .dockerignore + - .github/workflows/native-*.yml + - .github/actions/download-native/** - .github/workflows/container-ci.yml - Dockerfile - Dockerfile.dockerignore @@ -18,6 +20,8 @@ on: pull_request: paths: - .dockerignore + - .github/workflows/native-*.yml + - .github/actions/download-native/** - .github/workflows/container-ci.yml - Dockerfile - Dockerfile.dockerignore @@ -38,7 +42,11 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + native: + uses: ./.github/workflows/native-artifacts.yml + container: + needs: native name: linux-amd64 runs-on: ubuntu-latest timeout-minutes: 30 @@ -48,6 +56,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Verify customer build excludes secrets and scan data shell: bash diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 61ee8d4a9..8d4adcd91 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - .dockerignore + - .github/workflows/native-*.yml + - .github/actions/download-native/** - .github/workflows/container-release.yml - Dockerfile - Dockerfile.dockerignore @@ -27,7 +29,12 @@ permissions: contents: read jobs: + native: + if: github.repository == 'openai/codex-security' + uses: ./.github/workflows/native-artifacts.yml + validate: + needs: native if: github.repository == 'openai/codex-security' name: validate-linux-${{ matrix.architecture }} runs-on: ${{ matrix.runner }} @@ -46,6 +53,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 @@ -353,6 +362,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 diff --git a/.github/workflows/native-artifacts.yml b/.github/workflows/native-artifacts.yml new file mode 100644 index 000000000..b0426ced1 --- /dev/null +++ b/.github/workflows/native-artifacts.yml @@ -0,0 +1,34 @@ +name: native-artifacts + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + unix: + uses: ./.github/workflows/native-unix.yml + windows: + uses: ./.github/workflows/native-windows.yml + musl: + uses: ./.github/workflows/native-musl.yml + bundle: + needs: [unix, windows, musl] + runs-on: ubuntu-24.04 + steps: + - name: Download verified platform payloads + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: native-platform-*-${{ github.sha }} + merge-multiple: true + path: native + - name: Upload universal native payload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: native-universal-${{ github.sha }} + path: native/*/*.node + if-no-files-found: error + overwrite: true + retention-days: 7 diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml index fef17c62b..2d306e249 100644 --- a/.github/workflows/native-unix.yml +++ b/.github/workflows/native-unix.yml @@ -1,18 +1,11 @@ name: native-unix on: - push: - branches: [main] - pull_request: - types: [opened, reopened, synchronize] + workflow_call: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - jobs: primitives: name: primitives / ${{ matrix.artifact }} @@ -125,7 +118,8 @@ jobs: - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: native-${{ matrix.artifact }}-${{ github.sha }} - path: plugins/codex-security/native/dist/${{ matrix.artifact }}/unix.node + name: native-platform-${{ matrix.artifact }}-${{ github.sha }} + path: plugins/codex-security/native/dist/*/*.node if-no-files-found: error + overwrite: true retention-days: 7 diff --git a/.github/workflows/native-windows.yml b/.github/workflows/native-windows.yml index e02212615..258ddf40a 100644 --- a/.github/workflows/native-windows.yml +++ b/.github/workflows/native-windows.yml @@ -1,18 +1,11 @@ name: native-windows on: - push: - branches: [main] - pull_request: - types: [opened, reopened, synchronize] + workflow_call: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - jobs: primitives: name: primitives / win32-${{ matrix.arch }} @@ -87,7 +80,8 @@ jobs: - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: native-win32-${{ matrix.arch }}-${{ github.sha }} - path: plugins/codex-security/native/dist/win32-${{ matrix.arch }}/windows.node + name: native-platform-win32-${{ matrix.arch }}-${{ github.sha }} + path: plugins/codex-security/native/dist/*/*.node if-no-files-found: error + overwrite: true retention-days: 7 diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 9a6855264..e1d6987dc 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -156,9 +156,14 @@ jobs: - name: Check formatting run: pnpm --dir sdk/typescript run format + native: + needs: validate-title + if: needs.validate-title.outputs.ci-mode == 'full' + uses: ./.github/workflows/native-artifacts.yml + package: name: build and check package - needs: validate-title + needs: [validate-title, native] if: needs.validate-title.outputs.ci-mode == 'full' runs-on: ubuntu-latest timeout-minutes: 20 @@ -169,6 +174,8 @@ jobs: persist-credentials: false - name: Check plugin source boundary run: node sdk/typescript/scripts/check-plugin-source.mjs + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: @@ -219,6 +226,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: @@ -319,6 +328,8 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: @@ -457,6 +468,8 @@ jobs: uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index 10c2975ee..de8f8988d 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -11,7 +11,12 @@ concurrency: queue: max jobs: + native: + if: github.repository == 'openai/codex-security' + uses: ./.github/workflows/native-artifacts.yml + verify: + needs: native if: github.repository == 'openai/codex-security' name: verify runs-on: ubuntu-latest @@ -33,6 +38,8 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - name: Set up Socket Firewall uses: SocketDev/action@937f824ec476dfd164d4a4d9995751427b0be143 # v1 diff --git a/.github/workflows/test-quality.yml b/.github/workflows/test-quality.yml index 859a25c20..0b21f15ba 100644 --- a/.github/workflows/test-quality.yml +++ b/.github/workflows/test-quality.yml @@ -20,7 +20,11 @@ env: CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST: "false" jobs: + native: + uses: ./.github/workflows/native-artifacts.yml + runner: + needs: native name: ${{ matrix.os }} / ${{ matrix.mode }} runs-on: ${{ matrix.os }} timeout-minutes: 30 @@ -63,6 +67,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: "22.13.0" diff --git a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs index 855062673..46d94271c 100644 --- a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs +++ b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { brotliCompressSync, constants as zlibConstants } from "node:zlib"; @@ -20,6 +20,19 @@ export async function buildMcpApp({ output }) { await mkdir(mcpDir, { recursive: true }); await writeRuntime("server", "main.ts"); + for (const target of [ + "darwin-arm64", "darwin-x64", + "linux-arm64-gnu", "linux-arm64-musl", "linux-x64-gnu", "linux-x64-musl", + "win32-arm64", "win32-x64" + ]) { + const name = target.startsWith("win32-") ? "windows.node" : "unix.node"; + const destination = join(mcpDir, "native", target); + await mkdir(destination, { recursive: true }); + await copyFile( + join(root, "../native/prebuilt", target, name), + join(destination, name) + ); + } async function writeRuntime(name, entryPoint) { const bundle = join(mcpDir, name + ".bundle.cjs"); diff --git a/plugins/codex-security/native/.gitignore b/plugins/codex-security/native/.gitignore index 5fb9d30c0..2e665804b 100644 --- a/plugins/codex-security/native/.gitignore +++ b/plugins/codex-security/native/.gitignore @@ -1,3 +1,4 @@ /target/ /dist/ +/prebuilt/ /*.mjs diff --git a/plugins/codex-security/plugin-files.json b/plugins/codex-security/plugin-files.json index b9f59c88d..d9c7e6167 100644 --- a/plugins/codex-security/plugin-files.json +++ b/plugins/codex-security/plugin-files.json @@ -11,6 +11,14 @@ "examples/completed-scan/findings.json", "examples/completed-scan/report.md", "examples/completed-scan/scan-manifest.json", + "mcp/native/darwin-arm64/unix.node", + "mcp/native/darwin-x64/unix.node", + "mcp/native/linux-arm64-gnu/unix.node", + "mcp/native/linux-arm64-musl/unix.node", + "mcp/native/linux-x64-gnu/unix.node", + "mcp/native/linux-x64-musl/unix.node", + "mcp/native/win32-arm64/windows.node", + "mcp/native/win32-x64/windows.node", "mcp/server.mjs", "mcp/server.mjs.br.part-000", "mcp/server.mjs.br.part-001", diff --git a/sdk/typescript/scripts/smoke-package.mjs b/sdk/typescript/scripts/smoke-package.mjs index 4e51bdcd3..c75e913f2 100644 --- a/sdk/typescript/scripts/smoke-package.mjs +++ b/sdk/typescript/scripts/smoke-package.mjs @@ -394,6 +394,39 @@ try { "Installed npm package does not match the complete bundled-plugin contract.", ); + const libc = + process.platform === "linux" + ? process.report.getReport().header.glibcVersionRuntime === undefined + ? "-musl" + : "-gnu" + : ""; + const nativeLibrary = join( + installedRoot, + "_bundled_plugin", + "mcp", + "native", + `${process.platform}-${process.arch}${libc}`, + process.platform === "win32" ? "windows.node" : "unix.node", + ); + run( + process.execPath, + [ + "--input-type=commonjs", + "--eval", + `const assert = require("node:assert/strict"); +const native = require(process.argv[1]); +if (process.platform === "win32") { + const result = native.openWindowsFile(Buffer.from(process.argv[2], "utf16le"), 0, 7, 3, 0); + assert.equal(result.error, 2); +} else { + assert.deepEqual(native.duplicate(-1), { value: -1, errno: 9 }); +}`, + nativeLibrary, + join(consumer, "missing-native-file"), + ], + { cwd: consumer, env: { ...process.env, PATH: "" } }, + ); + run( process.execPath, [ diff --git a/sdk/typescript/tests-ts/build-plugin.test.ts b/sdk/typescript/tests-ts/build-plugin.test.ts index b1097d920..edbe95657 100644 --- a/sdk/typescript/tests-ts/build-plugin.test.ts +++ b/sdk/typescript/tests-ts/build-plugin.test.ts @@ -101,7 +101,21 @@ describe("bundled plugin build", () => { }, ); - expect(await files(destination)).toContain("server.mjs"); + const contract = JSON.parse( + await readFile( + new URL( + "../../../plugins/codex-security/plugin-files.json", + import.meta.url, + ), + "utf8", + ), + ) as { shippedExact: string[] }; + expect(await files(destination)).toEqual( + contract.shippedExact + .filter((path) => path.startsWith("mcp/")) + .map((path) => path.slice(4)) + .sort(), + ); }); test("builds from a source snapshot without Git metadata", async () => { From 8b4ec71a06fc096cbab1fb4b07350af43aeca410 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 00:43:25 +0000 Subject: [PATCH 2/8] build(plugin): preserve native dependency notices --- .github/workflows/native-artifacts.yml | 2 +- .github/workflows/native-musl.yml | 14 +-- .github/workflows/native-unix.yml | 9 +- docker/README.md | 4 +- plugins/codex-security/mcp-app/TESTING.md | 2 +- .../mcp-app/scripts/build_mcp_app.mjs | 22 ++++- plugins/codex-security/native/README.md | 16 +++- .../codex-security/native/licenses/napi.txt | 43 +++++++++ plugins/codex-security/native/notices.mts | 89 +++++++++++++++++++ plugins/codex-security/plugin-files.json | 6 ++ sdk/typescript/TESTING.md | 2 + 11 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 plugins/codex-security/native/licenses/napi.txt create mode 100644 plugins/codex-security/native/notices.mts diff --git a/.github/workflows/native-artifacts.yml b/.github/workflows/native-artifacts.yml index b0426ced1..a59b2aeaf 100644 --- a/.github/workflows/native-artifacts.yml +++ b/.github/workflows/native-artifacts.yml @@ -28,7 +28,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: native-universal-${{ github.sha }} - path: native/*/*.node + path: native/ if-no-files-found: error overwrite: true retention-days: 7 diff --git a/.github/workflows/native-musl.yml b/.github/workflows/native-musl.yml index 98d241d4a..eef25fd43 100644 --- a/.github/workflows/native-musl.yml +++ b/.github/workflows/native-musl.yml @@ -1,18 +1,11 @@ name: native-musl on: - push: - branches: [main] - pull_request: - types: [opened, reopened, synchronize] + workflow_call: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - jobs: primitives: name: primitives / linux-${{ matrix.arch }}-musl @@ -106,7 +99,8 @@ jobs: - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: native-linux-${{ matrix.arch }}-musl-${{ github.sha }} - path: plugins/codex-security/native/dist/linux-${{ matrix.arch }}-musl/unix.node + name: native-platform-linux-${{ matrix.arch }}-musl-${{ github.sha }} + path: plugins/codex-security/native/dist/*/*.node if-no-files-found: error + overwrite: true retention-days: 7 diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml index 2d306e249..05f5ace5f 100644 --- a/.github/workflows/native-unix.yml +++ b/.github/workflows/native-unix.yml @@ -115,11 +115,18 @@ jobs: node check.mjs native_node="$(command -v node)" PATH= "$native_node" proof.mjs + - name: Prepare shared native notices + if: matrix.platform == 'linux' && matrix.arch == 'x64' + run: node notices.mjs - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: native-platform-${{ matrix.artifact }}-${{ github.sha }} - path: plugins/codex-security/native/dist/*/*.node + path: | + plugins/codex-security/native/dist/*/*.node + plugins/codex-security/native/dist/THIRD_PARTY_NOTICES.txt + plugins/codex-security/native/dist/COPYRIGHT-library.html + plugins/codex-security/native/dist/licenses/*.txt if-no-files-found: error overwrite: true retention-days: 7 diff --git a/docker/README.md b/docker/README.md index 50d9ce468..4517b6f06 100644 --- a/docker/README.md +++ b/docker/README.md @@ -55,6 +55,8 @@ than build arguments. Before the first release, an administrator must prepare the package: +Prepare the [universal native payload](../plugins/codex-security/native/README.md#package-inputs) before building an image from source. + 1. Allow organization package creation and, if the package is missing, bootstrap it with a reviewed image and a non-release tag: @@ -135,7 +137,7 @@ Git authentication uses the existing `GH_TOKEN`/`GITHUB_TOKEN` and optional the findings service's embedding credentials are configured separately. Use a version or digest in `CODEX_SECURITY_IMAGE` for repeatable deployments. To test an unreleased checkout, build the same scanner target locally instead -of pulling: +of pulling. First prepare the [universal native payload](../plugins/codex-security/native/README.md#package-inputs): ```bash docker build --target scanner -t codex-security:local . diff --git a/plugins/codex-security/mcp-app/TESTING.md b/plugins/codex-security/mcp-app/TESTING.md index 4b1787d79..a79b05197 100644 --- a/plugins/codex-security/mcp-app/TESTING.md +++ b/plugins/codex-security/mcp-app/TESTING.md @@ -1,6 +1,6 @@ # MCP tests -From `sdk/typescript`, install both packages' dependencies and run `pnpm run test:mcp`. That command builds the bundled plugin before testing it. To rerun only the tests after a build, run `pnpm run test:mcp` from this directory. +From `sdk/typescript`, install both packages' dependencies, prepare the [universal native payload](../native/README.md#package-inputs), and run `pnpm run test:mcp`. That command builds the bundled plugin before testing it. To rerun only the tests after a build, run `pnpm run test:mcp` from this directory. Node's test runner discovers `tests/test_*.mjs` and runs at most two files at once, each in a separate process. Assertions within a file remain sequential. Keep shared helpers outside that filename pattern. A failing script fails the command. The reporter streams Node's TAP output and then tries to write Node's JUnit report to `reports/junit.xml`; an unavailable report path warns without changing the test result. CI uploads that file with the Node 22 reports. diff --git a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs index 46d94271c..100724c61 100644 --- a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs +++ b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { brotliCompressSync, constants as zlibConstants } from "node:zlib"; import { execFileSync } from "node:child_process"; @@ -21,9 +21,14 @@ export async function buildMcpApp({ output }) { await writeRuntime("server", "main.ts"); for (const target of [ - "darwin-arm64", "darwin-x64", - "linux-arm64-gnu", "linux-arm64-musl", "linux-x64-gnu", "linux-x64-musl", - "win32-arm64", "win32-x64" + "darwin-arm64", + "darwin-x64", + "linux-arm64-gnu", + "linux-arm64-musl", + "linux-x64-gnu", + "linux-x64-musl", + "win32-arm64", + "win32-x64" ]) { const name = target.startsWith("win32-") ? "windows.node" : "unix.node"; const destination = join(mcpDir, "native", target); @@ -33,6 +38,15 @@ export async function buildMcpApp({ output }) { join(destination, name) ); } + for (const path of [ + "THIRD_PARTY_NOTICES.txt", "COPYRIGHT-library.html", + "licenses/MIT.txt", "licenses/Apache-2.0.txt", + "licenses/Unicode-3.0.txt", "licenses/BSD-2-Clause.txt" + ]) { + const destination = join(mcpDir, "native", path); + await mkdir(dirname(destination), { recursive: true }); + await copyFile(join(root, "../native/prebuilt", path), destination); + } async function writeRuntime(name, entryPoint) { const bundle = join(mcpDir, name + ".bundle.cjs"); diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index 4a4f804df..da7a64506 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -31,7 +31,7 @@ node plugins/codex-security/native/check.mjs GNU Linux artifacts must import no glibc version newer than 2.28. Musl artifacts must be ELF images for the current architecture, depend on that architecture's musl library, and have no version requirements from glibc. GCC's own `GLIBC_2.0` compatibility exports are attributed to `libgcc_s.so.1`, not the C library. Musl has no glibc-style symbol version floor, so its runtime compatibility also requires the load proofs below. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer GNU Linux workstation can pass the behavioral proof and still fail this distribution check. -The `native-unix` workflow builds Linux artifacts in digest-pinned manylinux 2.28 images. It mounts the pinned Rust toolchain and fetched Cargo registry, builds offline, and blocks Python commands during compilation. macOS builds set `MACOSX_DEPLOYMENT_TARGET=11.0`. CI verifies separate x64 and arm64 artifacts on both platforms using Node 20.0.0 and 22.13.0. These artifacts are inputs to the later universal-package gate. +The `native-unix` workflow builds Linux artifacts in digest-pinned manylinux 2.28 images. It mounts the pinned Rust toolchain and fetched Cargo registry, builds offline, and blocks Python commands during compilation. macOS builds set `MACOSX_DEPLOYMENT_TARGET=11.0`. CI verifies separate x64 and arm64 artifacts on both platforms using Node 20.0.0 and 22.13.0. The `native-musl` workflow uses native x64 and arm64 Ubuntu workers with digest-pinned Rust 1.97.1 Alpine compiler images. Musl builds disable static CRT linkage so Node can load the shared library. After the ELF and private-path checks, each unchanged artifact runs the full proof in pinned Node 20.0.0 Alpine 3.17 and Node 22.13.0 Alpine 3.21 images, with musl 1.2.3 and 1.2.5 respectively. Compilation uses the locked registry offline; runtime containers mount only the source and artifact read-only. Python is absent, and proof processes receive an empty `PATH`. @@ -52,3 +52,17 @@ The `native-windows` workflow builds x64 and arm64 with MSVC and a static CRT. I ```sh node --expose-gc plugins/codex-security/native/proof-windows.mjs python plugins/codex-security/scripts ``` + +## Package inputs + +The `native-artifacts` workflow calls all three platform workflows and combines their eight verified payloads into `native-universal-`. Package, release, container, and test workflows prepare this artifact before building the plugin. The standalone MCP builder and npm package include the same complete `mcp/native` tree; neither compiles nor downloads code at runtime. + +The GNU x64 job also runs `notices.mjs` against the locked Cargo metadata. It collects crate licenses and the pinned Rust standard-library notices for both package surfaces. The NAPI crates omit license files from their registry archives, so `licenses/napi.txt` preserves their [pinned upstream license](https://github.com/napi-rs/napi-rs/blob/956e4525fea6a676ea3680b711382f167b899af9/LICENSE). Review that override when upgrading those dependencies. + +Before local plugin builds, tests, or Docker builds, select a successful run for the checkout's native sources. You can run `native-artifacts` manually on a pushed branch. Use the artifact name shown by that run; pull-request artifacts use the tested merge commit. From the repository root: + +```sh +gh run download --name native-universal- --dir plugins/codex-security/native/prebuilt +``` + +The ignored `prebuilt` directory must contain all eight platform directories and the shared notices. Refresh it after changing the native source or build toolchain. Missing payloads fail the build, including on hosts that only load one of them. Installed-package checks load the matching artifact with an empty `PATH`. diff --git a/plugins/codex-security/native/licenses/napi.txt b/plugins/codex-security/native/licenses/napi.txt new file mode 100644 index 000000000..7fe7e35ef --- /dev/null +++ b/plugins/codex-security/native/licenses/napi.txt @@ -0,0 +1,43 @@ +MIT License + +Copyright (c) 2020-present LongYinan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +MIT License + +Copyright (c) 2018 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/codex-security/native/notices.mts b/plugins/codex-security/native/notices.mts new file mode 100644 index 000000000..e26f4f58b --- /dev/null +++ b/plugins/codex-security/native/notices.mts @@ -0,0 +1,89 @@ +import { execFileSync } from "node:child_process"; +import { + copyFile, + mkdir, + readFile, + readdir, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { root } from "./binding.mjs"; + +interface Package { + name: string; + version: string; + source: string | null; + manifest_path: string; +} + +const metadata = JSON.parse( + execFileSync( + "cargo", + ["metadata", "--locked", "--offline", "--format-version", "1"], + { + cwd: root, + encoding: "utf8", + }, + ), +) as { packages: Package[] }; +const packages = metadata.packages + .filter((entry) => entry.source?.startsWith("registry+")) + .sort((left, right) => { + const leftName = `${left.name}@${left.version}`; + const rightName = `${right.name}@${right.version}`; + return leftName < rightName ? -1 : leftName > rightName ? 1 : 0; + }); +// These archives omit LICENSE; their pinned upstream revisions share this text. +const napiLicense = new Set([ + "napi@3.12.2", + "napi-build@2.4.1", + "napi-derive@3.6.3", + "napi-derive-backend@6.1.2", + "napi-sys@3.3.0", +]); +const notices: string[] = []; +for (const entry of packages) { + const directory = dirname(entry.manifest_path); + const files = (await readdir(directory, { withFileTypes: true })) + .filter( + (file) => + file.isFile() && + /^(?:licen[sc]e|copying|copyright)(?:$|[._-])/iu.test(file.name), + ) + .map((file) => join(directory, file.name)) + .sort(); + const name = `${entry.name}@${entry.version}`; + if (files.length === 0 && napiLicense.has(name)) { + files.push(join(root, "licenses", "napi.txt")); + } + if (files.length === 0) + throw new Error(`Missing native dependency license: ${name}`); + notices.push( + `${name}\n\n${(await Promise.all(files.map((file) => readFile(file, "utf8")))).join("\n\n")}`, + ); +} + +const destination = join(root, "dist"); +await mkdir(join(destination, "licenses"), { recursive: true }); +await writeFile( + join(destination, "THIRD_PARTY_NOTICES.txt"), + notices.join("\n\n---\n\n"), +); +const sysroot = execFileSync("rustc", ["--print", "sysroot"], { + cwd: root, + encoding: "utf8", +}).trim(); +const rustNotices = join(sysroot, "share", "doc", "rust"); +await copyFile( + join(rustNotices, "COPYRIGHT-library.html"), + join(destination, "COPYRIGHT-library.html"), +); +for (const license of ["MIT", "Apache-2.0", "Unicode-3.0", "BSD-2-Clause"]) { + await copyFile( + join(rustNotices, "licenses", `${license}.txt`), + join(destination, "licenses", `${license}.txt`), + ); +} +console.log( + `Prepared native notices for ${packages.length} registry packages and the Rust standard library.`, +); diff --git a/plugins/codex-security/plugin-files.json b/plugins/codex-security/plugin-files.json index d9c7e6167..dd454eafe 100644 --- a/plugins/codex-security/plugin-files.json +++ b/plugins/codex-security/plugin-files.json @@ -11,12 +11,18 @@ "examples/completed-scan/findings.json", "examples/completed-scan/report.md", "examples/completed-scan/scan-manifest.json", + "mcp/native/COPYRIGHT-library.html", + "mcp/native/THIRD_PARTY_NOTICES.txt", "mcp/native/darwin-arm64/unix.node", "mcp/native/darwin-x64/unix.node", "mcp/native/linux-arm64-gnu/unix.node", "mcp/native/linux-arm64-musl/unix.node", "mcp/native/linux-x64-gnu/unix.node", "mcp/native/linux-x64-musl/unix.node", + "mcp/native/licenses/Apache-2.0.txt", + "mcp/native/licenses/BSD-2-Clause.txt", + "mcp/native/licenses/MIT.txt", + "mcp/native/licenses/Unicode-3.0.txt", "mcp/native/win32-arm64/windows.node", "mcp/native/win32-x64/windows.node", "mcp/server.mjs", diff --git a/sdk/typescript/TESTING.md b/sdk/typescript/TESTING.md index 11a7c1074..b538f948b 100644 --- a/sdk/typescript/TESTING.md +++ b/sdk/typescript/TESTING.md @@ -2,6 +2,8 @@ Use the pnpm version in `package.json` and Bun 1.3.14, matching required CI. Install both the SDK and MCP app dependencies before building or testing. +Prepare the [universal native payload](../../plugins/codex-security/native/README.md#package-inputs) +before running a command that builds the bundled plugin. Run these commands from `sdk/typescript`: ```sh From b2882946edb246fb6f78144d0894e7a31ae00cb8 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 01:01:22 +0000 Subject: [PATCH 3/8] fix(ci): prepare native notices before container build --- .github/workflows/native-unix.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml index 05f5ace5f..01e15ff84 100644 --- a/.github/workflows/native-unix.yml +++ b/.github/workflows/native-unix.yml @@ -66,6 +66,9 @@ jobs: cargo fmt --check cargo clippy --locked -- -D warnings cargo fetch --locked + - name: Prepare shared native notices + if: matrix.platform == 'linux' && matrix.arch == 'x64' + run: node notices.mjs - name: Build and verify Linux on glibc 2.28 with Node.js 22 if: matrix.platform == 'linux' run: | @@ -115,9 +118,6 @@ jobs: node check.mjs native_node="$(command -v node)" PATH= "$native_node" proof.mjs - - name: Prepare shared native notices - if: matrix.platform == 'linux' && matrix.arch == 'x64' - run: node notices.mjs - name: Upload verified native artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: From 1d029b2d0dafb10697b79f27b33ad0575211079a Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 01:06:33 +0000 Subject: [PATCH 4/8] test(ci): account for reusable native build jobs --- sdk/typescript/tests-ts/skeleton.test.ts | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index 5636217c8..fc0c8d65b 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -25,7 +25,7 @@ interface WorkflowJob { needs?: string | string[]; env?: Record; strategy?: { matrix: Record }; - steps: WorkflowStep[]; + steps?: WorkflowStep[]; } async function workflow(name: string) { @@ -141,7 +141,7 @@ describe("TypeScript package skeleton", () => { run: "node scripts/check-package.mjs ../../dist/*.tgz", }), ); - expect(jobs[name]?.steps.some(({ name }) => name === "Set up Bun")).toBe( + expect(jobs[name]?.steps!.some(({ name }) => name === "Set up Bun")).toBe( false, ); } @@ -169,7 +169,7 @@ describe("TypeScript package skeleton", () => { test("checks one archive and restores its plugin before every test shard", async () => { const { jobs } = await workflow("node-ci.yml"); - const uploads = jobs["package"]!.steps; + const uploads = jobs["package"]!.steps!; const inspection = uploads.findIndex( ({ name }) => name === "Inspect archive contents", ); @@ -193,13 +193,13 @@ describe("TypeScript package skeleton", () => { const job = jobs[name]!; expect(job.needs).toContain("package"); expect( - job.steps.find( + job.steps!.find( ({ name }) => name === "Download package for this commit", )?.with, ).toEqual({ name: "package-${{ github.sha }}", path: "dist" }); } for (const name of ["test", "windows-test", "mcp"]) { - const steps = jobs[name]!.steps; + const steps = jobs[name]!.steps!; const restore = steps.findIndex( ({ name }) => name === "Restore bundled plugin", ); @@ -218,28 +218,28 @@ describe("TypeScript package skeleton", () => { test("installs ripgrep before the independent MCP job", async () => { const { jobs } = await workflow("node-ci.yml"); - const steps = jobs["mcp"]!.steps; + const steps = jobs["mcp"]!.steps!; const ripgrep = steps.findIndex(({ name }) => name === "Install ripgrep"); const tests = steps.findIndex(({ name }) => name === "Test MCP app"); expect(steps[ripgrep]?.run).toContain("apt-get install --yes ripgrep"); expect(ripgrep).toBeLessThan(tests); expect( - jobs["test"]!.steps.some(({ name }) => name === "Test MCP app"), + jobs["test"]!.steps!.some(({ name }) => name === "Test MCP app"), ).toBe(false); }); test("runs static checks independently and keeps diagnostic uploads non-blocking", async () => { const { jobs } = await workflow("node-ci.yml"); - const steps = Object.values(jobs).flatMap((job) => job.steps); + const steps = Object.values(jobs).flatMap((job) => job.steps ?? []); expect(jobs["static-checks"]?.needs).toBe("validate-title"); - expect(jobs["package"]?.needs).toBe("validate-title"); + expect(jobs["package"]?.needs).toEqual(["validate-title", "native"]); for (const [name, job] of [ ["Check plugin source boundary", "package"], ["Typecheck", "static-checks"], ["Check formatting", "static-checks"], ] as const) { expect(steps.filter((step) => step.name === name)).toHaveLength(1); - expect(jobs[job]!.steps.some((step) => step.name === name)).toBe(true); + expect(jobs[job]!.steps!.some((step) => step.name === name)).toBe(true); } for (const name of [ "Upload test reports", @@ -253,7 +253,7 @@ describe("TypeScript package skeleton", () => { }); } expect( - jobs["plugin-source"]!.steps.find( + jobs["plugin-source"]!.steps!.find( ({ name }) => name === "Test Python source contracts", )?.run, ).toContain( @@ -263,7 +263,7 @@ describe("TypeScript package skeleton", () => { test("keeps machine-wide policy changes out of parallel and experimental runs", async () => { const ci = await workflow("node-ci.yml"); - const windows = ci.jobs["windows-test"]!.steps; + const windows = ci.jobs["windows-test"]!.steps!; expect( windows.find((step) => step.name === "Test shard ${{ matrix.shard }}") ?.env?.["CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST"], @@ -324,7 +324,7 @@ describe("TypeScript package skeleton", () => { args, }); } - const command = runner.steps.find( + const command = runner.steps!.find( (step) => step.name === "Test runner mode", )?.run; expect(command).toContain( @@ -334,7 +334,7 @@ describe("TypeScript package skeleton", () => { expect(command).toContain("--seed=${{ env.CODEX_SECURITY_PROPERTY_SEED }}"); const uploads = [...Object.values(ci.jobs), ...Object.values(quality.jobs)] - .flatMap((job) => job.steps) + .flatMap((job) => job.steps ?? []) .filter((step) => step.uses?.startsWith("actions/upload-artifact@")); for (const upload of uploads) { expect(upload.with?.["overwrite"]).toBe(true); @@ -346,7 +346,7 @@ describe("TypeScript package skeleton", () => { uploads.find((step) => step.name === "Upload runner report"), ).not.toHaveProperty("continue-on-error"); expect( - quality.jobs["mutation"]?.steps.find( + quality.jobs["mutation"]?.steps!.find( (step) => step.name === "Run mutation trial", ), ).not.toHaveProperty("continue-on-error"); @@ -379,7 +379,7 @@ describe("TypeScript package skeleton", () => { for (const workflowName of ["node-ci.yml", "node-release.yml"]) { const { jobs } = await workflow(workflowName); const audits = Object.values(jobs) - .flatMap((job) => job.steps) + .flatMap((job) => job.steps ?? []) .filter((step) => step.name === "Audit production dependencies"); expect(audits.length).toBeGreaterThan(0); for (const audit of audits) { From 150e97d67c6cd6fbe7ff612a964701534b88bcc9 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Thu, 3 Sep 2026 18:12:03 +0000 Subject: [PATCH 5/8] test(ci): avoid stale Windows synchronous subprocess timers --- .../deep-scan-reducer-recovery.test.ts | 30 ++++++++++++------- sdk/typescript/tests-ts/test-shards.test.ts | 28 +++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/sdk/typescript/tests-ts/deep-scan-reducer-recovery.test.ts b/sdk/typescript/tests-ts/deep-scan-reducer-recovery.test.ts index b3905c4ab..471e3b83e 100644 --- a/sdk/typescript/tests-ts/deep-scan-reducer-recovery.test.ts +++ b/sdk/typescript/tests-ts/deep-scan-reducer-recovery.test.ts @@ -1,4 +1,3 @@ -import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -66,19 +65,28 @@ test("advertises distinct Standard worker and Deep reducer contracts", async () ); expect(Object.keys(servers)).toEqual(["cs_artifacts"]); const server = servers["cs_artifacts"]!; - const result = spawnSync(node!, server.args, { - encoding: "utf8", + const child = Bun.spawn({ + cmd: [node!, ...server.args], env: { ...process.env, ...server.env }, - input: [ - '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"codex-security-test","version":"1.0.0"}}}', - '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}', - '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}', - "", - ].join("\n"), + stdin: Buffer.from( + [ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"codex-security-test","version":"1.0.0"}}}', + '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}', + '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}', + "", + ].join("\n"), + ), + stdout: "pipe", + stderr: "pipe", timeout: 30_000, }); - expect(result.status, result.stderr).toBe(0); - const response = result.stdout + const [status, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + expect(status, stderr).toBe(0); + const response = stdout .trim() .split("\n") .map((line) => JSON.parse(line) as { id?: number; result?: unknown }) diff --git a/sdk/typescript/tests-ts/test-shards.test.ts b/sdk/typescript/tests-ts/test-shards.test.ts index a416db464..6dcf02d42 100644 --- a/sdk/typescript/tests-ts/test-shards.test.ts +++ b/sdk/typescript/tests-ts/test-shards.test.ts @@ -1,4 +1,3 @@ -import { spawnSync } from "node:child_process"; import { copyFile, mkdir, @@ -188,18 +187,21 @@ test("isolated timeout", async () => { ); try { - const result = spawnSync( - process.execPath, - ["test", "--timeout", "30000", fixture], - { - encoding: "utf8", - env: { ...process.env, CODEX_SECURITY_TEST_TIMEOUT_MS: "100" }, - timeout: 30_000, - windowsHide: true, - }, - ); - expect(result.status, result.stderr || result.error?.message).toBe(1); - expect(result.stderr).toContain("this test timed out after 100ms"); + const child = Bun.spawn({ + cmd: [process.execPath, "test", "--timeout", "30000", fixture], + env: { ...process.env, CODEX_SECURITY_TEST_TIMEOUT_MS: "100" }, + stdin: "ignore", + stdout: "ignore", + stderr: "pipe", + timeout: 30_000, + windowsHide: true, + }); + const [status, stderr] = await Promise.all([ + child.exited, + new Response(child.stderr).text(), + ]); + expect(status, stderr).toBe(1); + expect(stderr).toContain("this test timed out after 100ms"); } finally { await rm(directory, { recursive: true, force: true }); } From c2e13a721ea04e20376dcde8b9c9fe0d6b2c954f Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Fri, 4 Sep 2026 22:32:00 +0000 Subject: [PATCH 6/8] fix(plugin): derive native copies from the package manifest --- .../mcp-app/scripts/build_mcp_app.mjs | 26 +++---------------- sdk/typescript/README.md | 8 ++++-- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs index 100724c61..5a856e3c2 100644 --- a/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs +++ b/plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs @@ -20,29 +20,9 @@ export async function buildMcpApp({ output }) { await mkdir(mcpDir, { recursive: true }); await writeRuntime("server", "main.ts"); - for (const target of [ - "darwin-arm64", - "darwin-x64", - "linux-arm64-gnu", - "linux-arm64-musl", - "linux-x64-gnu", - "linux-x64-musl", - "win32-arm64", - "win32-x64" - ]) { - const name = target.startsWith("win32-") ? "windows.node" : "unix.node"; - const destination = join(mcpDir, "native", target); - await mkdir(destination, { recursive: true }); - await copyFile( - join(root, "../native/prebuilt", target, name), - join(destination, name) - ); - } - for (const path of [ - "THIRD_PARTY_NOTICES.txt", "COPYRIGHT-library.html", - "licenses/MIT.txt", "licenses/Apache-2.0.txt", - "licenses/Unicode-3.0.txt", "licenses/BSD-2-Clause.txt" - ]) { + const contract = JSON.parse(await readFile(join(root, "../plugin-files.json"), "utf8")); + for (const file of contract.shippedExact.filter((path) => path.startsWith("mcp/native/"))) { + const path = file.slice("mcp/native/".length); const destination = join(mcpDir, "native", path); await mkdir(dirname(destination), { recursive: true }); await copyFile(join(root, "../native/prebuilt", path), destination); diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 6cbee5318..b0775c060 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -1217,7 +1217,9 @@ checkout or Node.js installation is required. `CODEX_SECURITY_FINDINGS_IMAGE` defaults to `ghcr.io/openai/codex-security:latest`. Set it to a published version, `sha-` tag, or digest for repeatable deployments. -To build from a source checkout instead: +To build from a source checkout, first prepare the +[universal native payload](../../plugins/codex-security/native/README.md#package-inputs) +for that checkout. Then run from the repository root: ```bash docker build --target scanner -t codex-security:local . @@ -1764,7 +1766,9 @@ Export `OPENAI_API_KEY` or `CODEX_API_KEY` to import findings with embeddings. Startup and listing need no key. The service does not load `.env` or authenticate requests; keep it on loopback or behind an authenticated TLS proxy. -From a source checkout's `sdk/typescript` directory: +For a source build, first prepare the +[universal native payload](../../plugins/codex-security/native/README.md#package-inputs) +for that checkout. Then run from its `sdk/typescript` directory: ```bash pnpm install --frozen-lockfile From 9cbcca67ae29ce78be085c8124b52c5c008ac6c4 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sat, 5 Sep 2026 00:28:40 +0000 Subject: [PATCH 7/8] ci: share native artifacts across pull request checks --- .github/workflows/container-ci.yml | 25 +-- .github/workflows/container-release.yml | 225 +---------------------- .github/workflows/container-validate.yml | 220 ++++++++++++++++++++++ .github/workflows/node-ci.yml | 61 +++++- .github/workflows/test-quality.yml | 13 +- plugins/codex-security/native/README.md | 2 +- sdk/typescript/scripts/smoke-package.mjs | 10 +- 7 files changed, 299 insertions(+), 257 deletions(-) create mode 100644 .github/workflows/container-validate.yml diff --git a/.github/workflows/container-ci.yml b/.github/workflows/container-ci.yml index cd5499b6c..07e5d58d2 100644 --- a/.github/workflows/container-ci.yml +++ b/.github/workflows/container-ci.yml @@ -17,36 +17,29 @@ on: - docker/** - plugins/codex-security/** - sdk/typescript/** - pull_request: - paths: - - .dockerignore - - .github/workflows/native-*.yml - - .github/actions/download-native/** - - .github/workflows/container-ci.yml - - Dockerfile - - Dockerfile.dockerignore - - compose.yaml - - compose.apparmor.yaml - - compose.findings.yaml - - compose.runner.yaml - - docker/** - - plugins/codex-security/** - - sdk/typescript/** + workflow_call: + inputs: + native-artifacts-ready: + description: Use the native artifacts already built by the caller in this run. + type: boolean + default: false workflow_dispatch: permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + group: container-ci-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: native: + if: ${{ !inputs.native-artifacts-ready }} uses: ./.github/workflows/native-artifacts.yml container: needs: native + if: ${{ !cancelled() && (inputs.native-artifacts-ready || needs.native.result == 'success') }} name: linux-amd64 runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 8d4adcd91..679f4fe20 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -1,21 +1,6 @@ name: container-release on: - pull_request: - paths: - - .dockerignore - - .github/workflows/native-*.yml - - .github/actions/download-native/** - - .github/workflows/container-release.yml - - Dockerfile - - Dockerfile.dockerignore - - compose.yaml - - compose.apparmor.yaml - - compose.findings.yaml - - compose.runner.yaml - - docker/** - - plugins/codex-security/** - - sdk/typescript/** push: tags: - "container-v*" @@ -36,215 +21,7 @@ jobs: validate: needs: native if: github.repository == 'openai/codex-security' - name: validate-linux-${{ matrix.architecture }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - include: - - architecture: amd64 - runner: ubuntu-24.04 - - architecture: arm64 - runner: ubuntu-24.04-arm - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Prepare native runtime - uses: ./.github/actions/download-native - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Build native customer image - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - env: - DOCKER_BUILD_RECORD_UPLOAD: "false" - with: - context: . - load: true - platforms: linux/${{ matrix.architecture }} - push: false - tags: codex-security:release-candidate - cache-from: type=gha,scope=codex-security-${{ matrix.architecture }} - cache-to: ${{ github.event_name != 'pull_request' && format('type=gha,mode=max,scope=codex-security-{0}', matrix.architecture) || '' }} - - - name: Verify native image - env: - EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} - shell: bash - run: | - set -euo pipefail - actual_architecture="$(docker image inspect --format '{{.Architecture}}' codex-security:release-candidate)" - if [[ "$actual_architecture" != "$EXPECTED_ARCHITECTURE" ]]; then - echo "Expected a native $EXPECTED_ARCHITECTURE image; found $actual_architecture." >&2 - exit 1 - fi - docker run --rm codex-security:release-candidate --version - docker run --rm codex-security:release-candidate bulk-scan --help - docker run --rm codex-security:release-candidate info --json - [[ "$(docker run --rm --entrypoint id codex-security:release-candidate -u)" == 10001 ]] - - - name: Verify host-aware AppArmor sandbox selection - shell: bash - run: | - set -euo pipefail - docker run --rm --entrypoint /bin/sh codex-security:release-candidate -ec ' - command_directory="$(mktemp -d)" - trap '\''rm -rf "$command_directory"'\'' EXIT - printf "%s\\n" "#!/bin/sh" '\''printf "%s\\n" "$@"'\'' > "$command_directory/codex-security" - chmod 755 "$command_directory/codex-security" - - actual="$( - PATH="$command_directory:$PATH" \ - /usr/local/bin/codex-security-entrypoint \ - bulk-scan /input/repositories.csv --output-dir /output - )" - restricted_user_namespaces= - if [ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then - IFS= read -r restricted_user_namespaces \ - < /proc/sys/kernel/apparmor_restrict_unprivileged_userns || true - fi - - apparmor_profile= - if [ -r /proc/self/attr/current ]; then - IFS= read -r apparmor_profile < /proc/self/attr/current || true - fi - - if [ "$restricted_user_namespaces" = 1 ] && - [ "$apparmor_profile" != "codex-security-container (enforce)" ]; then - printf "%s\\n" "$actual" | grep -Fxq features.use_legacy_landlock=true - elif printf "%s\\n" "$actual" | grep -Fxq features.use_legacy_landlock=true; then - printf "%s\\n" "Landlock must not be forced when the preferred sandbox is available." >&2 - exit 1 - fi - ' - - - name: Verify hardened Codex command sandbox - shell: bash - run: | - set -euo pipefail - command=( - docker run --rm - --cap-drop ALL - --security-opt no-new-privileges - --security-opt "seccomp=$GITHUB_WORKSPACE/docker/codex-security-seccomp.json" - --entrypoint node - codex-security:release-candidate - /usr/local/lib/node_modules/@openai/codex-security/node_modules/@openai/codex/bin/codex.js - ) - - if output="$("${command[@]}" sandbox /usr/bin/true 2>&1)"; then - printf '%s\n' "$output" - elif grep -Eq 'bwrap: (Failed to make / slave: Permission denied|loopback: Failed RTM_NEW(ADDR|LINK): Operation not permitted|setting up uid map: Permission denied|No permissions to create a new namespace)' <<< "$output"; then - echo '::notice::This Docker host blocks nested Bubblewrap namespaces; verifying the supported Landlock fallback.' - "${command[@]}" sandbox --enable use_legacy_landlock /usr/bin/true - else - printf 'The hardened Codex sandbox failed unexpectedly:\n%s\n' "$output" >&2 - exit 1 - fi - - - name: Verify host-scoped Git credentials - shell: bash - run: | - set -euo pipefail - docker run --rm \ - --entrypoint /bin/sh \ - --env GH_TOKEN=SYNTHETIC_GITHUB_TOKEN \ - codex-security:release-candidate \ - -ec 'actual="$(printf "protocol=https\nhost=github.com\n\n" | /usr/local/bin/codex-security-git-credential get)"; test "$actual" = "$(printf "username=x-access-token\npassword=SYNTHETIC_GITHUB_TOKEN")"; test -z "$(printf "protocol=https\nhost=untrusted.example\n\n" | /usr/local/bin/codex-security-git-credential get)"' - - - name: Verify hardened customer Compose configuration - env: - CODEX_SECURITY_IMAGE: codex-security:release-candidate - shell: bash - run: | - set -euo pipefail - mkdir -p results state - chmod 700 results state - printf 'id,repository,revision\n' > repositories.csv - CODEX_SECURITY_USER="$(id -u):$(id -g)" - export CODEX_SECURITY_USER - docker compose config --quiet - docker compose run --rm codex-security --version - if output="$(docker compose run --rm codex-security 2>&1)"; then - echo 'An empty repository CSV must not start a security scan.' >&2 - exit 1 - else - status=$? - fi - if [[ "$status" -ne 2 ]] || ! grep -Fq 'Multiscan CSV must contain at least one repository.' <<< "$output"; then - printf 'Unexpected empty-repository scan behavior:\n%s\n' "$output" >&2 - exit 1 - fi - - - name: Verify optional hardened AppArmor Compose override - env: - CODEX_SECURITY_IMAGE: codex-security:release-candidate - shell: bash - run: | - set -euo pipefail - CODEX_SECURITY_USER="$(id -u):$(id -g)" - export CODEX_SECURITY_USER - compose=(docker compose -f compose.yaml -f compose.apparmor.yaml) - - "${compose[@]}" config --format json | - jq --exit-status ' - .services["codex-security"].security_opt as $options | - ($options | index("apparmor=codex-security-container")) != null and - ($options | index("no-new-privileges:true")) != null and - any($options[]; startswith("seccomp=")) - ' > /dev/null - - if ! docker info --format '{{json .SecurityOptions}}' | - grep -Fq '"name=apparmor"'; then - echo '::notice::This Docker host does not expose AppArmor; the default customer workflow remains available.' - exit 0 - fi - - sudo install -m 0644 docker/codex-security.apparmor \ - /etc/apparmor.d/codex-security-container - sudo apparmor_parser -r -W /etc/apparmor.d/codex-security-container - sudo grep -Fxq 'codex-security-container (enforce)' \ - /sys/kernel/security/apparmor/profiles - - # The single-quoted program is evaluated inside the customer container. - # shellcheck disable=SC2016 - "${compose[@]}" run --rm --entrypoint /bin/sh codex-security -ec ' - test "$(cat /proc/self/attr/current)" = "codex-security-container (enforce)" - command_directory="$(mktemp -d)" - trap '\''rm -rf "$command_directory"'\'' EXIT - printf "%s\\n" "#!/bin/sh" '\''printf "%s\\n" "$@"'\'' \ - > "$command_directory/codex-security" - chmod 755 "$command_directory/codex-security" - actual="$( - PATH="$command_directory:$PATH" \ - /usr/local/bin/codex-security-entrypoint \ - bulk-scan /input/repositories.csv --output-dir /output - )" - if printf "%s\\n" "$actual" | - grep -Fxq features.use_legacy_landlock=true; then - printf "%s\\n" "The AppArmor profile must retain the preferred Codex sandbox." >&2 - exit 1 - fi - ' - - "${compose[@]}" run --rm --entrypoint node codex-security \ - /usr/local/lib/node_modules/@openai/codex-security/node_modules/@openai/codex/bin/codex.js \ - sandbox /usr/bin/true - - - name: Set up Bun for findings service verification - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: "1.3.14" - - - name: Verify findings API and persistent storage through consumer Compose - env: - IMAGE: codex-security:release-candidate - run: bun sdk/typescript/scripts/smoke-findings-service.ts "$IMAGE" + uses: ./.github/workflows/container-validate.yml authorize: if: github.repository == 'openai/codex-security' && github.event_name != 'pull_request' diff --git a/.github/workflows/container-validate.yml b/.github/workflows/container-validate.yml new file mode 100644 index 000000000..43cec482e --- /dev/null +++ b/.github/workflows/container-validate.yml @@ -0,0 +1,220 @@ +name: container-validate + +on: + workflow_call: + +permissions: + contents: read + +jobs: + validate: + if: github.repository == 'openai/codex-security' + name: validate-linux-${{ matrix.architecture }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Prepare native runtime + uses: ./.github/actions/download-native + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Build native customer image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + env: + DOCKER_BUILD_RECORD_UPLOAD: "false" + with: + context: . + load: true + platforms: linux/${{ matrix.architecture }} + push: false + tags: codex-security:release-candidate + cache-from: type=gha,scope=codex-security-${{ matrix.architecture }} + cache-to: ${{ github.event_name != 'pull_request' && format('type=gha,mode=max,scope=codex-security-{0}', matrix.architecture) || '' }} + + - name: Verify native image + env: + EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} + shell: bash + run: | + set -euo pipefail + actual_architecture="$(docker image inspect --format '{{.Architecture}}' codex-security:release-candidate)" + if [[ "$actual_architecture" != "$EXPECTED_ARCHITECTURE" ]]; then + echo "Expected a native $EXPECTED_ARCHITECTURE image; found $actual_architecture." >&2 + exit 1 + fi + docker run --rm codex-security:release-candidate --version + docker run --rm codex-security:release-candidate bulk-scan --help + docker run --rm codex-security:release-candidate info --json + [[ "$(docker run --rm --entrypoint id codex-security:release-candidate -u)" == 10001 ]] + + - name: Verify host-aware AppArmor sandbox selection + shell: bash + run: | + set -euo pipefail + docker run --rm --entrypoint /bin/sh codex-security:release-candidate -ec ' + command_directory="$(mktemp -d)" + trap '\''rm -rf "$command_directory"'\'' EXIT + printf "%s\\n" "#!/bin/sh" '\''printf "%s\\n" "$@"'\'' > "$command_directory/codex-security" + chmod 755 "$command_directory/codex-security" + + actual="$( + PATH="$command_directory:$PATH" \ + /usr/local/bin/codex-security-entrypoint \ + bulk-scan /input/repositories.csv --output-dir /output + )" + restricted_user_namespaces= + if [ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then + IFS= read -r restricted_user_namespaces \ + < /proc/sys/kernel/apparmor_restrict_unprivileged_userns || true + fi + + apparmor_profile= + if [ -r /proc/self/attr/current ]; then + IFS= read -r apparmor_profile < /proc/self/attr/current || true + fi + + if [ "$restricted_user_namespaces" = 1 ] && + [ "$apparmor_profile" != "codex-security-container (enforce)" ]; then + printf "%s\\n" "$actual" | grep -Fxq features.use_legacy_landlock=true + elif printf "%s\\n" "$actual" | grep -Fxq features.use_legacy_landlock=true; then + printf "%s\\n" "Landlock must not be forced when the preferred sandbox is available." >&2 + exit 1 + fi + ' + + - name: Verify hardened Codex command sandbox + shell: bash + run: | + set -euo pipefail + command=( + docker run --rm + --cap-drop ALL + --security-opt no-new-privileges + --security-opt "seccomp=$GITHUB_WORKSPACE/docker/codex-security-seccomp.json" + --entrypoint node + codex-security:release-candidate + /usr/local/lib/node_modules/@openai/codex-security/node_modules/@openai/codex/bin/codex.js + ) + + if output="$("${command[@]}" sandbox /usr/bin/true 2>&1)"; then + printf '%s\n' "$output" + elif grep -Eq 'bwrap: (Failed to make / slave: Permission denied|loopback: Failed RTM_NEW(ADDR|LINK): Operation not permitted|setting up uid map: Permission denied|No permissions to create a new namespace)' <<< "$output"; then + echo '::notice::This Docker host blocks nested Bubblewrap namespaces; verifying the supported Landlock fallback.' + "${command[@]}" sandbox --enable use_legacy_landlock /usr/bin/true + else + printf 'The hardened Codex sandbox failed unexpectedly:\n%s\n' "$output" >&2 + exit 1 + fi + + - name: Verify host-scoped Git credentials + shell: bash + run: | + set -euo pipefail + docker run --rm \ + --entrypoint /bin/sh \ + --env GH_TOKEN=SYNTHETIC_GITHUB_TOKEN \ + codex-security:release-candidate \ + -ec 'actual="$(printf "protocol=https\nhost=github.com\n\n" | /usr/local/bin/codex-security-git-credential get)"; test "$actual" = "$(printf "username=x-access-token\npassword=SYNTHETIC_GITHUB_TOKEN")"; test -z "$(printf "protocol=https\nhost=untrusted.example\n\n" | /usr/local/bin/codex-security-git-credential get)"' + + - name: Verify hardened customer Compose configuration + env: + CODEX_SECURITY_IMAGE: codex-security:release-candidate + shell: bash + run: | + set -euo pipefail + mkdir -p results state + chmod 700 results state + printf 'id,repository,revision\n' > repositories.csv + CODEX_SECURITY_USER="$(id -u):$(id -g)" + export CODEX_SECURITY_USER + docker compose config --quiet + docker compose run --rm codex-security --version + if output="$(docker compose run --rm codex-security 2>&1)"; then + echo 'An empty repository CSV must not start a security scan.' >&2 + exit 1 + else + status=$? + fi + if [[ "$status" -ne 2 ]] || ! grep -Fq 'Multiscan CSV must contain at least one repository.' <<< "$output"; then + printf 'Unexpected empty-repository scan behavior:\n%s\n' "$output" >&2 + exit 1 + fi + + - name: Verify optional hardened AppArmor Compose override + env: + CODEX_SECURITY_IMAGE: codex-security:release-candidate + shell: bash + run: | + set -euo pipefail + CODEX_SECURITY_USER="$(id -u):$(id -g)" + export CODEX_SECURITY_USER + compose=(docker compose -f compose.yaml -f compose.apparmor.yaml) + + "${compose[@]}" config --format json | + jq --exit-status ' + .services["codex-security"].security_opt as $options | + ($options | index("apparmor=codex-security-container")) != null and + ($options | index("no-new-privileges:true")) != null and + any($options[]; startswith("seccomp=")) + ' > /dev/null + + if ! docker info --format '{{json .SecurityOptions}}' | + grep -Fq '"name=apparmor"'; then + echo '::notice::This Docker host does not expose AppArmor; the default customer workflow remains available.' + exit 0 + fi + + sudo install -m 0644 docker/codex-security.apparmor \ + /etc/apparmor.d/codex-security-container + sudo apparmor_parser -r -W /etc/apparmor.d/codex-security-container + sudo grep -Fxq 'codex-security-container (enforce)' \ + /sys/kernel/security/apparmor/profiles + + # The single-quoted program is evaluated inside the customer container. + # shellcheck disable=SC2016 + "${compose[@]}" run --rm --entrypoint /bin/sh codex-security -ec ' + test "$(cat /proc/self/attr/current)" = "codex-security-container (enforce)" + command_directory="$(mktemp -d)" + trap '\''rm -rf "$command_directory"'\'' EXIT + printf "%s\\n" "#!/bin/sh" '\''printf "%s\\n" "$@"'\'' \ + > "$command_directory/codex-security" + chmod 755 "$command_directory/codex-security" + actual="$( + PATH="$command_directory:$PATH" \ + /usr/local/bin/codex-security-entrypoint \ + bulk-scan /input/repositories.csv --output-dir /output + )" + if printf "%s\\n" "$actual" | + grep -Fxq features.use_legacy_landlock=true; then + printf "%s\\n" "The AppArmor profile must retain the preferred Codex sandbox." >&2 + exit 1 + fi + ' + + "${compose[@]}" run --rm --entrypoint node codex-security \ + /usr/local/lib/node_modules/@openai/codex-security/node_modules/@openai/codex/bin/codex.js \ + sandbox /usr/bin/true + + - name: Set up Bun for findings service verification + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.14" + + - name: Verify findings API and persistent storage through consumer Compose + env: + IMAGE: codex-security:release-candidate + run: bun sdk/typescript/scripts/smoke-findings-service.ts "$IMAGE" diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index e1d6987dc..009590f04 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -20,6 +20,9 @@ jobs: timeout-minutes: 10 outputs: ci-mode: ${{ steps.scope.outputs.ci-mode }} + container-ci: ${{ steps.scope.outputs.container-ci }} + container-validate: ${{ steps.scope.outputs.container-validate }} + test-quality: ${{ steps.scope.outputs.test-quality }} steps: - name: Checkout pull request for change classification @@ -39,6 +42,14 @@ jobs: set -euo pipefail ci_mode=full check_markdown=false + container_ci=false + container_validate=false + test_quality=false + if [[ "$EVENT_NAME" == "pull_request" && "$BASE_CHANGED" == "true" ]]; then + container_ci=true + container_validate=true + test_quality=true + fi if [[ "$EVENT_NAME" == "pull_request" && "$BASE_CHANGED" != "true" ]]; then changed_files="$(mktemp)" @@ -57,6 +68,30 @@ jobs: "$path" == sdk/typescript/_bundled_plugin/* ]]; then plugin_changed=true fi + case "$path" in + .dockerignore | .github/workflows/native-*.yml | \ + .github/actions/download-native/* | Dockerfile | \ + Dockerfile.dockerignore | compose.yaml | compose.apparmor.yaml | \ + compose.findings.yaml | compose.runner.yaml | docker/* | \ + plugins/codex-security/* | sdk/typescript/*) + container_ci=true + container_validate=true + ;; + .github/workflows/container-ci.yml) + container_ci=true + ;; + .github/workflows/container-release.yml | .github/workflows/container-validate.yml) + container_validate=true + ;; + .github/workflows/node-ci.yml) + container_ci=true + container_validate=true + test_quality=true + ;; + .github/workflows/test-quality.yml) + test_quality=true + ;; + esac done < "$changed_files" if [[ "$changed" == "true" && "$markdown_only" == "true" ]]; then check_markdown=true @@ -67,6 +102,9 @@ jobs: fi printf 'ci-mode=%s\n' "$ci_mode" >> "$GITHUB_OUTPUT" printf 'check-markdown=%s\n' "$check_markdown" >> "$GITHUB_OUTPUT" + printf 'container-ci=%s\n' "$container_ci" >> "$GITHUB_OUTPUT" + printf 'container-validate=%s\n' "$container_validate" >> "$GITHUB_OUTPUT" + printf 'test-quality=%s\n' "$test_quality" >> "$GITHUB_OUTPUT" - name: Require a Conventional Commit pull request title if: github.event_name == 'pull_request' @@ -158,9 +196,28 @@ jobs: native: needs: validate-title - if: needs.validate-title.outputs.ci-mode == 'full' + if: needs.validate-title.outputs.ci-mode == 'full' || needs.validate-title.outputs.container-ci == 'true' || needs.validate-title.outputs.container-validate == 'true' || needs.validate-title.outputs.test-quality == 'true' uses: ./.github/workflows/native-artifacts.yml + container-ci: + needs: [validate-title, native] + if: needs.validate-title.outputs.container-ci == 'true' + uses: ./.github/workflows/container-ci.yml + with: + native-artifacts-ready: true + + container-validate: + needs: [validate-title, native] + if: needs.validate-title.outputs.container-validate == 'true' + uses: ./.github/workflows/container-validate.yml + + test-quality: + needs: [validate-title, native] + if: needs.validate-title.outputs.test-quality == 'true' + uses: ./.github/workflows/test-quality.yml + with: + native-artifacts-ready: true + package: name: build and check package needs: [validate-title, native] @@ -328,8 +385,6 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - name: Prepare native runtime - uses: ./.github/actions/download-native - name: Set up pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: diff --git a/.github/workflows/test-quality.yml b/.github/workflows/test-quality.yml index 0b21f15ba..81c71835e 100644 --- a/.github/workflows/test-quality.yml +++ b/.github/workflows/test-quality.yml @@ -1,9 +1,12 @@ name: test-quality on: - pull_request: - paths: - - .github/workflows/test-quality.yml + workflow_call: + inputs: + native-artifacts-ready: + description: Use the native artifacts already built by the caller in this run. + type: boolean + default: false workflow_dispatch: schedule: - cron: "23 9 * * 1" @@ -12,7 +15,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: test-quality-${{ github.ref }} cancel-in-progress: true env: @@ -21,10 +24,12 @@ env: jobs: native: + if: ${{ !inputs.native-artifacts-ready }} uses: ./.github/workflows/native-artifacts.yml runner: needs: native + if: ${{ !cancelled() && (inputs.native-artifacts-ready || needs.native.result == 'success') }} name: ${{ matrix.os }} / ${{ matrix.mode }} runs-on: ${{ matrix.os }} timeout-minutes: 30 diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index da7a64506..bbc3175de 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -55,7 +55,7 @@ node --expose-gc plugins/codex-security/native/proof-windows.mjs python plugins/ ## Package inputs -The `native-artifacts` workflow calls all three platform workflows and combines their eight verified payloads into `native-universal-`. Package, release, container, and test workflows prepare this artifact before building the plugin. The standalone MCP builder and npm package include the same complete `mcp/native` tree; neither compiles nor downloads code at runtime. +The `native-artifacts` workflow calls all three platform workflows and combines their eight verified payloads into `native-universal-`. PR validation jobs share one artifact assembled by `node-ci`; release and standalone validation runs assemble their own. The standalone MCP builder and npm package include the same complete `mcp/native` tree; neither compiles nor downloads code at runtime. The GNU x64 job also runs `notices.mjs` against the locked Cargo metadata. It collects crate licenses and the pinned Rust standard-library notices for both package surfaces. The NAPI crates omit license files from their registry archives, so `licenses/napi.txt` preserves their [pinned upstream license](https://github.com/napi-rs/napi-rs/blob/956e4525fea6a676ea3680b711382f167b899af9/LICENSE). Review that override when upgrading those dependencies. diff --git a/sdk/typescript/scripts/smoke-package.mjs b/sdk/typescript/scripts/smoke-package.mjs index c75e913f2..eea6fc99a 100644 --- a/sdk/typescript/scripts/smoke-package.mjs +++ b/sdk/typescript/scripts/smoke-package.mjs @@ -413,16 +413,8 @@ try { [ "--input-type=commonjs", "--eval", - `const assert = require("node:assert/strict"); -const native = require(process.argv[1]); -if (process.platform === "win32") { - const result = native.openWindowsFile(Buffer.from(process.argv[2], "utf16le"), 0, 7, 3, 0); - assert.equal(result.error, 2); -} else { - assert.deepEqual(native.duplicate(-1), { value: -1, errno: 9 }); -}`, + "require(process.argv[1])", nativeLibrary, - join(consumer, "missing-native-file"), ], { cwd: consumer, env: { ...process.env, PATH: "" } }, ); From bfd9a2699f7bbef18eec4cdf89876c4bad75688d Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sat, 5 Sep 2026 00:41:11 +0000 Subject: [PATCH 8/8] test: update reusable workflow contract --- sdk/typescript/tests-ts/skeleton.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/tests-ts/skeleton.test.ts b/sdk/typescript/tests-ts/skeleton.test.ts index fc0c8d65b..5e40f2cbb 100644 --- a/sdk/typescript/tests-ts/skeleton.test.ts +++ b/sdk/typescript/tests-ts/skeleton.test.ts @@ -279,12 +279,15 @@ describe("TypeScript package skeleton", () => { }); const quality = await workflow("test-quality.yml"); expect(Object.keys(quality.on).sort()).toEqual([ - "pull_request", "schedule", + "workflow_call", "workflow_dispatch", ]); - expect(quality.on["pull_request"]).toEqual({ - paths: [".github/workflows/test-quality.yml"], + expect(ci.jobs["test-quality"]).toMatchObject({ + needs: ["validate-title", "native"], + if: "needs.validate-title.outputs.test-quality == 'true'", + uses: "./.github/workflows/test-quality.yml", + with: { "native-artifacts-ready": true }, }); expect(quality.env?.["CODEX_SECURITY_ALLOW_MACHINE_POLICY_TEST"]).toBe( "false",