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
16 changes: 15 additions & 1 deletion .github/workflows/plananvil-codex-qualification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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 }}
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion docs/CODEX_SANDBOX_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand All @@ -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-<run-id>` 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-<run-id>` 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

Expand Down
15 changes: 15 additions & 0 deletions tests/test_live_codex_qualification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()