From 6ee7d781f88878326c2fbc3f0e836939ff10f2c0 Mon Sep 17 00:00:00 2001 From: Antoine Zambelli Date: Fri, 31 Jul 2026 11:36:46 -0500 Subject: [PATCH] Restructure docs around conversational role dispatch --- README.md | 493 ++++++------------------------------- docs/artifact-lifecycle.md | 29 +++ docs/installation.md | 153 ++++++++++++ docs/security.md | 57 +++++ docs/usage.md | 201 +++++++++++++++ pyproject.toml | 2 +- scripts/prepare_release.py | 3 + tests/test_docs.py | 83 +++++-- tests/test_installer.py | 6 + 9 files changed, 582 insertions(+), 445 deletions(-) create mode 100644 docs/installation.md create mode 100644 docs/security.md create mode 100644 docs/usage.md diff --git a/README.md b/README.md index 7bfc7a3..4101d27 100644 --- a/README.md +++ b/README.md @@ -1,466 +1,117 @@ # codex-exec-subagents -An unofficial external role runner for the OpenAI Codex CLI. +Run named Codex roles as independent, auditable `codex exec` sessions. > **Unofficial project:** This project is not affiliated with or endorsed by -> OpenAI. Codex is a trademark of OpenAI. The project does not imitate OpenAI -> branding. +> OpenAI. Codex is a trademark of OpenAI. -Some current Codex runtimes can make configured custom-agent profiles -unreachable through the native spawn surface, leaving the requested role's -model, reasoning effort, sandbox, and instructions unapplied -([upstream issue #31097](https://github.com/openai/codex/issues/31097)). This -package is a pragmatic bridge while that behavior is repaired, not a -replacement for native subagents. +## What this is and why it exists -`codex-exec-subagents` launches independent `codex exec` sessions from global -role files and gives each session a durable handle, captured transport -artifacts, and exact-thread follow-up. It is useful when a developer wants -repeatable roles that can be launched, observed, resumed, and audited outside a -single interactive Codex conversation. +On affected Codex runtimes, the native spawn tool can no longer select a +configured custom role. A child may quietly run without that role's model, +reasoning effort, sandbox, or instructions +([upstream issue #31097](https://github.com/openai/codex/issues/31097)). -These are external root sessions, not native subagents. The runner supplies a -role file, the task you explicitly provide, and the selected working directory; -it does not pass along hidden parent-chat context, a shared parent UI, or -automatic task framing and coordination. Write self-contained tasks: name the -objective, boundaries, relevant paths, validation, and expected result. +That is especially wasteful when the main conversation runs on GPT-5.6-Sol. +Sol is useful as the orchestrator, but it is overkill for every repository +search or first-pass review. A configured GPT-5.6-Terra or Luna role should be +able to do that cheaper work; on affected runtimes, the Sol session cannot +reliably select it through the native tool. -## Platform and trust boundary +`codex-exec-subagents` is a practical workaround until that is fixed. It +launches the role you choose in an independent Codex session, returns a durable +handle, and keeps local evidence of what was requested and what Codex returned. +You can inspect a run, wait for it, read its result, and continue the exact same +Codex thread later. -Version 0.1 requires Python 3.11 or newer; Python 3.11, 3.12, or 3.13 are -tested on native Linux. The command and installer reject -macOS, WSL, native Windows, and unknown platforms before creating package -state, run artifacts, handles, or worker processes. - -`run` and `follow-up` require the `codex` executable to be discoverable on -`PATH` before the run root is prepared or read. Help, version, and example-role -surfaces are state-free and remain available without Codex: - -```bash -codex-exec-subagents --help -codex-exec-subagents --version -codex-exec-subagents example-role reviewer -codex-exec-subagents run --help -``` - -The security target is accidental exposure or deletion on a trusted -single-user native-Linux account. This package does not claim to isolate -hostile users, root, or a compromised host. See -[Security, privacy, and retention](#security-privacy-and-retention). +These are external sessions, not native subagents. They do not inherit hidden +parent-chat context or appear in a parent session's subagent UI. Give each role +a self-contained task and working directory. ## Install -### Stable installer +The supported platform is native Linux with Python 3.11 or newer and the Codex +CLI already available on `PATH`. -The primary install-or-update command is: +Install or update with `curl`: ```bash curl -fsSL https://antoinezambelli.github.io/codex-exec-subagents/install.sh | sh ``` -If `curl` is unavailable but `wget` is installed, use the equivalent bootstrap: +Or with `wget`: ```bash wget -qO- https://antoinezambelli.github.io/codex-exec-subagents/install.sh | sh ``` -Piping a remote script to a shell executes code that may have changed since -you last reviewed it. The equivalent inspect-first form downloads that same -script so you can review the exact bytes before running them: - -```bash -curl -fsSLo install.sh \ - https://antoinezambelli.github.io/codex-exec-subagents/install.sh -less install.sh -sh ./install.sh -``` - -Pin a normalized stable version: - -```bash -sh ./install.sh --version 0.1.0 -``` - -Rerun the unpinned command to update, or rerun a pin to install or verify that -exact version. Custom paths, including paths with spaces, are supported: - -```bash -sh ./install.sh \ - --install-root "$HOME/tools/codex-exec-subagents" \ - --bin-dir "$HOME/tools/bin" -``` - -The defaults are `~/.local/share/codex-exec-subagents/` for private -environments and `~/.local/bin/` for the public command link. The installer -does not edit shell startup files. If the bin directory is not an exact -colon-delimited `PATH` component, it prints the `export PATH=...` command -needed for the current shell. - -The installer checks native Linux, Python 3.11 or newer with `venv`, Codex, -either `curl` or `wget`, and `sha256sum`. It verifies the tagged wheel against -its checksum, stages and checks a new environment, and then promotes it. A -failed update leaves the previous verified command usable. Older environments -remain until uninstall so already-detached processes can finish; pin an earlier -hosted version to roll back. Changing `--bin-dir` requires uninstall and -reinstall. - -An inspect-first installation can reuse its reviewed local script. Use the -same path overrides used for installation: - -```bash -sh ./install.sh --uninstall -``` - -The primary piped installation does not retain `./install.sh`. Safely obtain a -fresh copy, review the exact bytes again, and then run uninstall: - -```bash -INSTALLER_COPY="$(mktemp)" -curl -fsSLo "$INSTALLER_COPY" \ - https://antoinezambelli.github.io/codex-exec-subagents/install.sh -less "$INSTALLER_COPY" -sh "$INSTALLER_COPY" --uninstall -rm -f "$INSTALLER_COPY" -``` - -If the piped install used custom paths, replace the uninstall line above with -the exact matching values: - -```bash -sh "$INSTALLER_COPY" --uninstall \ - --install-root "$HOME/tools/codex-exec-subagents" \ - --bin-dir "$HOME/tools/bin" -``` - -Uninstall validates the ownership marker and exact links, removes only -recognized installer slots and bookkeeping, and preserves unknown files. It -never inspects or removes any run root. - -### pipx - -`pipx` exposes the same runtime CLI: +Or install from PyPI with `pipx`: ```bash pipx install codex-exec-subagents -# or pin one stable release -pipx install codex-exec-subagents==0.1.0 -``` - -Use `pipx upgrade codex-exec-subagents` to update and -`pipx uninstall codex-exec-subagents` to uninstall. The runtime CLI is the -same; only `install.sh` provides the project-specific checksum, staged -promotion, retained environments, and ownership-aware uninstall described -above. - -### Source checkout - -For development from source: - -```bash -git clone https://github.com/antoinezambelli/codex-exec-subagents.git -cd codex-exec-subagents -python3 -m venv .venv -.venv/bin/python -m pip install -e '.[dev]' -source .venv/bin/activate -codex-exec-subagents --version -``` - -## Define roles - -Roles are user-created global TOML files at -`~/.codex/agents/.toml`. The package reads them but never installs, -rewrites, or silently modifies Codex configuration. Each role requires these -six nonempty string fields: `name`, `description`, `model`, -`model_reasoning_effort`, `sandbox_mode`, and `developer_instructions`. -`name` must match the requested filename. Supported declared sandboxes are -`read-only`, `workspace-write`, and `danger-full-access`. - -A small inspection role is maintained as -[`examples/agents/reviewer.toml`](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/examples/agents/reviewer.toml). -Print the same valid TOML from an installed command without changing -configuration: - -```bash -codex-exec-subagents example-role reviewer -``` - -Its contents are: - -```toml -# ~/.codex/agents/reviewer.toml -name = "reviewer" -description = "Read-only repository reviewer" -model = "gpt-5.6-terra" -model_reasoning_effort = "medium" -sandbox_mode = "read-only" -developer_instructions = "Inspect the requested repository. Do not edit files. Report evidence, risks, and missing validation concisely." -``` - -Inspect the output before redirecting it; shell redirection can overwrite an -existing role file. - -A trusted editing role: - -```toml -# ~/.codex/agents/implementer.toml -name = "implementer" -description = "Bounded repository implementer" -model = "gpt-5.6-sol" -model_reasoning_effort = "high" -sandbox_mode = "workspace-write" -developer_instructions = "Implement only the supplied task in the selected repository. Preserve unrelated work, run focused validation, and report changed files." ``` -The role sandbox is a declaration used by sandboxed execution. YOLO is a -separate execution policy, not a role capability. - -## Clone-to-result quickstart - -On native Linux, after creating `reviewer.toml` and installing both Codex and -this package: - -```bash -TARGET="$(git rev-parse --show-toplevel)" -codex-exec-subagents run reviewer \ - --cwd "$TARGET" \ - --sandboxed \ - --prompt "Review this repository's packaging metadata. Do not edit files. Report concrete issues with file paths." \ - --detach -``` - -The JSON response contains a UUID `handle`. Keep it private and use the same -handle for every lifecycle command: - -```bash -HANDLE='' -codex-exec-subagents status "$HANDLE" -codex-exec-subagents wait "$HANDLE" --timeout 900 -codex-exec-subagents result "$HANDLE" --sequence 1 -``` - -`--detach` returns after dispatch. Without it, `run` waits up to `--timeout` -(900 seconds by default; `0` means indefinitely) and returns completion JSON. -A wait timeout exits without signalling or killing the worker; the handle -remains usable. - -`result` prints the raw `last-message.txt` artifact. Its presence alone does -not prove that transport or verification succeeded, so inspect `status` and -the receipt as well. - -The status JSON includes `receiptPath`; open that file to inspect evidence for -the exact invocation. A completed receipt includes fields like: - -```json -{ - "state": "completed", - "role": "reviewer", - "requested": { - "model": "gpt-5.6-terra", - "effort": "medium" - }, - "declaredSandboxMode": "read-only", - "effectivePolicy": { - "executionMode": "sandboxed", - "effectiveSandbox": "read-only", - "approvalBehavior": "never" - }, - "threadId": "...", - "processExitStatus": 0, - "terminalEventType": "turn.completed", - "verificationErrors": [] -} -``` - -Together, `receipt.json`, `events.jsonl`, and `last-message.txt` record what the -runner requested, what Codex emitted, and whether transport-level verification -succeeded. They do not prove that the model semantically followed its role -instructions; inspect the result and any repository changes accordingly. - -## Lifecycle commands +See the +[installation guide](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/installation.md) +for prerequisites, inspect-first installation, version pins, custom paths, +updates, rollback, uninstall, and development setup. -Tasks can come from exactly one explicit source, or from standard input: +## Use -```bash -codex-exec-subagents run reviewer --cwd "$TARGET" --prompt "Self-contained task" -codex-exec-subagents run reviewer --cwd "$TARGET" --prompt-file ./task.txt -printf '%s\n' "Self-contained task" | - codex-exec-subagents run reviewer --cwd "$TARGET" -``` +The model comes from the role file under `~/.codex/agents/`, not from the Sol +conversation. The bundled +[`reviewer` example](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/examples/agents/reviewer.toml) +uses GPT-5.6-Terra with medium reasoning and a read-only sandbox. Print it with +`codex-exec-subagents example-role reviewer` if you need a starting point. -Start a new external thread: +Once the role exists, tell your main Codex session: ```text -codex-exec-subagents run ROLE --cwd /absolute/repository \ - [--sandboxed | --yolo] \ - [--prompt TEXT | --prompt-file FILE] [--detach] [--timeout SECONDS] -``` - -Inspect or wait for its latest invocation: - -```bash -codex-exec-subagents status "$HANDLE" -codex-exec-subagents wait "$HANDLE" --timeout 900 -``` - -Read the latest result or an exact invocation: - -```bash -codex-exec-subagents result "$HANDLE" -codex-exec-subagents result "$HANDLE" --sequence 1 -``` - -Resume the exact Codex thread after a completed invocation: - -```bash -codex-exec-subagents follow-up "$HANDLE" \ - --sandboxed \ - --prompt "Now check whether the earlier findings also affect the installation documentation." \ - --detach -codex-exec-subagents wait "$HANDLE" --timeout 900 -codex-exec-subagents result "$HANDLE" --sequence 2 -``` - -Follow-up reuses the pinned role, working directory, thread, and execution -authority. Omitting its mode flag reuses the pinned mode; repeating the same -flag is accepted; requesting the conflicting mode is rejected. An active, -interrupted, missing-thread, or changed-role session cannot be followed up. +Use the installed codex-exec-subagents command for this task. -Pruning is run-root-wide; there is no handle-scoped prune: - -```bash -codex-exec-subagents prune # all recognizable terminal handles -codex-exec-subagents prune 30 # recognizable terminal handles older than 30 days -``` - -There is no automatic retention schedule. Review the JSON categories before -removing the run root itself. - -## Execution policy - -For a fresh `run`, execution mode precedence is: - -1. the explicit `--sandboxed` or `--yolo` flag; -2. the exact personal file - `~/.config/codex-exec-subagents/config.toml`; then -3. the package default, `sandboxed`. - -The optional personal file must contain exactly one top-level key: - -```toml -execution_mode = "sandboxed" +First read `codex-exec-subagents --help`. Then dispatch the global `reviewer` +role against the current repository in sandboxed mode. Ask it to map the +packaging and installation flow without editing files. Run it detached, keep +the returned handle, wait for completion, and give me the reviewer's result +with the handle and final status. Do not perform the exploration in this Sol +session. ``` -Sandboxed execution applies the role's declared `read-only` or -`workspace-write` sandbox and forces noninteractive approval behavior to -`never`. It rejects a `danger-full-access` role. YOLO passes Codex the full -approval-and-sandbox bypass and must be selected only for a trusted role, -prompt, repository, account, and host: - -```bash -codex-exec-subagents run implementer \ - --cwd "$TARGET" \ - --yolo \ - --prompt "Implement the already-reviewed bounded change and run its tests." -``` - -Prompt-level instructions still matter, but they are not a security boundary -and cannot reduce authority granted by YOLO. Receipts, status, and session -metadata record `effectivePolicy`; status and receipts also record -`policySelection`, including whether the choice came from an explicit flag, -the personal config, the package default, or pinned follow-up authority. - -## Artifacts and run roots +The Sol session remains the orchestrator while the Terra role performs the +bounded exploration. The returned handle also gives the parent a durable run +to inspect instead of relying on a silent native handoff. -Every run persists under -`~/.codex/codex-exec-subagents/runs/`. The sole override is -`CODEX_EXEC_SUBAGENTS_RUNS_ROOT`; set it to one absolute path consistently for -`run`, `follow-up`, `status`, `wait`, `result`, and `prune`: +For a follow-up, tell the same conversation: -```bash -export CODEX_EXEC_SUBAGENTS_RUNS_ROOT=/absolute/private/run-root +```text +Use the same codex-exec-subagents handle to ask the reviewer to inspect the +release workflow next. Wait for that turn and bring back its result and final +status. ``` -Empty or relative overrides fail. A run root equal to or inside the target -repository also fails. New roots receive a private ownership marker; nonempty -unmarked roots are not adopted. Existing roots, markers, and `.locks` -directories must be owned by the current UID and must not be symlinks. - -Each UUID handle contains `session.json` and numbered invocation directories. -Each invocation retains `prompt.txt`, `events.jsonl`, `stderr.txt`, -`last-message.txt`, `status.json`, and `receipt.json`. Handle and invocation -directories are `0700`; owned artifact files are `0600`. See -[the artifact lifecycle and local security contract](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/artifact-lifecycle.md) -for the exact schema, atomic publication, interruption, and pruning behavior. - -If a nonterminal worker disappears, `status` reports an inspectable -`interrupted` handle after checking for a racing terminal commit. Interrupted -handles cannot be followed up or pruned. Pruning conservatively retains -active, interrupted, busy, malformed, contradictory, symlinked, and -unrecognized entries. The ownership marker and `.locks` infrastructure remain. - -## Security, privacy, and retention - -Owner-only permissions are not encryption. Until explicit successful pruning, -artifacts may persist prompts, developer instructions, role definitions and -hashes, working directories, model events, stderr, process identifiers, final -messages, receipts, status, usage, and exact thread-continuity metadata. -Invocation text and command arguments can also be visible through local -process inspection while a run is active. - -Review prompts and results for credentials, private repository content, -personal data, and secrets before running or retaining them. Do not commit raw -run output that may contain sensitive data. The package does not provide -remote storage, scheduling, automatic retention, multi-user isolation, or -protection from a malicious user who controls the account or host. +Codex runs the underlying commands through its shell tools. You can run those +commands directly from Bash too. See the +[usage guide](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/usage.md) +for role setup, direct CLI examples, execution policy, lifecycle commands, +run roots, pruning, and troubleshooting. -Sandboxed mode enforces the declared role sandbox with approvals set to -`never`; it is the safe public default. YOLO removes both approval and sandbox -boundaries. Neither mode makes prompt instructions a trustworthy defense -against hostile input. +## Durable evidence -## Troubleshooting +Each invocation retains its prompt, Codex event stream, stderr, final message, +status, and receipt under `~/.codex/codex-exec-subagents/runs/`. The receipt +records the requested role, model, effort, instructions, working directory, +effective sandbox policy, process outcome, and exact Codex thread ID. -- **Unsupported platform:** use a directly selected native-Linux host. macOS, - WSL, native Windows, and unknown platforms require a future port. -- **`codex` not found:** install the Codex CLI and put its executable on - `PATH`. `run` and `follow-up` fail before touching the run root. -- **Role invalid or missing:** check - `~/.codex/agents/.toml`, all six required nonempty string fields, the - matching `name`, and a supported `sandbox_mode`. -- **Policy conflict:** a sandboxed run cannot use a - `danger-full-access` role. A follow-up cannot change its pinned mode. Check - `effectivePolicy` and `policySelection` in status and receipts. -- **Run-root error:** use one absolute, owner-controlled path outside the - target repository. Do not pre-populate an unmarked override or use symlinks. -- **Wait timed out:** the worker was not signalled. Re-run `status` or `wait` - with the same handle. -- **Interrupted handle:** inspect its artifacts. It cannot be resumed or - automatically pruned; the tool does not kill, retry, adopt, or restart it. -- **Command missing after install:** apply the exact `export PATH=...` guidance - printed by the installer, then open a new shell if appropriate. -- **Disk use after update:** verified superseded installer slots are retained - intentionally. Ownership-safe uninstall removes recognized slots; it - preserves all run roots and unknown files. +This evidence shows what the runner requested, what Codex emitted, and whether +transport-level verification succeeded. It does not prove that the model +semantically followed its role instructions; inspect the result and any +repository changes accordingly. -## Development +## Documentation -```bash -python3 -m venv .venv -.venv/bin/python -m pip install -e ".[dev]" - -# Fast unit suite -.venv/bin/python -m pytest - -# Complete unit and integration gate -.venv/bin/python scripts/green_gate.py -``` - -The default pytest configuration runs unit tests only. Real subprocess, -Git-boundary, release, installer, and other host-level tests carry the -`integration` marker. The green gate restores them and enforces one -180-second wall-clock limit across the complete suite. - -See the -[changelog](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/CHANGELOG.md). -The project is available under the -[MIT License](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/LICENSE). +- [Installation](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/installation.md) +- [Roles and usage](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/usage.md) +- [Security, privacy, and retention](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/security.md) +- [Artifact lifecycle and local-security contract](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/docs/artifact-lifecycle.md) +- [Changelog](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/CHANGELOG.md) +- [MIT License](https://github.com/antoinezambelli/codex-exec-subagents/blob/main/LICENSE) diff --git a/docs/artifact-lifecycle.md b/docs/artifact-lifecycle.md index eb95954..f092bd6 100644 --- a/docs/artifact-lifecycle.md +++ b/docs/artifact-lifecycle.md @@ -46,6 +46,35 @@ working directory, worker process identity, thread continuity, usage, and verification. `result` intentionally reads the raw last-message artifact; its presence does not assert successful completion. +## Receipt evidence + +A completed receipt includes evidence like: + +```json +{ + "state": "completed", + "role": "reviewer", + "requested": { + "model": "gpt-5.6-terra", + "effort": "medium" + }, + "declaredSandboxMode": "read-only", + "effectivePolicy": { + "executionMode": "sandboxed", + "effectiveSandbox": "read-only", + "approvalBehavior": "never" + }, + "threadId": "...", + "processExitStatus": 0, + "terminalEventType": "turn.completed", + "verificationErrors": [] +} +``` + +Together, the receipt, event stream, and final-message artifact record what the +runner requested, what Codex emitted, and whether transport verification +succeeded. They do not prove semantic compliance with the role instructions. + ## Completion and interruption `status.json` is the terminal commit marker. Terminal publication writes the diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..c505084 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,153 @@ +# Installation + +## Requirements + +Version 0.1 supports directly selected native-Linux hosts and requires: + +- Python 3.11 or newer with `venv` support; +- the Codex CLI on `PATH`; +- either `curl` or `wget`; and +- `sha256sum` and the standard GNU/Linux userland used by the installer. + +Python 3.11, 3.12, and 3.13 are tested. The command and installer reject +macOS, WSL, native Windows, and unknown platforms before creating package +state, run artifacts, handles, or worker processes. + +## Stable installer + +Install or update with `curl`: + +```bash +curl -fsSL https://antoinezambelli.github.io/codex-exec-subagents/install.sh | sh +``` + +If `curl` is unavailable, use `wget`: + +```bash +wget -qO- https://antoinezambelli.github.io/codex-exec-subagents/install.sh | sh +``` + +The installer creates and manages its own private virtual environments. It +does not install system packages, use `sudo`, or edit shell startup files. + +### Inspect before running + +Piping a remote script to a shell executes code that may have changed since +you last reviewed it. Download and inspect the same script first if that is not +appropriate for your environment: + +```bash +curl -fsSLo install.sh \ + https://antoinezambelli.github.io/codex-exec-subagents/install.sh +less install.sh +sh ./install.sh +``` + +### Versions, updates, and rollback + +Pin a normalized stable version: + +```bash +sh ./install.sh --version 0.1.0 +``` + +Rerun the unpinned installer to update, or rerun a version pin to install or +verify that exact release. The installer verifies the tagged wheel against its +published checksum, stages and probes a new environment, and only then +promotes it. A failed update leaves the previous verified command usable. + +Older environments remain until uninstall so detached processes can finish. +Pin an earlier hosted version to roll back. + +### Locations and `PATH` + +The default private installation root is +`~/.local/share/codex-exec-subagents/`. The public command link is placed in +`~/.local/bin/`. + +Custom paths, including paths with spaces, are supported: + +```bash +sh ./install.sh \ + --install-root "$HOME/tools/codex-exec-subagents" \ + --bin-dir "$HOME/tools/bin" +``` + +If the selected bin directory is not an exact colon-delimited `PATH` +component, the installer prints the `export PATH=...` command needed for the +current shell. It does not edit shell startup files. Changing `--bin-dir` +requires uninstall and reinstall. + +### Uninstall + +If you kept the inspected installer, reuse it with the same path overrides +used for installation: + +```bash +sh ./install.sh --uninstall +``` + +A piped install does not retain `./install.sh`. Obtain and inspect a fresh copy +before uninstalling: + +```bash +INSTALLER_COPY="$(mktemp)" +curl -fsSLo "$INSTALLER_COPY" \ + https://antoinezambelli.github.io/codex-exec-subagents/install.sh +less "$INSTALLER_COPY" +sh "$INSTALLER_COPY" --uninstall +rm -f "$INSTALLER_COPY" +``` + +For a custom installation, repeat the exact paths: + +```bash +sh "$INSTALLER_COPY" --uninstall \ + --install-root "$HOME/tools/codex-exec-subagents" \ + --bin-dir "$HOME/tools/bin" +``` + +Uninstall validates ownership markers and exact links, removes only recognized +installer slots and bookkeeping, and preserves unknown files. It never +inspects or removes a run root. + +## pipx + +`pipx` exposes the same runtime CLI: + +```bash +pipx install codex-exec-subagents +# or pin one stable release +pipx install codex-exec-subagents==0.1.0 +``` + +Update or uninstall with: + +```bash +pipx upgrade codex-exec-subagents +pipx uninstall codex-exec-subagents +``` + +Only `install.sh` provides the project-specific checksum verification, staged +promotion, retained environments, and ownership-aware uninstall described +above. + +## Development from source + +```bash +git clone https://github.com/antoinezambelli/codex-exec-subagents.git +cd codex-exec-subagents +python3 -m venv .venv +.venv/bin/python -m pip install -e ".[dev]" +source .venv/bin/activate +codex-exec-subagents --version +``` + +The default test command runs the fast unit suite. The green gate adds real +subprocess, Git-boundary, release, installer, and other host-level integration +tests under one 180-second wall-clock limit: + +```bash +.venv/bin/python -m pytest +.venv/bin/python scripts/green_gate.py +``` diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..f9497a8 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,57 @@ +# Security, privacy, and retention + +## Trust boundary + +The protection target is accidental exposure or deletion on a trusted, +single-user native-Linux account. The package does not claim to isolate +hostile users, root, or a compromised host. + +Owner-only permissions and ownership markers are not encryption. The package +does not provide remote storage, scheduling, automatic retention, multi-user +isolation, or protection from someone who already controls the account or +host. + +## Persisted data + +Until explicit successful pruning, local artifacts may retain: + +- prompts and developer instructions; +- role definitions, paths, and hashes; +- task working directories; +- model events, stderr, and final messages; +- status, receipts, usage, and thread-continuity metadata; and +- process identifiers and execution-policy evidence. + +Invocation text and command arguments may also be visible through local +process inspection while a run is active. Review prompts and results for +credentials, private repository content, personal data, and secrets. Do not +commit raw run output without reviewing it. + +## Sandboxed and YOLO execution + +Sandboxed mode is the public default. It applies a role's declared read-only or +workspace-write sandbox with approvals set to `never`. + +YOLO removes both approval and sandbox boundaries. Use it only when the role, +prompt, repository, account, and host are trusted. Neither mode makes prompt +instructions a dependable defense against hostile input. + +## What receipts prove + +Receipts, status, event streams, and final-message artifacts record what the +runner requested, what Codex emitted, the effective execution policy, process +outcome, verification errors, and exact thread continuity. + +They provide durable transport and configuration evidence. They do not attest +that the model understood or semantically complied with its role. Review the +result and any filesystem or repository changes independently. + +## Retention and deletion + +There is no automatic retention schedule. `prune` removes only recognizable, +validated terminal handles and retains anything active or ambiguous. Installer +updates and uninstall do not inspect or delete run roots. + +See [Roles and usage](usage.md#run-roots-and-pruning) for the commands and the +[artifact lifecycle and local-security contract](artifact-lifecycle.md) for +the exact permissions, publication, interruption, and pruning behavior. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..61ec03f --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,201 @@ +# Roles and usage + +## External sessions and task framing + +`codex-exec-subagents` starts independent `codex exec` root sessions. They are +not native subagents and do not receive hidden parent-chat context, a shared +parent UI, or automatic coordination. + +Write self-contained tasks. Include the objective, boundaries, relevant +paths, validation, and expected result. + +`run` and `follow-up` require the `codex` executable on `PATH` before touching +the run root. Help, version, and example-role commands remain available +without Codex: + +```bash +codex-exec-subagents --help +codex-exec-subagents --version +codex-exec-subagents example-role reviewer +codex-exec-subagents run --help +``` + +## Role files + +Roles are user-created global TOML files at +`~/.codex/agents/.toml`. The package reads them but never installs, +rewrites, or silently modifies Codex configuration. + +Each role requires six nonempty string fields: `name`, `description`, `model`, +`model_reasoning_effort`, `sandbox_mode`, and `developer_instructions`. `name` +must match the filename. Supported declared sandboxes are `read-only`, +`workspace-write`, and `danger-full-access`. + +Print the bundled reviewer role without writing configuration: + +```bash +codex-exec-subagents example-role reviewer +``` + +It produces: + +```toml +name = "reviewer" +description = "Read-only repository reviewer" +model = "gpt-5.6-terra" +model_reasoning_effort = "medium" +sandbox_mode = "read-only" +developer_instructions = "Inspect the requested repository. Do not edit files. Report evidence, risks, and missing validation concisely." +``` + +Inspect output before redirecting it; shell redirection can overwrite an +existing role file. + +A trusted editing role might look like: + +```toml +name = "implementer" +description = "Bounded repository implementer" +model = "gpt-5.6-sol" +model_reasoning_effort = "high" +sandbox_mode = "workspace-write" +developer_instructions = "Implement only the supplied task in the selected repository. Preserve unrelated work, run focused validation, and report changed files." +``` + +The role sandbox is a declaration used by sandboxed execution. YOLO is a +separate execution policy, not a role capability. + +## Starting a run + +Tasks can come from exactly one explicit source or from standard input: + +```bash +codex-exec-subagents run reviewer --cwd "$TARGET" --prompt "Self-contained task" +codex-exec-subagents run reviewer --cwd "$TARGET" --prompt-file ./task.txt +printf '%s\n' "Self-contained task" | + codex-exec-subagents run reviewer --cwd "$TARGET" +``` + +The full command shape is: + +```text +codex-exec-subagents run ROLE --cwd /absolute/repository \ + [--sandboxed | --yolo] \ + [--prompt TEXT | --prompt-file FILE] [--detach] [--timeout SECONDS] +``` + +`--detach` returns after dispatch. Without it, `run` waits up to `--timeout` +(900 seconds by default; `0` means indefinitely) and returns completion JSON. +A wait timeout exits without signalling or killing the worker; the handle +remains usable. + +## Status, results, and follow-up + +Use the UUID handle returned by `run` with every lifecycle command: + +```bash +codex-exec-subagents status "$HANDLE" +codex-exec-subagents wait "$HANDLE" --timeout 900 +codex-exec-subagents result "$HANDLE" +codex-exec-subagents result "$HANDLE" --sequence 1 +``` + +`result` prints the raw `last-message.txt` artifact. Its presence alone does +not prove that transport or verification succeeded, so inspect `status` and +the receipt too. + +Resume the exact Codex thread after a completed invocation: + +```bash +codex-exec-subagents follow-up "$HANDLE" \ + --prompt "Now check the installation documentation." \ + --detach +codex-exec-subagents wait "$HANDLE" --timeout 900 +codex-exec-subagents result "$HANDLE" --sequence 2 +``` + +Follow-up reuses the pinned role, working directory, thread, and execution +authority. Omitting its mode flag reuses the pinned mode; repeating the same +flag is accepted; requesting the conflicting mode is rejected. Active, +interrupted, missing-thread, and changed-role sessions cannot be followed up. + +## Execution policy + +For a fresh run, execution mode precedence is: + +1. an explicit `--sandboxed` or `--yolo` flag; +2. `~/.config/codex-exec-subagents/config.toml`; then +3. the package default, `sandboxed`. + +The optional personal config contains exactly one top-level key: + +```toml +execution_mode = "sandboxed" +``` + +Sandboxed execution applies the role's declared `read-only` or +`workspace-write` sandbox and forces approval behavior to `never`. It rejects +a `danger-full-access` role. + +YOLO passes Codex the full approval-and-sandbox bypass: + +```bash +codex-exec-subagents run implementer \ + --cwd "$TARGET" \ + --yolo \ + --prompt "Implement the reviewed change and run its tests." +``` + +Use YOLO only for a trusted role, prompt, repository, account, and host. +Prompt instructions are not a security boundary and cannot reduce the +authority granted by YOLO. Status, receipts, and session metadata record the +`effectivePolicy`; status and receipts also record `policySelection`. + +## Run roots and pruning + +Runs persist under `~/.codex/codex-exec-subagents/runs/`. Override the root by +setting one absolute path consistently for every lifecycle command: + +```bash +export CODEX_EXEC_SUBAGENTS_RUNS_ROOT=/absolute/private/run-root +``` + +Empty and relative overrides fail. A root equal to or inside the target +repository also fails. Nonempty unmarked roots are not adopted, and existing +roots, markers, and lock directories must belong to the current UID and must +not be symlinks. + +Pruning is run-root-wide; there is no handle-scoped prune: + +```bash +codex-exec-subagents prune # all recognizable terminal handles +codex-exec-subagents prune 30 # terminal handles older than 30 days +``` + +There is no automatic retention schedule. Pruning conservatively retains +active, interrupted, busy, malformed, contradictory, symlinked, and +unrecognized entries. See the [artifact lifecycle](artifact-lifecycle.md) for +the exact storage and deletion contract. + +## Troubleshooting + +- **Unsupported platform:** use a directly selected native-Linux host. macOS, + WSL, native Windows, and unknown platforms require a future port. +- **`codex` not found:** install the Codex CLI and put it on `PATH`. `run` and + `follow-up` fail before touching the run root. +- **Role invalid or missing:** check the role path, all six required fields, + the matching `name`, and the declared sandbox. +- **Policy conflict:** sandboxed runs reject `danger-full-access` roles, and a + follow-up cannot change its pinned mode. Inspect `effectivePolicy` and + `policySelection`. +- **Run-root error:** use one absolute owner-controlled path outside the target + repository. Do not pre-populate an unmarked override or use symlinks. +- **Wait timed out:** the worker was not signalled. Reuse the same handle with + `status` or `wait`. +- **Interrupted handle:** inspect its artifacts. It cannot be resumed or + automatically pruned; the tool does not kill, retry, adopt, or restart it. +- **Command missing after install:** apply the exact `export PATH=...` guidance + printed by the installer, then open a new shell if appropriate. +- **Disk use after update:** verified superseded installer slots are retained + intentionally. Ownership-safe uninstall removes recognized slots while + preserving every run root and unknown file. diff --git a/pyproject.toml b/pyproject.toml index 3ef9bbf..686f4c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ include = [ "/install.sh", "/README.md", "/CHANGELOG.md", - "/docs/artifact-lifecycle.md", + "/docs/", "/LICENSE", "/pyproject.toml", ] diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py index bc024ca..ff6f66e 100755 --- a/scripts/prepare_release.py +++ b/scripts/prepare_release.py @@ -67,6 +67,9 @@ def inspect_sdist(path: Path, version: str) -> None: f"{expected_root}/README.md", f"{expected_root}/CHANGELOG.md", f"{expected_root}/docs/artifact-lifecycle.md", + f"{expected_root}/docs/installation.md", + f"{expected_root}/docs/security.md", + f"{expected_root}/docs/usage.md", f"{expected_root}/install.sh", } if not required.issubset(names): diff --git a/tests/test_docs.py b/tests/test_docs.py index 0248ae7..88bc06e 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -10,10 +10,16 @@ REPOSITORY = Path(__file__).resolve().parent.parent README = REPOSITORY / "README.md" +INSTALLATION = REPOSITORY / "docs" / "installation.md" +USAGE = REPOSITORY / "docs" / "usage.md" +SECURITY = REPOSITORY / "docs" / "security.md" PUBLIC_MARKDOWN = ( README, REPOSITORY / "CHANGELOG.md", REPOSITORY / "docs" / "artifact-lifecycle.md", + INSTALLATION, + SECURITY, + USAGE, ) MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)") @@ -155,27 +161,51 @@ def test_lifecycle_subcommand_help_describes_arguments( assert phrase in output -def test_readme_matches_public_platform_policy_and_security_contracts() -> None: +def test_readme_is_a_concise_human_entry_point() -> None: text = README.read_text(encoding="utf-8") + assert re.findall(r"^## (.+)$", text, re.MULTILINE) == [ + "What this is and why it exists", + "Install", + "Use", + "Durable evidence", + "Documentation", + ] + assert len(text.splitlines()) <= 150 + for detail in ( + "--install-root", + "--uninstall", + "CODEX_EXEC_SUBAGENTS_RUNS_ROOT", + "codex-exec-subagents prune", + 'execution_mode = "sandboxed"', + "## Troubleshooting", + "## Development", + ): + assert detail not in text + + +def test_public_docs_cover_platform_policy_and_security_contracts() -> None: + text = " ".join( + "\n".join(path.read_text(encoding="utf-8") for path in PUBLIC_MARKDOWN).split() + ) required = ( - "independent `codex exec` sessions", + "independent `codex exec` root sessions", "not native subagents", - "Python 3.11, 3.12, or 3.13", + "Python 3.11, 3.12, and 3.13", "macOS, WSL, native Windows, and unknown platforms", - "`codex` executable to be discoverable on", + "`codex` executable on `PATH`", "explicit `--sandboxed` or `--yolo` flag", "`~/.config/codex-exec-subagents/config.toml`", "package default, `sandboxed`", "`effectivePolicy`", "`policySelection`", - "approval behavior to\n`never`", - "full\napproval-and-sandbox bypass", - "Owner-only permissions are not encryption", - "There is no automatic retention schedule", + "approval behavior to `never`", + "full approval-and-sandbox bypass", + "Owner-only permissions and ownership markers are not encryption", + "There is no automatic retention schedule.", "A wait timeout exits without signalling or killing the worker", "`result` prints the raw `last-message.txt` artifact", - "Interrupted\nhandles cannot be followed up or pruned", - "active, interrupted, busy, malformed, contradictory, symlinked, and\nunrecognized", + "Interrupted handles remain inspectable, cannot be followed up, and are not pruned", + "active, interrupted, busy, malformed, contradictory, symlinked, and unrecognized", ) for phrase in required: assert phrase in text @@ -194,39 +224,46 @@ def test_reviewer_example_is_canonical_and_cli_discoverable() -> None: ): assert phrase in help_text readme = README.read_text(encoding="utf-8") - assert ( - "[`examples/agents/reviewer.toml`](https://github.com/antoinezambelli/" - "codex-exec-subagents/blob/main/examples/agents/reviewer.toml)" - in readme - ) + assert "blob/main/examples/agents/reviewer.toml" in readme assert "codex-exec-subagents example-role reviewer" in readme def test_installer_and_pipx_guidance_are_distinct() -> None: - text = README.read_text(encoding="utf-8") + readme = README.read_text(encoding="utf-8") + guide = INSTALLATION.read_text(encoding="utf-8") assert ( "curl -fsSL https://antoinezambelli.github.io/" "codex-exec-subagents/install.sh | sh" - ) in text + ) in readme assert ( "wget -qO- https://antoinezambelli.github.io/" "codex-exec-subagents/install.sh | sh" - ) in text + ) in readme + assert "pipx install codex-exec-subagents" in readme for phrase in ( "Piping a remote script to a shell", - "either `curl` or `wget`", "curl -fsSLo install.sh", "sh ./install.sh --version 0.1.0", "--install-root", "--bin-dir", "sh ./install.sh --uninstall", - "The primary piped installation does not retain `./install.sh`", + "A piped install does not retain `./install.sh`", 'INSTALLER_COPY="$(mktemp)"', 'sh "$INSTALLER_COPY" --uninstall', 'sh "$INSTALLER_COPY" --uninstall \\\n --install-root', - "pipx install codex-exec-subagents", "pipx install codex-exec-subagents==0.1.0", "same runtime CLI", - "only `install.sh` provides", + "Only `install.sh` provides", ): - assert phrase in text + assert phrase not in readme + assert phrase in guide + for summary in ( + "installation guide", + "prerequisites", + "inspect-first installation", + "version pins", + "custom paths", + "rollback", + "uninstall", + ): + assert summary in readme diff --git a/tests/test_installer.py b/tests/test_installer.py index 3588813..747bd89 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -417,6 +417,9 @@ def test_built_wheel_installs_and_probes_from_clean_external_environment( f"{root}/README.md", f"{root}/CHANGELOG.md", f"{root}/docs/artifact-lifecycle.md", + f"{root}/docs/installation.md", + f"{root}/docs/security.md", + f"{root}/docs/usage.md", f"{root}/examples/agents/reviewer.toml", f"{root}/pyproject.toml", f"{root}/scripts/prepare_release.py", @@ -428,6 +431,9 @@ def test_built_wheel_installs_and_probes_from_clean_external_environment( f"{root}/README.md", f"{root}/CHANGELOG.md", f"{root}/docs/artifact-lifecycle.md", + f"{root}/docs/installation.md", + f"{root}/docs/security.md", + f"{root}/docs/usage.md", } assert_archive_markdown_links(names, markdown_payloads)