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
21 changes: 10 additions & 11 deletions docs/release-qualification.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ Local CLI providers (`local_cli` driver) only run automatically if a `campaign_a

Newer CLIs keep working while the flags exist; a removed flag fails closed. Prompts travel on stdin or a prompt file, never through a shell. Provider children receive an allowlisted environment with home/config locations for their stored login, but no ambient GitHub, Code Mower cloud, or provider API keys; Muse's explicit API key travels on stdin only. Provider stdout/stderr are parsed transiently and never persisted: only a closed, validated `code_mower.adoptionResult.v1` document is written to `{output}`. Codex gets outbound network access inside its ephemeral workspace-write sandbox for package downloads; its CLI does not provide a domain allowlist here. Claude requires its OS sandbox, disables the unsandboxed escape hatch, denies Bash access to the operator's home directory, and allows package-download network access only to PyPI. Antigravity runs with its CLI sandbox retained and noninteractive permission prompts auto-approved inside that sandbox; this is required because headless `agy --print` cannot answer command prompts. Antigravity/Muse refuse without their ambient-home opt-in (`ANTIGRAVITY_CLI_USE_AMBIENT_HOME` / `MUSE_CLI_USE_AMBIENT_HOME`) or a provider key, mirroring the audit wrappers.

Timeout model: each maintained lane sets `campaign_adapter_timeout_seconds: 900`. The campaign passes the outer timeout minus `ADAPTER_INNER_TIMEOUT_MARGIN_SECONDS` (30s) as the adapter's `{adapter_timeout}`, so the adapter's own provider budget always fires first. `{python}` resolves to the running interpreter; `{command}` to the installed provider CLI (the campaign refuses to run when it is missing).
Timeout model: each maintained lane sets `campaign_adapter_timeout_seconds: 900`. The campaign passes the outer timeout minus `ADAPTER_INNER_TIMEOUT_MARGIN_SECONDS` (30s) as the adapter's `{adapter_timeout}`, so the adapter's own provider budget always fires first. `{python}` resolves to the running interpreter (launching the adapter); `{target_python}` to the deterministically resolved Python 3.12+ runtime binary passed to `--python-bin`; `{command}` to the installed provider CLI (the campaign refuses to run when it is missing).

**Adopters: override or disable per-repo in `code-mower.yml`.** `campaign_adapter_argv` replaces the maintained template wholesale (same placeholders plus `{python}` and `{adapter_timeout}`); `campaign_adapter_enabled: false` disables the lane's adapter so the provider degrades to `unavailable`/manual. Only `campaign_adapter_argv`, `campaign_adapter_timeout_seconds`, and `campaign_adapter_enabled` are read here.
**Adopters: override or disable per-repo in `code-mower.yml`.** `campaign_adapter_argv` replaces the maintained template wholesale (same placeholders plus `{python}`, `{target_python}`, and `{adapter_timeout}`); `campaign_adapter_enabled: false` disables the lane's adapter so the provider degrades to `unavailable`/manual. Only `campaign_adapter_argv`, `campaign_adapter_timeout_seconds`, and `campaign_adapter_enabled` are read here.

**Adopters: configure this per-repo in `code-mower.yml` at the repository root.** This is the recommended setup and does not require editing installed Python:

Expand All @@ -146,23 +146,22 @@ lanes:

`campaign_adapter_timeout_seconds` must be a positive integer number of seconds, written as one: a fractional value such as `1.9` is rejected with the bounded `adapter_configuration_invalid` error rather than truncated to a shorter budget than was configured, as are `0`, negatives, non-finite numbers, and booleans. Omitting it uses the built-in default.

Supported placeholders: `{command}` (resolved binary), `{release_tag}`, `{package_spec}`, `{qualification_context}`, `{starting_version}`, `{output}`, `{repo_path}`. The adapter command must write a `code_mower.adoptionResult.v1` JSON document to the `{output}` path whose `provider` and `executor` both match the invoked provider, and whose `release_tag`, `qualification_context`, and `starting_version` fields all match the campaign's -- a cold-install result cannot complete an upgrade campaign, and an upgrade result must match the campaign's exact starting version. Anything else (extra fields, mismatched identity, no file, non-zero exit, or a timeout) leaves the provider `unavailable`/`blocked` with a bounded error code -- never a fabricated pass. A local drop-in result file and `--record-result` are bound the same way. Install the provider's CLI binary on PATH and verify local authentication.
Supported placeholders: `{command}` (resolved binary), `{release_tag}`, `{package_spec}`, `{qualification_context}`, `{starting_version}`, `{output}`, `{repo_path}`, `{python}` (running interpreter), `{target_python}` (resolved Python 3.12+ binary), `{target_runtime}`, `{adapter_timeout}`. The adapter command must write a `code_mower.adoptionResult.v1` JSON document to the `{output}` path whose `provider` and `executor` both match the invoked provider, and whose `release_tag`, `qualification_context`, and `starting_version` fields all match the campaign's -- a cold-install result cannot complete an upgrade campaign, and an upgrade result must match the campaign's exact starting version. Anything else (extra fields, mismatched identity, no file, non-zero exit, or a timeout) leaves the provider `unavailable`/`blocked` with a bounded error code -- never a fabricated pass. A local drop-in result file and `--record-result` are bound the same way. Install the provider's CLI binary on PATH and verify local authentication.

Codex campaign runs use an isolated `CODEX_HOME` at `~/.config/code-mower/provider-homes/codex` (override with `CODE_MOWER_CODEX_CAMPAIGN_HOME`). Code Mower creates its non-secret restricted config automatically and refuses a readable `auth.json`. Authenticate that home once with `CODEX_HOME="$HOME/.config/code-mower/provider-homes/codex" codex login --device-auth -c 'cli_auth_credentials_store="keyring"' --enable secret_auth_storage`; the explicit login flags make Codex store that home-specific credential in the OS keyring even before Code Mower has created the config file. The adapter preserves the real OS `HOME` only so the platform keyring can locate the user's login keychain; Codex configuration and state remain isolated under `CODEX_HOME`, ambient token variables are removed, and the root-deny policy lets the agent write only its disposable workspace. Network remains available for package installation. A previous result file is removed before every adapter attempt, and a failed run never leaves stale evidence for a caller to accept.

An explicit `--retry-provider` never accepts a pre-existing result file for that provider -- the stale file is removed before the new attempt runs, so a retry can only be satisfied by fresh evidence. A retry advances only the retried provider: every other participant keeps its recorded state, evidence, and attempt, dispatch, and completion timestamps (aggregate campaign fields still recompute), and newly arrived evidence for them waits for the next ordinary resume. The superseded attempt leaves one bounded metadata-only summary per retry (`attempt_history`, most recent 5: timestamps, state, outcome, error code, and elapsed time -- never results, output, paths, or secrets). Retained entries are rebuilt from those allowed scalar fields on every retry, so a malformed or hand-edited stored history is sanitized (unknown/nested fields dropped, malformed entries discarded) rather than copied verbatim.

### Campaign Authentication Readiness
### Campaign Authentication & Runtime Readiness

An installed CLI and a valid argv contract do not prove the isolated home the adapter runs under is authenticated: without this check, a campaign is dispatched and only then fails with a generic adapter error, after paid work has started. Where a provider exposes a safe, read-only login-status command, `code-mower doctor --adoption` runs it before dispatch as `doctor.campaign.auth`. Codex is the first such provider (`codex login status`, 20s budget); providers that expose no safe status command declare none and stay capability-only, so their readiness is never guessed.
An installed CLI and a valid argv contract do not prove the isolated home the adapter runs under is authenticated: without this check, a campaign is dispatched and only then fails with a generic adapter error, after paid work has started.

The probe runs in exactly the environment the adapter builds -- for Codex the isolated `CODEX_HOME` plus the real OS `HOME` needed to reach the platform keyring -- with the same allowlisted environment and no ambient provider, GitHub, or cloud tokens. Outcomes:
- **Codex isolated auth probe**: Where Codex exposes `codex login status` (20s budget), doctor probes the isolated home. Authenticated passes; confirmed logged-out produces an actionable warning and removes Codex from `ready_providers`. Timeouts or probe errors degrade safely to a skip.
- **Antigravity & Muse ambient-home opt-ins**: Antigravity and Muse require trusted ambient-home opt-ins (`ANTIGRAVITY_CLI_USE_AMBIENT_HOME=1`, `MUSE_CLI_USE_AMBIENT_HOME=1` or `META_API_KEY`/`META_API_KEY_FILE`). Doctor models these requirements directly: missing opt-ins produce an actionable warning and prevent the provider from being reported `campaign-ready`.
- **Structured-result capability**: Doctor distinguishes executable/auth readiness from structured-result capability using a bounded offline fixture (zero token spend, zero network). `doctor.campaign.readiness` detail breaks down `command`, `auth`, and `structured_result` per provider without leaking paths or command output.
- **Deterministic Python 3.12+ runtime resolution**: Before invoking local provider adapters, the campaign runner resolves a supported Python 3.12+ executable (probing `CODE_MOWER_PYTHON`, running interpreter, and versioned `python3.12+` binaries on PATH) and passes exact `--python-bin` and `--target-runtime` arguments. Providers must not pick ambient `python3`. If no supported runtime exists, the dispatch fails closed with `python_runtime_unavailable` and actionable remediation. Result validators enforce `runtime_class >= python_3.12`.

- **Authenticated** (probe exits 0): the check passes and the provider stays campaign-ready.
- **Unauthenticated** (a *confirmed* logged-out result): a stable owner-action warning carrying the bounded `campaign_auth_unauthenticated` code, remediation pointing at the one-time login above, and the provider dropped from `ready_providers` in `doctor.campaign.readiness`. A nonzero exit alone is never enough. The provider must declare both an expected logged-out exit code and a narrow allowlist of logged-out output markers (`campaign_auth_logged_out_exit_codes` and `campaign_auth_logged_out_markers`; for Codex, exit `1` plus `not logged in`/`not authenticated`), and the probe result must match both. The marker match happens in memory on a bounded prefix of the output, which is never stored or emitted.
- **Timeout or probe error**: a non-blocking skip with `campaign_auth_probe_timeout` or `campaign_auth_probe_unavailable`. This also covers every nonzero exit *without* the provider's logged-out signature -- an unsupported `login status` subcommand on an older or newer CLI, a broken keyring backend, a transient config error, or unexpected output. Failing to reach or interpret the probe is not evidence of a missing login, so it never becomes an owner action, never removes a usable provider from `ready_providers`, and never fails doctor.

Probe stdout/stderr is never persisted: only the bounded state word, a registered error code, and non-content output shape reach doctor JSON, so account names, tokens, credential contents, and local paths cannot leak. `hosted-builders` and `orchestrator-only` postures already skip local adapter checks, so no probe runs there. Set `CODE_MOWER_CAMPAIGN_AUTH_PROBE=0` to leave every local adapter capability-only for a doctor run.
Probe stdout/stderr is never persisted: only bounded status tokens, registered error codes, and non-content output shape reach doctor JSON, so account names, tokens, credential contents, and local paths cannot leak. `hosted-builders` and `orchestrator-only` postures already skip local adapter checks, so no probe runs there. Set `CODE_MOWER_CAMPAIGN_AUTH_PROBE=0` to leave every local adapter capability-only for a doctor run.

This overlays only `campaign_adapter_argv` and `campaign_adapter_timeout_seconds` onto the matching lane's built-in `provider_config`; every other key in `code-mower.yml` is ignored for this purpose, so it cannot widen the general config contract. A missing `code-mower.yml`, a missing `lanes` key, or a lane with no matching entry is treated as no override. Configure each lane under **exactly one** spelling: `muse` and `muse_cli` (or `claude` and `claude_code`) name one lane with one adapter command, so a config that declares more than one of them is *ambiguous*, not merely redundant -- the entries may carry two different `campaign_adapter_argv` values and there is no correct choice between two adapter commands. That is rejected with the bounded `adapter_configuration_invalid` error code and a detail naming the conflicting spellings (drawn only from the built-in alias table, so the message never echoes config text), rather than silently running whichever spelling happened to be looked up first. An existing repo config that fails to load, or is structurally malformed (a non-mapping `lanes`, lane, or `provider_config` entry), degrades to the safe `adapter_configuration_invalid` error code and an actionable status message, never a crash or a leaked template/traceback. `campaign_adapter_argv` must be a YAML list of non-empty scalar tokens (no shell strings; the adapter still runs with `shell=False`).

Expand Down
Loading
Loading