diff --git a/.codex/REVIEW.md b/.codex/REVIEW.md new file mode 100644 index 0000000..3c476dc --- /dev/null +++ b/.codex/REVIEW.md @@ -0,0 +1,43 @@ +# Codex project environment review + +## Applied + +- `.codex/config.toml` uses project-scoped, approval-gated workspace access with + agent shell network access disabled. +- `.codex/environments/environment.toml` replaces the former no-op setup with a + repository-local Python virtual environment and exposes the checks documented + by the repository and CI. + +## Evidence + +- `requirements.txt` is the repository's pinned pip dependency input. +- `AGENTS.md`, `README.md`, and `.github/workflows/ci.yml` define the unit, + hygiene, Compose-render, and Compose-security checks. +- The repository is a single Python service; no nested repositories, package + workspaces, database migrations, or private package registries were detected. + +## Boundaries and assumptions + +- Initial setup needs Python 3 and outbound access to the public Python package + index. Agent shell network access remains disabled by default and should be + enabled deliberately only for dependency installation or other reviewed work. +- Docker is required only for the Compose actions and image build; setup does + not start containers, services, databases, or deployments. +- `.env` and `data/` are intentionally not copied or generated. They can contain + the wrapper secret and a live container-scoped Codex login. +- The setup and actions use repository-relative paths and assume a POSIX shell. + Windows users should configure an app-generated platform-specific override. +- No hooks, rules, MCP servers, additional writable roots, or user-level Codex + settings are introduced. + +## Validation + +The project setup validator passed with no warnings. The setup command completed +successfully on macOS using Python 3.14, and the unit, hygiene, Compose-render, +and Compose-security actions passed. CI remains the authoritative Python 3.12 +and Linux verification surface. + +## Next safe action + +Review this configuration in the Codex app and confirm the generated environment +remains selected for new worktrees. diff --git a/.codex/config.toml b/.codex/config.toml index 513292a..9c4274a 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -2,3 +2,9 @@ # # Keep credentials and runtime state out of this file. Codex/ChatGPT auth for # the provider container belongs in data/codex-home, mounted at /root/.codex. + +approval_policy = "on-request" +sandbox_mode = "workspace-write" + +[sandbox_workspace_write] +network_access = false diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 0000000..8fc2e8e --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,26 @@ +# THIS IS AUTOGENERATED. REVIEW CHANGES IN THE CODEX APP. +version = 1 +name = "codex-cli-provider" + +[setup] +script = "python3 -m venv .venv && .venv/bin/python -m pip install --requirement requirements.txt" + +[[actions]] +name = "Unit tests" +icon = "tool" +command = "PYTHONPATH=. .venv/bin/python -m pytest -q" + +[[actions]] +name = "Repository hygiene" +icon = "tool" +command = ".venv/bin/python scripts/check_repo_hygiene.py" + +[[actions]] +name = "Compose security" +icon = "tool" +command = ".venv/bin/python scripts/check_compose_security.py" + +[[actions]] +name = "Compose render" +icon = "tool" +command = "docker compose config" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e3c2b3c..d95e494 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -11,32 +11,12 @@ on: permissions: contents: read - security-events: write concurrency: group: security-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - codeql: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - security-events: write - - steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - - - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - languages: python - - - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - dependency-audit: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.gitignore b/.gitignore index be87a03..5996ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ history.jsonl # Track reviewed project-level Codex config without exposing Codex home state. .codex/* !.codex/config.toml +!.codex/REVIEW.md +!.codex/environments/ +.codex/environments/* +!.codex/environments/environment.toml !.codex/rules/ .codex/rules/* !.codex/rules/*.rules diff --git a/AGENTS.md b/AGENTS.md index 2b52b1c..1bdd1a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,17 @@ docker compose up --build -d Use `docker-compose.image.yml` only when pulling a published image. +## Codex Worktrees + +- `.codex/environments/environment.toml` creates a repository-local `.venv`; + its setup needs public package-index access but must not start Docker or copy + ignored runtime state. +- Keep `.codex/config.toml` approval-gated and repository-scoped. Do not add + models, providers, credentials, user profiles, external writable roots, or + default agent-shell network access. +- Never copy `.env`, `data/`, Codex auth files, or registry credentials between + worktrees. + ## Verification Before handing off changes, run: @@ -68,3 +79,6 @@ The Dockerfile currently copies only `requirements.txt` and `src/`. Keep it that way unless there is a specific reason to widen the build context. `.dockerignore` excludes `.env`, `data/`, Codex auth files, virtualenvs, logs, and other local state that must not be published in images. + +Publishing an image, pushing a release tag, changing registry configuration, +or widening the service beyond loopback requires separate release approval. diff --git a/Dockerfile b/Dockerfile index f1696af..cf042ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.11-slim-bookworm +FROM python:3.12.13-slim-trixie ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ diff --git a/scripts/check_compose_security.py b/scripts/check_compose_security.py index ec9be6d..a637309 100644 --- a/scripts/check_compose_security.py +++ b/scripts/check_compose_security.py @@ -8,6 +8,7 @@ FORBIDDEN_ENV = {"OPENAI_API_KEY"} +FORBIDDEN_MOUNT_FRAGMENTS = ("/var/run/docker.sock", ".cli-proxy-api") def fail(message: str) -> None: @@ -36,6 +37,13 @@ def as_list(value: Any) -> list[Any]: return [value] +def check_forbidden_mount(source: str, target: str) -> None: + if any(fragment in source or fragment in target for fragment in FORBIDDEN_MOUNT_FRAGMENTS): + fail(f"forbidden mount detected: {source}:{target}") + if target in {"/home", "/root"} or source in {"/home", "/root"}: + fail(f"forbidden home/root mount detected: {source}:{target}") + + def main() -> None: os.chdir(Path(__file__).resolve().parents[1]) config = load_config() @@ -92,7 +100,6 @@ def main() -> None: codex_home_mount_ok = False codex_work_mount_ok = False secret_mount_ok = False - forbidden_fragments = ("/var/run/docker.sock", ".cli-proxy-api", "/runner") for volume in volumes: source = str(volume.get("source", "")) target = str(volume.get("target", "")) @@ -102,10 +109,7 @@ def main() -> None: codex_work_mount_ok = True elif target == "/run/secrets/proxy_api_key" and source.endswith("/data/secrets/proxy_api_key") and volume.get("read_only"): secret_mount_ok = True - if any(fragment in source or fragment in target for fragment in forbidden_fragments): - fail(f"forbidden mount detected: {source}:{target}") - if target in {"/home", "/root"} or source in {"/home", "/root"}: - fail(f"forbidden home/root mount detected: {source}:{target}") + check_forbidden_mount(source, target) if not codex_home_mount_ok: fail("expected project data/codex-home mount at /root/.codex") if not codex_work_mount_ok: diff --git a/scripts/check_repo_hygiene.py b/scripts/check_repo_hygiene.py index e1d26e8..d2affc8 100644 --- a/scripts/check_repo_hygiene.py +++ b/scripts/check_repo_hygiene.py @@ -7,6 +7,8 @@ ALLOWED_CODEX_PATHS = { ".codex/config.toml", + ".codex/environments/environment.toml", + ".codex/REVIEW.md", } FORBIDDEN_STAGEABLE_PATHS = ( diff --git a/tests/test_compose_security.py b/tests/test_compose_security.py new file mode 100644 index 0000000..04bad39 --- /dev/null +++ b/tests/test_compose_security.py @@ -0,0 +1,24 @@ +import pytest + +from scripts.check_compose_security import check_forbidden_mount + + +def test_github_runner_checkout_is_not_treated_as_a_forbidden_mount(): + check_forbidden_mount( + "/home/runner/work/codex-cli-provider/codex-cli-provider/data/codex-home", + "/root/.codex", + ) + + +@pytest.mark.parametrize( + ("source", "target"), + [ + ("/var/run/docker.sock", "/var/run/docker.sock"), + ("/tmp/.cli-proxy-api", "/workspace"), + ("/root", "/workspace"), + ("/tmp/project", "/home"), + ], +) +def test_dangerous_mounts_remain_forbidden(source, target): + with pytest.raises(SystemExit): + check_forbidden_mount(source, target) diff --git a/tests/test_repo_hygiene.py b/tests/test_repo_hygiene.py index 576fa23..ecbc1af 100644 --- a/tests/test_repo_hygiene.py +++ b/tests/test_repo_hygiene.py @@ -8,6 +8,18 @@ def test_codex_project_config_is_stageable(): check_paths([".codex/config.toml"]) +@pytest.mark.parametrize( + "path", + [ + ".codex/environments/environment.toml", + ".codex/REVIEW.md", + ], +) +def test_reviewed_codex_project_files_are_stageable(path): + assert is_allowed_codex_path(path) + check_paths([path]) + + def test_codex_rules_files_are_stageable(): assert is_allowed_codex_path(".codex/rules/default.rules") check_paths([".codex/rules/default.rules"])