Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions .github/workflows/codex-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ on:
description: Optional previous stable tag override
required: false
type: string
upload_sarif:
description: Upload manual-run results to Code Scanning
required: false
default: false
type: boolean
allow_full_bootstrap:
description: Allow a full scan when no previous stable tag exists
required: false
Expand All @@ -65,13 +70,16 @@ concurrency:
cancel-in-progress: true

env:
CODEX_SECURITY_MAX_CONCURRENT_THREADS: "8"
CODEX_SECURITY_REASONING_EFFORT: medium
NVIDIA_INFERENCE_BASE_URL: https://inference-api.nvidia.com/v1
NVIDIA_INFERENCE_MODEL: openai/openai/gpt-5.6-sol

jobs:
analyze:
name: Codex Security (${{ inputs.candidate_ref || github.ref_name }})
# The agent executes no shell commands on the repository self-hosted runner,
# so its preflight never scopes the diff and it seals no draft.
runs-on: ubuntu-latest
timeout-minutes: 120
outputs:
Expand All @@ -93,6 +101,17 @@ jobs:
with:
python-version: "3.14"

# Codex confines model-run commands with bubblewrap, which needs
# unprivileged user namespaces. Ubuntu 24.04 restricts those through
# AppArmor, so bubblewrap cannot set up the sandbox network namespace and
# the scan agent executes nothing at all.
- name: Allow unprivileged user namespaces
run: |
set -euo pipefail
if [ -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi

- name: Install Codex Security
run: |
set -euo pipefail
Expand All @@ -111,6 +130,25 @@ jobs:
test -x "$CODEX_SECURITY_BIN"
"$CODEX_SECURITY_BIN" --version

# The range resolver has to come from the workflow's own revision. A
# scanned candidate predates it, and running the resolver from the
# revision under scan would let that revision pick its own scan range.
- name: Check out the workflow revision
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: workflow-revision
sparse-checkout: tasks/scripts
persist-credentials: false

- name: Stage the range resolver
run: |
set -euo pipefail
install -d -m 700 "$RUNNER_TEMP/range-resolver"
cp workflow-revision/tasks/scripts/release.py \
workflow-revision/tasks/scripts/codex_security_range.py \
"$RUNNER_TEMP/range-resolver/"
rm -rf workflow-revision

- name: Check out the pre-release
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -139,17 +177,17 @@ jobs:
args+=(--allow-full-bootstrap)
fi

node tasks/scripts/codex-security-release-range.mjs "${args[@]}"
python3 "$RUNNER_TEMP/range-resolver/codex_security_range.py" "${args[@]}"

- name: Scan changes since the previous stable
env:
BASE_SHA: ${{ steps.range.outputs.base_sha }}
CODEX_SECURITY_BIN: ${{ runner.temp }}/codex-security/node_modules/.bin/codex-security
CODEX_SECURITY_STATE_DIR: ${{ runner.temp }}/codex-security-state
CODEX_SECURITY_STATE_DIR: ${{ runner.temp }}/codex-security-state-${{ github.run_id }}-${{ github.run_attempt }}
HEAD_SHA: ${{ steps.range.outputs.candidate_sha }}
NVIDIA_INFERENCE_API_KEY: ${{ secrets.CODEX_SECURITY_API_KEY }}
OPENAI_API_KEY: ${{ secrets.CODEX_SECURITY_API_KEY }}
SCAN_DIR: ${{ runner.temp }}/codex-security-results
SCAN_DIR: ${{ runner.temp }}/codex-security-results-${{ github.run_id }}-${{ github.run_attempt }}
SCAN_SCOPE: ${{ steps.range.outputs.scan_scope }}
run: |
set -euo pipefail
Expand All @@ -174,14 +212,16 @@ jobs:
--codex 'model_providers.nvidia.env_key="NVIDIA_INFERENCE_API_KEY"' \
--codex 'model_providers.nvidia.wire_api="responses"' \
--codex 'model_providers.nvidia.supports_websockets=false' \
--codex "features.multi_agent_v2.max_concurrent_threads_per_session=$CODEX_SECURITY_MAX_CONCURRENT_THREADS" \
--codex 'approval_policy="never"' \
--output-dir "$SCAN_DIR" \
--headless > /dev/null

- name: Export SARIF
env:
CODEX_SECURITY_BIN: ${{ runner.temp }}/codex-security/node_modules/.bin/codex-security
SARIF_FILE: ${{ runner.temp }}/codex-security.sarif
SCAN_DIR: ${{ runner.temp }}/codex-security-results
SCAN_DIR: ${{ runner.temp }}/codex-security-results-${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
"$CODEX_SECURITY_BIN" export "$SCAN_DIR" \
Expand All @@ -205,6 +245,7 @@ jobs:
echo
echo "- Train: \`$TRAIN\`"
echo "- Candidate: \`$CANDIDATE_TAG\`"
echo "- Maximum concurrent agent threads: $CODEX_SECURITY_MAX_CONCURRENT_THREADS"
if [ "$SCAN_SCOPE" = "diff" ]; then
echo "- Previous stable: \`$BASE_TAG\`"
echo "- Commits in cumulative diff: $COMMIT_COUNT"
Expand All @@ -218,6 +259,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload SARIF to Code Scanning
if: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_sarif }}
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
sarif_file: ${{ runner.temp }}/codex-security.sarif
Expand All @@ -227,7 +269,7 @@ jobs:

result:
name: OpenShell / Codex Security (informational)
if: always()
if: ${{ always() }}
needs: analyze
runs-on: ubuntu-latest
permissions: {}
Expand Down
75 changes: 58 additions & 17 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ the release tag.

## CI and E2E

Required checks run on GitHub Actions. Workflows that use NVIDIA self-hosted runners trigger from copy-pr-bot mirror branches, so trusted PRs are mirrored into `pull-request/<N>` branches before those workflows run. `main` also uses GitHub merge queue so the final queued integration commit is validated before it merges.
Required checks run on GitHub Actions. Pull-request workflows that use NVIDIA self-hosted runners trigger from copy-pr-bot mirror branches, so trusted PRs are mirrored into `pull-request/<N>` branches before those workflows run. `main` also uses GitHub merge queue so the final queued integration commit is validated before it merges.

The high-level CI model:

Expand All @@ -307,16 +307,19 @@ synthetic activity from contributing to product usage metrics.
Static security checks are deliberately outside the mirror-branch path. They run
directly on GitHub-hosted runners and none of them consume NVIDIA self-hosted
capacity. The change-oriented ones receive no secrets, so they also cover fork
pull requests; Codex Security release qualification is the exception because it
needs a scoped API key. That key routes Codex Security's model calls to
NVIDIA-hosted inference; the job itself still runs on a GitHub-hosted runner and
uses no NVIDIA self-hosted runner. Scanner jobs request `security-events: write`
and upload SARIF to Code Scanning directly on every event they run on, including
fork and Dependabot pull requests, which Code Scanning permits for
pull requests. Codex Security release qualification is the exception: it needs a
scoped API key, which routes its model calls to NVIDIA-hosted inference while
the job itself stays GitHub-hosted. That placement is load-bearing rather than
incidental: on the repository self-hosted runner the scan agent executes no
shell commands at all, so its preflight never scopes the diff and it seals no
draft. Scanner jobs request `security-events: write` and upload SARIF to Code
Scanning directly on every event they run on, including fork and Dependabot
pull requests, which Code Scanning permits for
`pull_request` runs despite their read-only `GITHUB_TOKEN`. No privileged
intermediate workflow relays those uploads. Report retention differs by scanner:
Actionlint, Zizmor, and CodeQL keep their reports as workflow artifacts, and
Codex Security keeps no raw report.
intermediate workflow relays those uploads. Manually dispatched Codex Security
runs are the one opt-in exception, described below. Report retention differs by
scanner: Actionlint, Zizmor, and CodeQL keep their reports as workflow artifacts,
and Codex Security keeps no raw report.
Triggers differ by workflow: `.github/workflows/workflow-security.yml` runs on
`pull_request`, `merge_group`, `main`, and a weekly schedule;
`.github/workflows/dependency-review.yml` runs on `pull_request` and
Expand Down Expand Up @@ -361,11 +364,23 @@ a pull request or merge group.
calls go to NVIDIA-hosted inference at `https://inference-api.nvidia.com/v1`,
declared as a custom Codex provider named `nvidia` that uses the Responses
wire API with WebSockets disabled. The scan runs `openai/openai/gpt-5.6-sol`
at `medium` reasoning effort. The `CODEX_SECURITY_API_KEY` secret holds the
at `medium` reasoning effort, with the multi-agent runtime capped at eight
concurrent threads through
`features.multi_agent_v2.max_concurrent_threads_per_session`. The
`CODEX_SECURITY_API_KEY` secret holds the
NVIDIA key and is exposed to the scan step alone, as `OPENAI_API_KEY` so the
CLI selects API-key auth and as `NVIDIA_INFERENCE_API_KEY`, the provider
`env_key` read by the Codex child process.
`tasks/scripts/codex-security-release-range.mjs` resolves the scan range: the
`env_key` read by the Codex child process. `CODEX_SECURITY_STATE_DIR` and
`SCAN_DIR` are suffixed with `github.run_id` and `github.run_attempt` and
created mode `700`, so no scanner state or result set from a previous run or
retry attempt is reused even on a runner with a reusable temp directory.
`tasks/scripts/codex_security_range.py` resolves the scan range, reusing the
tag parsers in `tasks/scripts/release.py` so both stay on one definition of a
release tag while requiring the `v` prefix that a release workflow needs. The
job stages both files out of the workspace from the workflow's own revision
and runs the resolver by absolute path, because a scanned candidate predates
them and a revision under scan must not choose its own scan range. The range
itself is resolved against the checked-out candidate: the
candidate must be a `vX.Y.Z-pre.N` tag that is an ancestor of `origin/main`,
and the base is the newest stable `vX.Y.Z` tag merged into the candidate that
is strictly older than the release train `vX.Y.Z` the candidate targets. A
Expand All @@ -374,12 +389,38 @@ a pull request or merge group.
stable-to-candidate diff, so later candidates re-cover earlier ones. SARIF is
uploaded against `refs/heads/main` at the candidate commit under the
train-scoped category `codex-security/vX.Y.Z`, which makes each candidate's
analysis replace the previous one for that train. Codex Security 0.1.24 cannot
apply `--max-cost` to a slash-qualified model identifier, so the run has no
CLI-enforced cost ceiling. Spend is bounded instead by the 120-minute job
timeout, a single repository-wide concurrency group that serializes
analysis replace the previous one for that train. Automatic pre-release tag
pushes and `workflow_call` runs always upload. `workflow_dispatch` runs still
perform the scan and the SARIF export, but skip the Code Scanning upload
unless the caller sets the `upload_sarif` input, so manual diagnostics do not
overwrite a train's published analysis by default. Codex Security 0.1.24
cannot apply `--max-cost` to a slash-qualified model identifier, so the run
has no CLI-enforced cost ceiling. Spend is bounded instead by the 120-minute
job timeout, a single repository-wide concurrency group that serializes
qualification so starting a newer candidate cancels an in-flight one, and
NVIDIA account-side controls. No raw report is retained.
- The job clears `kernel.apparmor_restrict_unprivileged_userns` before
installing the scanner. Codex confines model-run commands with bubblewrap,
which needs unprivileged user namespaces; Ubuntu 24.04 restricts those through
AppArmor, so bubblewrap fails to configure the sandbox network namespace
(`bwrap: loopback: Failed RTM_NEWADDR`) and the agent executes no commands at
all. The failure is silent: the agent retries its shell tool, gives up, and
seals no draft, while the scanner only reports a missing or incomplete draft.
Lifting a kernel restriction on the runner is what allows the sandbox that
confines the agent to start, and the runner is ephemeral and GitHub-hosted.
- The scan sets `approval_policy="never"`. Codex Security keeps
`approvals_reviewer="auto_review"` unconditionally, and that reviewer runs on
its own model rather than the configured one. Because the workflow declares a
single provider that serves only `openai/openai/gpt-5.6-sol`, any approval
request reaches a model the endpoint does not serve, so the agent never gets a
shell command approved and seals no draft. The scan stays confined by its
`workspace-write` sandbox with network access disabled and by the scanner's
own permission profile, which grants read access to the filesystem root and
write access only to the workspace roots.
- A scan that cannot execute commands reports only a missing or incomplete
draft, so diagnosing one means reading the scanner's session rollouts under
`CODEX_SECURITY_STATE_DIR`, where every shell command the agent ran is
recorded. No command at all is the signal that the sandbox failed to start.

Findings never fail these checks; scanner and build failures do. A scanner that
cannot run, a CodeQL analyzer that does not complete, an unexpected Dependency
Expand Down
Loading
Loading