diff --git a/.github/workflows/plananvil-codex-qualification.yml b/.github/workflows/plananvil-codex-qualification.yml index a6b5fe3..dab815d 100644 --- a/.github/workflows/plananvil-codex-qualification.yml +++ b/.github/workflows/plananvil-codex-qualification.yml @@ -84,6 +84,20 @@ jobs: git --version python3 --version + - name: Validate Linux Codex sandbox prerequisites + shell: bash + run: | + set -euo pipefail + if ! command -v bwrap >/dev/null 2>&1; then + echo "::error::System bubblewrap is required on the trusted Linux qualification runner. Install the Debian bubblewrap package instead of relying on the bundled Codex helper." + exit 2 + fi + bwrap --version + if ! bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / /bin/true; then + echo "::error::bubblewrap cannot create the user namespace required by the Codex command sandbox. Fix the Podman/host user-namespace policy before running C01-C16." + exit 2 + fi + - name: Create trusted main qualification workspace shell: bash run: | @@ -131,7 +145,7 @@ jobs: - name: Upload sanitized capability evidence if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: plananvil-codex-evidence-${{ github.run_id }} path: ${{ env.QUALIFICATION_ARTIFACT }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae4108..2bc375e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to PlanAnvil are documented here. ### Changed - update pinned `actions/checkout` and `actions/setup-python` workflow SHAs to the current v7 releases while retaining immutable action pinning and Node 24 compatibility; +- require the full Linux Codex qualification job to pass a system-`bubblewrap` user-namespace probe before C01-C16, so incompatible Podman runners fail fast instead of timing out capability-by-capability; +- update the qualification evidence uploader to the Node-24-native `actions/upload-artifact` v6 immutable SHA; - require production releases to use a GitHub-verified signed annotated tag whose target is reachable from `main`; - fail the production release gate closed when the release worktree is dirty or Git cleanliness cannot be verified; - document the controlled self-hosted Codex qualification path and keep the previous sandbox procedure as a manual fallback. diff --git a/docs/CODEX_SANDBOX_RUNBOOK.md b/docs/CODEX_SANDBOX_RUNBOOK.md index 64eba7f..98024a2 100644 --- a/docs/CODEX_SANDBOX_RUNBOOK.md +++ b/docs/CODEX_SANDBOX_RUNBOOK.md @@ -10,8 +10,19 @@ This is the remaining live-runtime step after deterministic/release hardening is - operating system recorded; - permission mode and project-trust mode recorded; - Git available and target fixture repositories disposable; +- system `bubblewrap` installed on Linux (`apt install bubblewrap` on Debian/Ubuntu); +- Linux user-namespace creation must work from the runner process; - no real credentials, private repository URLs, personal paths, or proprietary source in fixtures/evidence. +On a Podman-hosted runner, Codex creates an inner Linux command sandbox. The outer container must therefore permit the runner user to create the user namespace required by `bwrap`. Before a full qualification run, this probe must succeed inside the runner container: + +```text +command -v bwrap +bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / /bin/true +``` + +If the probe fails with `setting up uid map: Operation not permitted`, fix the Podman/host user-namespace policy rather than disabling Codex sandboxing. The qualification workflow deliberately does not use `danger-full-access` or `--dangerously-bypass-approvals-and-sandbox`. + ## Controlled GitHub Actions path The preferred qualification path is `.github/workflows/plananvil-codex-qualification.yml` in `full` mode. The workflow is intentionally `workflow_dispatch`-only, accepts execution only from `main`, uses Environment `plananvil-codex`, and targets `[self-hosted, linux, x64, plananvil, codex]`. @@ -20,7 +31,7 @@ The controlled runner must provide `plananvil-qualification-workspace`. The work Raw Codex session streams are not retained. The controller keeps only sanitized final assertions, event-type counts, and relative Git structure required for evaluation. The self-hosted runner has repository read permission only and never pushes qualification changes. -The workflow uploads `plananvil-codex-evidence-` as a short-lived artifact. Review that artifact before committing evidence through a normal protected pull request. A full workflow run exits successfully only when every release-gating capability is `REPRODUCED`; partial/failed runs still upload their sanitized evidence artifact for diagnosis. +The workflow performs the Linux `bubblewrap` user-namespace probe before preparing fixtures, so an incompatible container fails in seconds instead of consuming a full C01-C16 run. It uploads `plananvil-codex-evidence-` as a short-lived artifact. Review that artifact before committing evidence through a normal protected pull request. A full workflow run exits successfully only when every release-gating capability is `REPRODUCED`; partial/failed runs still upload their sanitized evidence artifact for diagnosis. ## Manual fallback diff --git a/tests/test_live_codex_qualification.py b/tests/test_live_codex_qualification.py index 3212b5d..1d967fe 100644 --- a/tests/test_live_codex_qualification.py +++ b/tests/test_live_codex_qualification.py @@ -104,6 +104,21 @@ def test_validate_plan_rejects_duplicate_trials(self) -> None: } self.assertIn("duplicate", live.validate_plan("C01", plan) or "") + def test_workflow_fails_fast_without_linux_user_namespace_sandbox(self) -> None: + workflow = (ROOT / ".github/workflows/plananvil-codex-qualification.yml").read_text( + encoding="utf-8" + ) + self.assertIn("command -v bwrap", workflow) + self.assertIn( + "bwrap --unshare-user --uid 0 --gid 0 --ro-bind / / /bin/true", + workflow, + ) + self.assertIn( + "actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6", + workflow, + ) + self.assertNotIn("actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02", workflow) + if __name__ == "__main__": unittest.main()