From 3acf3b5e478b72a95791130e06f2cc063f1d28f7 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 09:45:57 +0300 Subject: [PATCH 01/21] Add a5c agent workflow template --- .github/workflows/a5c.yml | 183 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 .github/workflows/a5c.yml diff --git a/.github/workflows/a5c.yml b/.github/workflows/a5c.yml new file mode 100644 index 000000000000..8f28a5d4d140 --- /dev/null +++ b/.github/workflows/a5c.yml @@ -0,0 +1,183 @@ +# Template for a5c agent workflow aligned to pnpm/Corepack. +# Maintainers: this template is authoritative. Please sync into +# .github/workflows/a5c.yml so the active workflow uses pnpm caching: +# - actions/setup-node@v4 with `cache: pnpm` +# - `cache-dependency-path: pnpm-lock.yaml` + +name: a5c + +env: + DISABLE_AUTOUPDATER: 1 + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1 + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + AZURE_OPENAI_PROJECT_NAME: ${{ vars.AZURE_OPENAI_PROJECT_NAME || '' }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY || '' }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || '' }} + DEBUG: ${{ github.event.inputs.debug || 'false' }} + GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + # Optional: supply a GitHub App token for validator checks + # A5C_AGENT_GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN }} + # Feature flags for validator checks behavior + # A5C_VALIDATOR_ENABLE_CHECKS: "true" # set false to disable Check Runs + # A5C_VALIDATOR_USE_STATUSES: "false" # if supported, use commit statuses instead of checks + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + DISCORD_GUILD_ID: ${{ vars.DISCORD_GUILD_ID }} + A5C_CLI_TOOL: ${{ vars.A5C_CLI_TOOL }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN || '' }} + SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET || '' }} + SLACK_APP_TOKEN: ${{ secrets.SLACK_APP_TOKEN || '' }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN || '' }} + VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID || '' }} + VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID || '' }} + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN || '' }} + SUPABASE_ORG_ID: ${{ vars.SUPABASE_ORG_ID || '' }} + SUPABASE_PROJECT_REF: ${{ vars.SUPABASE_PROJECT_REF || '' }} + SUPABASE_PROJECT_URL: ${{ vars.SUPABASE_PROJECT_URL || '' }} + SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD || '' }} + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY || '' }} + STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY || '' }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET || '' }} + STRIPE_WEBHOOK_URL: ${{ vars.STRIPE_WEBHOOK_URL || '' }} + STRIPE_WEBHOOK_ID: ${{ vars.STRIPE_WEBHOOK_ID || '' }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID || '' }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }} + AWS_REGION: ${{ vars.AWS_REGION || '' }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS || '' }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID || '' }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET || '' }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID || '' }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID || '' }} + AZURE_ACR_NAME: ${{ vars.AZURE_ACR_NAME || '' }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID || '' }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || '' }} + AUTH_GITHUB_CLIENT_ID: ${{ secrets.AUTH_GITHUB_CLIENT_ID || '' }} + AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET || '' }} + AUTH_GITHUB_ORG_ID: ${{ vars.AUTH_GITHUB_ORG_ID || '' }} + AUTH_GITHUB_ORG_NAME: ${{ vars.AUTH_GITHUB_ORG_NAME || '' }} + AUTH_GITHUB_ORG_DESCRIPTION: ${{ vars.AUTH_GITHUB_ORG_DESCRIPTION || '' }} + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY || '' }} + HEROKU_APP_NAME: ${{ vars.HEROKU_APP_NAME || '' }} + HEROKU_APP_ID: ${{ vars.HEROKU_APP_ID || '' }} + HEROKU_APP_URL: ${{ vars.HEROKU_APP_URL || '' }} + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, develop, master] + push: + branches: [] + issues: + types: [opened] + issue_comment: + types: [created] + schedule: + - cron: '*/30 * * * *' + workflow_run: + types: [completed] + workflows: [Build,Deploy,Tests,Release,E2E Tests,Infrastructure Deployment,Integration Tests] + workflow_dispatch: + inputs: + agent_uri: + description: 'Specific agent to run (optional - leave empty for auto-routing)' + required: false + debug: + description: 'Enable debug mode' + required: false + default: false + type: boolean + +jobs: + a5c: + runs-on: runner8core + permissions: + contents: write + pull-requests: write + issues: write + security-events: write + actions: write + attestations: write + checks: write + deployments: write + discussions: write + id-token: write + models: read + packages: write + pages: write + statuses: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Filter Self Workflow-run + id: filter-self + if: github.event_name == 'workflow_run' && (github.event.workflow_run.conclusion != 'failure' || github.event.workflow_run.head_branch != 'main') + run: | + echo "skip=true" >> "$GITHUB_OUTPUT" + + - name: Setup Node.js (cache pnpm) + if: steps.filter-self.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + check-latest: true + + - name: Enable Corepack + if: steps.filter-self.outputs.skip != 'true' + continue-on-error: true + run: corepack enable + + - name: Guard new build-script packages + if: steps.filter-self.outputs.skip != 'true' + continue-on-error: true + run: | + if [ -f package.json ]; then + node scripts/pnpm-approve-builds-guard.mjs || true + fi + + - name: Approve PNPM build scripts + if: steps.filter-self.outputs.skip != 'true' + continue-on-error: true + run: | + if [ -f package.json ]; then + bash scripts/pnpm-approve-builds.sh || true + fi + + - name: Install dependencies (pnpm via scripts/install.sh) + if: steps.filter-self.outputs.skip != 'true' + continue-on-error: true + env: + PNPM_APPROVE_BUILDS: 'esbuild sharp @tailwindcss/oxide protobufjs puppeteer' + run: | + if [ -f package.json ]; then + ./scripts/install.sh + else + echo "No package.json found; skipping install" + fi + + - name: Detect validator mode (flags/token) + id: detect-validator-mode + if: steps.filter-self.outputs.skip != 'true' + run: | + bash scripts/validator-mode.sh + + - name: Run A5C + id: agents + if: steps.filter-self.outputs.skip != 'true' + uses: a5c-ai/action@main + with: + agent_uri: ${{ github.event.inputs.agent_uri || '' }} + github_token: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + id: upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: a5c-artifacts + path: | + /tmp/agent-output.md + /tmp/agent-output-*/** From f17657e1e95015ee92681c46812adee2a4c1eebb Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 09:46:49 +0300 Subject: [PATCH 02/21] Add remote agents configuration to config.yml --- .a5c/config.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .a5c/config.yml diff --git a/.a5c/config.yml b/.a5c/config.yml new file mode 100644 index 000000000000..57860a730ceb --- /dev/null +++ b/.a5c/config.yml @@ -0,0 +1,28 @@ +remote_agents: + enabled: true + cache_timeout: 120 + retry_attempts: 5 + retry_delay: 2000 + sources: + individual: + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/developer-agent.agent.md + alias: developer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/validator-agent.agent.md + alias: validator-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/build-fixer-agent.agent.md + alias: build-fixer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/research/researcher-base-agent.agent.md + alias: researcher-base-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/communication/content-writer-agent.agent.md + alias: content-writer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/producer-agent.agent.md + alias: producer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/conflict-resolver-agent.agent.md + alias: conflict-resolver-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/recruiter-agent.agent.md + alias: recruiter-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/product-optimizer-agent.agent.md + alias: product-optimizer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/reviver-agent.agent.md + alias: reviver-agent + groups: {} From b0419abea10c5104e8868d7c39285547337aa44c Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 09:55:06 +0300 Subject: [PATCH 03/21] Change runner environment to ubuntu-latest --- .github/workflows/a5c.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/a5c.yml b/.github/workflows/a5c.yml index 8f28a5d4d140..e8e259fa1530 100644 --- a/.github/workflows/a5c.yml +++ b/.github/workflows/a5c.yml @@ -88,7 +88,7 @@ on: jobs: a5c: - runs-on: runner8core + runs-on: ubuntu-latest permissions: contents: write pull-requests: write From 288220d0e307f9ed2170bbcdb9c9ee995ee1fc6f Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 09:57:13 +0300 Subject: [PATCH 04/21] Update a5c.yml --- .github/workflows/a5c.yml | 48 --------------------------------------- 1 file changed, 48 deletions(-) diff --git a/.github/workflows/a5c.yml b/.github/workflows/a5c.yml index e8e259fa1530..897b42a9f1f2 100644 --- a/.github/workflows/a5c.yml +++ b/.github/workflows/a5c.yml @@ -117,54 +117,6 @@ jobs: run: | echo "skip=true" >> "$GITHUB_OUTPUT" - - name: Setup Node.js (cache pnpm) - if: steps.filter-self.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version-file: .node-version - cache: pnpm - cache-dependency-path: pnpm-lock.yaml - check-latest: true - - - name: Enable Corepack - if: steps.filter-self.outputs.skip != 'true' - continue-on-error: true - run: corepack enable - - - name: Guard new build-script packages - if: steps.filter-self.outputs.skip != 'true' - continue-on-error: true - run: | - if [ -f package.json ]; then - node scripts/pnpm-approve-builds-guard.mjs || true - fi - - - name: Approve PNPM build scripts - if: steps.filter-self.outputs.skip != 'true' - continue-on-error: true - run: | - if [ -f package.json ]; then - bash scripts/pnpm-approve-builds.sh || true - fi - - - name: Install dependencies (pnpm via scripts/install.sh) - if: steps.filter-self.outputs.skip != 'true' - continue-on-error: true - env: - PNPM_APPROVE_BUILDS: 'esbuild sharp @tailwindcss/oxide protobufjs puppeteer' - run: | - if [ -f package.json ]; then - ./scripts/install.sh - else - echo "No package.json found; skipping install" - fi - - - name: Detect validator mode (flags/token) - id: detect-validator-mode - if: steps.filter-self.outputs.skip != 'true' - run: | - bash scripts/validator-mode.sh - - name: Run A5C id: agents if: steps.filter-self.outputs.skip != 'true' From b3273d53c222f8eef1336f23e360382a64e9813f Mon Sep 17 00:00:00 2001 From: developer-agent Date: Fri, 29 Aug 2025 07:03:53 +0000 Subject: [PATCH 05/21] docs: add initial plan for GitHub Models provider\n\nBy: [developer-agent](https://app.a5c.ai/a5c/agents/development/developer-agent) --- .../github-models-provider-20250829T070352Z.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/dev/developer-agent/github-models-provider-20250829T070352Z.md diff --git a/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md b/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md new file mode 100644 index 000000000000..18a7a8db92ab --- /dev/null +++ b/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md @@ -0,0 +1,16 @@ +# Add GitHub Models provider + +## Context +Implement built-in provider `github` to use GitHub Models via the Responses API. + +## Plan +- Add `github` provider in `codex-rs/core/src/model_provider_info.rs` +- Default `base_url` = `https://models.inference.ai.azure.com` +- `env_key` = `GITHUB_TOKEN` (Bearer) +- `wire_api` = `responses` +- Docs: update `docs/config.md` with usage +- Verify: fmt, lint, tests (core, then workspace) + +## Notes +- No changes to sandbox env vars. +- Skip provider-specific headers unless required. From 4ccffdc3e731e8dbce262aa5758109b0967141ce Mon Sep 17 00:00:00 2001 From: developer-agent Date: Fri, 29 Aug 2025 07:11:56 +0000 Subject: [PATCH 06/21] feat(core): add built-in 'github' provider for GitHub Models (Responses API)\n\n- base_url: https://models.inference.ai.azure.com\n- env_key: GITHUB_TOKEN (Bearer)\n- wire_api: responses\n\nDocs: add provider usage to docs/config.md\n\nBy: [developer-agent](https://app.a5c.ai/a5c/agents/development/developer-agent) --- codex-rs/core/src/model_provider_info.rs | 22 ++++++++++++++++++++++ docs/config.md | 16 ++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 1a3e5a27679f..b0329b78546b 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -281,6 +281,28 @@ pub fn built_in_model_providers() -> HashMap { requires_openai_auth: true, }, ), + ( + "github", + P { + name: "GitHub Models".into(), + // GitHub Models exposes an OpenAI‑compatible API hosted on Azure AI Inference. + // Users authenticate with a GitHub token via Bearer auth. + base_url: Some("https://models.inference.ai.azure.com".to_string()), + env_key: Some("GITHUB_TOKEN".to_string()), + env_key_instructions: Some( + "Set GITHUB_TOKEN with access to GitHub Models. See https://docs.github.com/en/github-models".to_string(), + ), + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + // Use global defaults for retry/timeout unless overridden in config.toml. + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_openai_auth: false, + }, + ), (BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()), ] .into_iter() diff --git a/docs/config.md b/docs/config.md index 7d0bd7d2b807..f5c248d11d00 100644 --- a/docs/config.md +++ b/docs/config.md @@ -66,6 +66,22 @@ base_url = "https://api.mistral.ai/v1" env_key = "MISTRAL_API_KEY" ``` +GitHub Models via the Responses API is supported out of the box under the provider id `github`: + +```toml +# Use GitHub Models +model_provider = "github" +model = "gpt-4o-mini" # or any supported model + +[model_providers.github] +name = "GitHub Models" # built-in +base_url = "https://models.inference.ai.azure.com" +env_key = "GITHUB_TOKEN" # Bearer token for GitHub Models +wire_api = "responses" +``` + +Ensure `GITHUB_TOKEN` is set in your environment with access to GitHub Models. See the GitHub documentation for details. + Note that Azure requires `api-version` to be passed as a query parameter, so be sure to specify it as part of `query_params` when defining the Azure provider: ```toml From e8d1879ba1f8c1b7cd7ef5ea7ab14f2d5900fec4 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 10:22:21 +0300 Subject: [PATCH 07/21] Change model to GPT-5 and update base URL --- docs/config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index f5c248d11d00..1e66da439762 100644 --- a/docs/config.md +++ b/docs/config.md @@ -71,13 +71,13 @@ GitHub Models via the Responses API is supported out of the box under the provid ```toml # Use GitHub Models model_provider = "github" -model = "gpt-4o-mini" # or any supported model +model = "openai/gpt-5" # or any supported model [model_providers.github] name = "GitHub Models" # built-in -base_url = "https://models.inference.ai.azure.com" +base_url = "https://models.github.ai/inference" env_key = "GITHUB_TOKEN" # Bearer token for GitHub Models -wire_api = "responses" +wire_api = "chat" ``` Ensure `GITHUB_TOKEN` is set in your environment with access to GitHub Models. See the GitHub documentation for details. From 98aa46eab07af17176408f6c671014c3ef68043a Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 10:25:16 +0300 Subject: [PATCH 08/21] Update GitHub Models base URL and wire API --- codex-rs/core/src/model_provider_info.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index b0329b78546b..ecc530cd43aa 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -287,12 +287,12 @@ pub fn built_in_model_providers() -> HashMap { name: "GitHub Models".into(), // GitHub Models exposes an OpenAI‑compatible API hosted on Azure AI Inference. // Users authenticate with a GitHub token via Bearer auth. - base_url: Some("https://models.inference.ai.azure.com".to_string()), + base_url: Some("https://models.github.ai/inference".to_string()), env_key: Some("GITHUB_TOKEN".to_string()), env_key_instructions: Some( "Set GITHUB_TOKEN with access to GitHub Models. See https://docs.github.com/en/github-models".to_string(), ), - wire_api: WireApi::Responses, + wire_api: WireApi::Chat, query_params: None, http_headers: None, env_http_headers: None, From edd8764508a66099c1ad13bcd9090f6a7295c586 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 10:25:43 +0300 Subject: [PATCH 09/21] Delete docs/dev/developer-agent/github-models-provider-20250829T070352Z.md --- .../github-models-provider-20250829T070352Z.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 docs/dev/developer-agent/github-models-provider-20250829T070352Z.md diff --git a/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md b/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md deleted file mode 100644 index 18a7a8db92ab..000000000000 --- a/docs/dev/developer-agent/github-models-provider-20250829T070352Z.md +++ /dev/null @@ -1,16 +0,0 @@ -# Add GitHub Models provider - -## Context -Implement built-in provider `github` to use GitHub Models via the Responses API. - -## Plan -- Add `github` provider in `codex-rs/core/src/model_provider_info.rs` -- Default `base_url` = `https://models.inference.ai.azure.com` -- `env_key` = `GITHUB_TOKEN` (Bearer) -- `wire_api` = `responses` -- Docs: update `docs/config.md` with usage -- Verify: fmt, lint, tests (core, then workspace) - -## Notes -- No changes to sandbox env vars. -- Skip provider-specific headers unless required. From 014b5ced97af6b207d81c91c64666d45490db052 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 12:40:09 +0300 Subject: [PATCH 10/21] =?UTF-8?q?Revert=20"=E2=9C=A8=20Add=20GitHub=20Mode?= =?UTF-8?q?ls=20provider=20(Responses=20API)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codex-rs/core/src/model_provider_info.rs | 22 ---------------------- docs/config.md | 16 ---------------- 2 files changed, 38 deletions(-) diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index ecc530cd43aa..1a3e5a27679f 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -281,28 +281,6 @@ pub fn built_in_model_providers() -> HashMap { requires_openai_auth: true, }, ), - ( - "github", - P { - name: "GitHub Models".into(), - // GitHub Models exposes an OpenAI‑compatible API hosted on Azure AI Inference. - // Users authenticate with a GitHub token via Bearer auth. - base_url: Some("https://models.github.ai/inference".to_string()), - env_key: Some("GITHUB_TOKEN".to_string()), - env_key_instructions: Some( - "Set GITHUB_TOKEN with access to GitHub Models. See https://docs.github.com/en/github-models".to_string(), - ), - wire_api: WireApi::Chat, - query_params: None, - http_headers: None, - env_http_headers: None, - // Use global defaults for retry/timeout unless overridden in config.toml. - request_max_retries: None, - stream_max_retries: None, - stream_idle_timeout_ms: None, - requires_openai_auth: false, - }, - ), (BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()), ] .into_iter() diff --git a/docs/config.md b/docs/config.md index 1e66da439762..7d0bd7d2b807 100644 --- a/docs/config.md +++ b/docs/config.md @@ -66,22 +66,6 @@ base_url = "https://api.mistral.ai/v1" env_key = "MISTRAL_API_KEY" ``` -GitHub Models via the Responses API is supported out of the box under the provider id `github`: - -```toml -# Use GitHub Models -model_provider = "github" -model = "openai/gpt-5" # or any supported model - -[model_providers.github] -name = "GitHub Models" # built-in -base_url = "https://models.github.ai/inference" -env_key = "GITHUB_TOKEN" # Bearer token for GitHub Models -wire_api = "chat" -``` - -Ensure `GITHUB_TOKEN` is set in your environment with access to GitHub Models. See the GitHub documentation for details. - Note that Azure requires `api-version` to be passed as a query parameter, so be sure to specify it as part of `query_params` when defining the Azure provider: ```toml From 5fe6ef32d3dd2b8908285d15c628ecf9b0d4a378 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:04:34 +0000 Subject: [PATCH 11/21] =?UTF-8?q?=E2=9C=A8=20CLI:=20add=20/usage=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated and approved by Validator Agent. See #31 and #32 for follow-ups. --- codex-rs/README.md | 12 +++ codex-rs/chatgpt/src/lib.rs | 1 + codex-rs/chatgpt/src/usage.rs | 80 ++++++++++++++ codex-rs/cli/src/lib.rs | 1 + codex-rs/cli/src/main.rs | 10 ++ codex-rs/cli/src/usage.rs | 100 ++++++++++++++++++ codex-rs/cli/tests/usage.rs | 11 ++ codex-rs/tui/src/chatwidget.rs | 7 ++ codex-rs/tui/src/history_cell.rs | 42 ++++++++ codex-rs/tui/src/slash_command.rs | 3 + .../cli-usage-20250829T084245Z.md | 22 ++++ package-lock.json | 33 ++++++ 12 files changed, 322 insertions(+) create mode 100644 codex-rs/chatgpt/src/usage.rs create mode 100644 codex-rs/cli/src/usage.rs create mode 100644 codex-rs/cli/tests/usage.rs create mode 100644 docs/dev/developer-agent/cli-usage-20250829T084245Z.md create mode 100644 package-lock.json diff --git a/codex-rs/README.md b/codex-rs/README.md index 390f5d31aa22..a844b5ac8436 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -63,6 +63,18 @@ codex completion zsh codex completion fish ``` +### Usage and Guardrail Resets + +Check your current guardrail usage and next reset times via: + +``` +codex usage +# or +codex /usage +``` + +When usage data is unavailable, the command prints a clear, non-fatal message and exits successfully. + ### Experimenting with the Codex Sandbox To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: diff --git a/codex-rs/chatgpt/src/lib.rs b/codex-rs/chatgpt/src/lib.rs index 440a309db64a..9b47b6f04bb3 100644 --- a/codex-rs/chatgpt/src/lib.rs +++ b/codex-rs/chatgpt/src/lib.rs @@ -2,3 +2,4 @@ pub mod apply_command; mod chatgpt_client; mod chatgpt_token; pub mod get_task; +pub mod usage; diff --git a/codex-rs/chatgpt/src/usage.rs b/codex-rs/chatgpt/src/usage.rs new file mode 100644 index 000000000000..20dd49419199 --- /dev/null +++ b/codex-rs/chatgpt/src/usage.rs @@ -0,0 +1,80 @@ +use anyhow::Context; +use codex_core::config::Config; +use serde::Deserialize; + +use crate::chatgpt_client::chatgpt_get_request; + +/// High-level summary of guardrail usage for display in CLI. +#[derive(Debug, Clone, Default)] +pub struct UsageSummary { + pub plan: Option, + pub standard_used_minutes: Option, + pub standard_limit_minutes: Option, + pub reasoning_used_minutes: Option, + pub reasoning_limit_minutes: Option, + pub next_reset_at: Option, +} + +/// Flexible wire model so we can tolerate backend changes without breaking the CLI. +#[derive(Debug, Deserialize)] +struct RawUsage { + #[serde(default)] + plan: Option, + #[serde(default)] + next_reset_at: Option, + #[serde(default)] + reset_at: Option, + #[serde(default)] + standard: Option, + #[serde(default)] + reasoning: Option, +} + +#[derive(Debug, Deserialize)] +struct Bucket { + #[serde(default)] + used_minutes: Option, + #[serde(default)] + limit_minutes: Option, + #[serde(default)] + used: Option, + #[serde(default)] + limit: Option, +} + +impl From for UsageSummary { + fn from(raw: RawUsage) -> Self { + let plan = raw.plan; + let next_reset_at = raw.next_reset_at.or(raw.reset_at); + let (mut standard_used_minutes, mut standard_limit_minutes) = (None, None); + let (mut reasoning_used_minutes, mut reasoning_limit_minutes) = (None, None); + + if let Some(b) = raw.standard { + standard_used_minutes = b.used_minutes.or(b.used); + standard_limit_minutes = b.limit_minutes.or(b.limit); + } + if let Some(b) = raw.reasoning { + reasoning_used_minutes = b.used_minutes.or(b.used); + reasoning_limit_minutes = b.limit_minutes.or(b.limit); + } + + UsageSummary { + plan, + standard_used_minutes, + standard_limit_minutes, + reasoning_used_minutes, + reasoning_limit_minutes, + next_reset_at, + } + } +} + +/// Fetch ChatGPT guardrail usage using the current auth and config. +pub async fn get_usage(config: &Config) -> anyhow::Result { + // This path is provided by the ChatGPT backend for Codex usage display. + // The structure is intentionally parsed via a flexible wire model. + let raw: RawUsage = chatgpt_get_request(config, "/wham/usage".to_string()) + .await + .context("Failed to fetch usage from ChatGPT backend")?; + Ok(raw.into()) +} diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index c6d80c0adfa9..7c8da6b5b167 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -2,6 +2,7 @@ pub mod debug_sandbox; mod exit_status; pub mod login; pub mod proto; +pub mod usage; use clap::Parser; use codex_common::CliConfigOverrides; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 2acc3d84c50c..c23c83ec7fc2 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -12,12 +12,14 @@ use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; use codex_cli::login::run_logout; use codex_cli::proto; +use codex_cli::usage::run_usage_command; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use std::path::PathBuf; use crate::proto::ProtoCli; +use codex_cli::usage::UsageCommand; /// Codex CLI /// @@ -76,6 +78,10 @@ enum Subcommand { /// Internal: generate TypeScript protocol bindings. #[clap(hide = true)] GenerateTs(GenerateTsCommand), + + /// Show current guardrail usage and reset times. + #[clap(visible_alias = "/usage")] + Usage(UsageCommand), } #[derive(Debug, Parser)] @@ -212,6 +218,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() Some(Subcommand::GenerateTs(gen_cli)) => { codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?; } + Some(Subcommand::Usage(mut usage_cli)) => { + prepend_config_flags(&mut usage_cli.config_overrides, cli.config_overrides); + run_usage_command(usage_cli.config_overrides).await?; + } } Ok(()) diff --git a/codex-rs/cli/src/usage.rs b/codex-rs/cli/src/usage.rs new file mode 100644 index 000000000000..743234549856 --- /dev/null +++ b/codex-rs/cli/src/usage.rs @@ -0,0 +1,100 @@ +use codex_common::CliConfigOverrides; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; +use codex_login::AuthMode; +use codex_login::CodexAuth; + +use codex_chatgpt::usage::get_usage as get_chatgpt_usage; + +#[derive(Debug, clap::Parser)] +pub struct UsageCommand { + #[clap(skip)] + pub config_overrides: CliConfigOverrides, +} + +pub async fn run_usage_command(cli_config_overrides: CliConfigOverrides) -> anyhow::Result<()> { + let config = load_config_or_exit(cli_config_overrides); + + match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) { + Ok(Some(auth)) => match auth.mode { + AuthMode::ApiKey => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + println!("Plan: {plan}"); + println!( + "Using an API key. Guardrail usage does not apply; billing is per-token.\nSee https://platform.openai.com/account/usage for detailed usage." + ); + Ok(()) + } + AuthMode::ChatGPT => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + match get_chatgpt_usage(&config).await { + Ok(summary) => { + println!("Plan: {plan}"); + if let Some(when) = summary.next_reset_at.as_deref() { + println!("Next reset: {when}"); + } + if let (Some(u), Some(l)) = ( + summary.standard_used_minutes, + summary.standard_limit_minutes, + ) { + println!("Standard: {u} / {l} minutes used"); + } + if let (Some(u), Some(l)) = ( + summary.reasoning_used_minutes, + summary.reasoning_limit_minutes, + ) { + println!("Reasoning: {u} / {l} minutes used"); + } + + // If no buckets printed, fall back to a generic message. + if summary.standard_used_minutes.is_none() + && summary.reasoning_used_minutes.is_none() + { + println!("Usage data retrieved, but no bucket details available."); + } + Ok(()) + } + Err(e) => { + println!( + "Plan: {plan}\nUnable to retrieve usage from ChatGPT backend.\nReason: {e}\nUsage information is currently unavailable." + ); + Ok(()) + } + } + } + }, + Ok(None) => { + println!("Not logged in. Usage information requires authentication.\nRun: codex login"); + Ok(()) + } + Err(e) => { + println!( + "Unable to determine authentication status.\nReason: {e}\nUsage information is currently unavailable." + ); + Ok(()) + } + } +} + +fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { + let cli_overrides = match cli_config_overrides.parse_overrides() { + Ok(v) => v, + Err(e) => { + eprintln!("Error parsing -c overrides: {e}"); + std::process::exit(1); + } + }; + + let config_overrides = ConfigOverrides::default(); + match Config::load_with_cli_overrides(cli_overrides, config_overrides) { + Ok(config) => config, + Err(e) => { + eprintln!("Error loading configuration: {e}"); + std::process::exit(1); + } + } +} diff --git a/codex-rs/cli/tests/usage.rs b/codex-rs/cli/tests/usage.rs new file mode 100644 index 000000000000..dff0dd1fa357 --- /dev/null +++ b/codex-rs/cli/tests/usage.rs @@ -0,0 +1,11 @@ +use codex_cli::usage::run_usage_command; +use codex_common::CliConfigOverrides; + +#[tokio::test] +async fn usage_command_runs() { + // Should not error; prints a helpful message even if not logged in. + let overrides = CliConfigOverrides::default(); + run_usage_command(overrides) + .await + .expect("usage should run"); +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4695c6566cf4..17845e4dfd78 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -823,6 +823,9 @@ impl ChatWidget { SlashCommand::Status => { self.add_status_output(); } + SlashCommand::Usage => { + self.add_usage_output(); + } SlashCommand::Mcp => { self.add_mcp_output(); } @@ -1070,6 +1073,10 @@ impl ChatWidget { )); } + pub(crate) fn add_usage_output(&mut self) { + self.add_to_history(history_cell::new_usage_output(&self.config)); + } + /// Open a popup to choose the model preset (model + reasoning effort). pub(crate) fn open_model_popup(&mut self) { let current_model = self.config.model.clone(); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index cc05b36fc0c9..a6e10ec6a161 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -18,6 +18,8 @@ use codex_core::protocol::McpInvocation; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; +use codex_login::AuthMode; +use codex_login::CodexAuth; use codex_login::get_auth_file; use codex_login::try_read_auth_json; use codex_protocol::parse_command::ParsedCommand; @@ -846,6 +848,46 @@ pub(crate) fn new_status_output( PlainHistoryCell { lines } } +/// Render guardrail usage information (placeholder until providers are wired). +pub(crate) fn new_usage_output(config: &Config) -> PlainHistoryCell { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("")); + lines.push(Line::from("/usage".magenta())); + + match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) { + Ok(Some(auth)) => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + match auth.mode { + AuthMode::ApiKey | AuthMode::ChatGPT => { + lines.push(Line::from("")); + lines.push(Line::from("Usage information is currently unavailable.")); + lines.push(Line::from(format!("Plan: {plan}"))); + lines.push(Line::from( + "Note: Guardrail usage and reset times will appear here when supported.", + )); + } + } + } + Ok(None) => { + lines.push(Line::from("")); + lines.push(Line::from( + "Not logged in. Usage information requires authentication.", + )); + lines.push(Line::from("Run: codex login")); + } + Err(e) => { + lines.push(Line::from("")); + lines.push(Line::from("Unable to determine authentication status.")); + lines.push(Line::from(format!("Reason: {e}"))); + lines.push(Line::from("Usage information is currently unavailable.")); + } + } + + PlainHistoryCell { lines } +} + /// Render a summary of configured MCP servers from the current `Config`. pub(crate) fn empty_mcp_output() -> PlainHistoryCell { let lines: Vec> = vec![ diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 3268a92a2db2..919751dec6c4 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -20,6 +20,7 @@ pub enum SlashCommand { Diff, Mention, Status, + Usage, Mcp, Logout, Quit, @@ -38,6 +39,7 @@ impl SlashCommand { SlashCommand::Diff => "show git diff (including untracked files)", SlashCommand::Mention => "mention a file", SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Usage => "show guardrail usage and reset times", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Approvals => "choose what Codex can do without approval", SlashCommand::Mcp => "list configured MCP tools", @@ -65,6 +67,7 @@ impl SlashCommand { SlashCommand::Diff | SlashCommand::Mention | SlashCommand::Status + | SlashCommand::Usage | SlashCommand::Mcp | SlashCommand::Quit => true, diff --git a/docs/dev/developer-agent/cli-usage-20250829T084245Z.md b/docs/dev/developer-agent/cli-usage-20250829T084245Z.md new file mode 100644 index 000000000000..8dd88dd97fe6 --- /dev/null +++ b/docs/dev/developer-agent/cli-usage-20250829T084245Z.md @@ -0,0 +1,22 @@ +# CLI: /usage command + +## Context +Implement a discoverable `codex usage` (`/usage` alias) to print current 5h and weekly guardrail usage with reset times. Gracefully handle unavailable data. + +## Plan +- Add `usage` subcommand in `codex-rs/cli` (alias `/usage`). +- Create a simple usage reporter with pluggable providers; default to a clear fallback message. +- Update help, add a basic CLI test. +- Validate with `cargo test -p codex-cli`. + +## Notes +- Avoid touching sandbox env var logic. +- Keep TUI unchanged to avoid snapshot breaks; future PR can add a compact indicator. + +## Results +- Implemented `usage` subcommand (alias `/usage`) with graceful fallback. +- Added test and README updates. +- Validated with `cargo test -p codex-cli`. + +## Next +- Plug in real usage providers when upstream endpoints are available or when rate-limit headers are persisted by the client layer. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000000..bb67c7525818 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "codex-monorepo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-monorepo", + "devDependencies": { + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=22", + "pnpm": ">=9.0.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} From 1b5bc21ae1c7dffa3bb5e4fe16a70f0e7b81c194 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 14:18:58 +0300 Subject: [PATCH 12/21] Update a5c.yml --- .github/workflows/a5c.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/a5c.yml b/.github/workflows/a5c.yml index 897b42a9f1f2..ac9f8b06fc4c 100644 --- a/.github/workflows/a5c.yml +++ b/.github/workflows/a5c.yml @@ -74,7 +74,7 @@ on: - cron: '*/30 * * * *' workflow_run: types: [completed] - workflows: [Build,Deploy,Tests,Release,E2E Tests,Infrastructure Deployment,Integration Tests] + workflows: [Build,Deploy,Tests,Release,E2E Tests,Infrastructure Deployment,Integration Tests,rust-ci] workflow_dispatch: inputs: agent_uri: From 81d036d6f0fa838dfd82851c31df0f9109bf85a9 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 14:30:43 +0300 Subject: [PATCH 13/21] Bump version to 0.27.1 in Cargo.toml --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 4155992293e1..563054f94372 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -22,7 +22,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.0.0" +version = "0.27.1" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From 058787a76daff9235e43a0b0e0209e0bfb0c8aa6 Mon Sep 17 00:00:00 2001 From: build-fixer-agent Date: Fri, 29 Aug 2025 11:31:53 +0000 Subject: [PATCH 14/21] fix(core): insert pending approval before emitting events to avoid race on fast paths (ARM)\n\n- Move map insert before send in request_command_approval & request_patch_approval\n- Resolves flaky mcp-server test on aarch64 gnu (pending approval not found)\n\nBy: build-fixer-agent(https://app.a5c.ai/a5c/agents/development/build-fixer-agent) --- codex-rs/core/src/codex.rs | 20 +++++++++---------- .../arm-elicit-race-20250829T113152Z.md | 17 ++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d7c39c14a566..aed76aade242 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -613,8 +613,12 @@ impl Session { reason: Option, ) -> oneshot::Receiver { let (tx_approve, rx_approve) = oneshot::channel(); + { + let mut state = self.state.lock_unchecked(); + state.pending_approvals.insert(sub_id.clone(), tx_approve); + } let event = Event { - id: sub_id.clone(), + id: sub_id, msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id, command, @@ -623,10 +627,6 @@ impl Session { }), }; let _ = self.tx_event.send(event).await; - { - let mut state = self.state.lock_unchecked(); - state.pending_approvals.insert(sub_id, tx_approve); - } rx_approve } @@ -639,8 +639,12 @@ impl Session { grant_root: Option, ) -> oneshot::Receiver { let (tx_approve, rx_approve) = oneshot::channel(); + { + let mut state = self.state.lock_unchecked(); + state.pending_approvals.insert(sub_id.clone(), tx_approve); + } let event = Event { - id: sub_id.clone(), + id: sub_id, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id, changes: convert_apply_patch_to_protocol(action), @@ -649,10 +653,6 @@ impl Session { }), }; let _ = self.tx_event.send(event).await; - { - let mut state = self.state.lock_unchecked(); - state.pending_approvals.insert(sub_id, tx_approve); - } rx_approve } diff --git a/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md b/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md new file mode 100644 index 000000000000..78969bc99f98 --- /dev/null +++ b/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md @@ -0,0 +1,17 @@ +# Fix: Patch approval race on ARM GNU runner + +## Context +- Workflow run failed on job: ubuntu-24.04-arm - aarch64-unknown-linux-gnu +- Failure: mcp-server test `suite::codex_tool::test_patch_approval_triggers_elicitation` timed out +- Log excerpt showed: `No pending approval found for sub_id: 1` + +## Root cause +- Race condition in `codex-core`: inserted `pending_approvals` after sending event on channel. On fast paths, approval arrives before insertion. + +## Change +- Reordered insertion before sending events in: + - `request_command_approval` + - `request_patch_approval` + +## Verification +- Ran `cargo test -p codex-mcp-server`: all tests passed locally. From c43d30f6c91f0020f94a351b2ca4a23538c17ca6 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 14:58:52 +0300 Subject: [PATCH 15/21] Change repository from 'openai/codex' to 'a5c-incubator/codex' --- scripts/publish_to_npm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish_to_npm.py b/scripts/publish_to_npm.py index f79843ffc853..1abe4252578d 100755 --- a/scripts/publish_to_npm.py +++ b/scripts/publish_to_npm.py @@ -69,7 +69,7 @@ def main() -> int: download_dir.mkdir(parents=True, exist_ok=True) # 1) Download the artifact using gh - repo = "openai/codex" + repo = "a5c-incubator/codex" gh_cmd = [ "gh", "release", From dc9b2b8eba5887923d8023ed80fd5608d6d44db1 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 15:17:32 +0300 Subject: [PATCH 16/21] Update Cargo.toml --- codex-rs/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 563054f94372..4155992293e1 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -22,7 +22,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.27.1" +version = "0.0.0" # Track the edition for all workspace crates in one place. Individual # crates can still override this value, but keeping it here means new # crates created with `cargo new -w ...` automatically inherit the 2024 From d91021286179c8f6f5eae838614d45a993a988b4 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Fri, 29 Aug 2025 15:28:39 +0300 Subject: [PATCH 17/21] Change workflow URL in install_native_deps.sh Updated workflow URL to point to the latest GitHub Action run. --- codex-cli/scripts/install_native_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh index 6cf2faafc849..0bcac89bff1e 100755 --- a/codex-cli/scripts/install_native_deps.sh +++ b/codex-cli/scripts/install_native_deps.sh @@ -20,7 +20,7 @@ CODEX_CLI_ROOT="" # Until we start publishing stable GitHub releases, we have to grab the binaries # from the GitHub Action that created them. Update the URL below to point to the # appropriate workflow run: -WORKFLOW_URL="https://github.com/openai/codex/actions/runs/16840150768" # rust-v0.20.0-alpha.2 +WORKFLOW_URL="https://github.com/a5c-incubator/codex/actions/runs/17322665068" # rust-v0.20.0-alpha.2 while [[ $# -gt 0 ]]; do case "$1" in From c86bcf531c95caa130c6b34effe95bea9d09e8b3 Mon Sep 17 00:00:00 2001 From: Tal Muskal Date: Sat, 30 Aug 2025 04:23:18 +0300 Subject: [PATCH 18/21] Update config.yml --- .a5c/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.a5c/config.yml b/.a5c/config.yml index 57860a730ceb..5d0f96a682ad 100644 --- a/.a5c/config.yml +++ b/.a5c/config.yml @@ -23,6 +23,4 @@ remote_agents: alias: recruiter-agent - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/product-optimizer-agent.agent.md alias: product-optimizer-agent - - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/reviver-agent.agent.md - alias: reviver-agent groups: {} From d0747481623813908bac4ff785aedc15f5effe94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 02:31:16 +0000 Subject: [PATCH 19/21] =?UTF-8?q?=F0=9F=93=9D=20docs:=20codex=20exec=20aut?= =?UTF-8?q?omation=20guide=20(CI/CLI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approved by Validator Agent\n\nBy: validator-agent(https://app.a5c.ai/a5c/agents/development/validator-agent) --- README.md | 2 +- docs/advanced.md | 4 +- docs/automation-exec.md | 113 ++++++++++++++++++ .../automation-codex-exec-20250830T022024Z.md | 4 + .../capture_jsonl_and_fail_on_errors.sh | 24 ++++ scripts/codex-exec-examples/run_sync.sh | 8 ++ scripts/codex-exec-examples/stdin_prompt.sh | 11 ++ 7 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 docs/automation-exec.md create mode 100644 docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md create mode 100755 scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh create mode 100755 scripts/codex-exec-examples/run_sync.sh create mode 100755 scripts/codex-exec-examples/stdin_prompt.sh diff --git a/README.md b/README.md index 668a03acaf85..5b946e8f5caf 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Codex CLI supports a rich set of configuration options, with preferences stored - [Login on a "Headless" machine](./docs/authentication.md#connecting-on-a-headless-machine) - [**Advanced**](./docs/advanced.md) - [Non-interactive / CI mode](./docs/advanced.md#non-interactive--ci-mode) + - [Automation with codex exec](./docs/automation-exec.md) - [Tracing / verbose logging](./docs/advanced.md#tracing--verbose-logging) - [Model Context Protocol (MCP)](./docs/advanced.md#model-context-protocol-mcp) - [**Zero data retention (ZDR)**](./docs/zdr.md) @@ -99,4 +100,3 @@ Codex CLI supports a rich set of configuration options, with preferences stored ## License This repository is licensed under the [Apache-2.0 License](LICENSE). - diff --git a/docs/advanced.md b/docs/advanced.md index 26f735991f2b..74f5b117dbf8 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -2,7 +2,7 @@ ## Non-interactive / CI mode -Run Codex head-less in pipelines. Example GitHub Action step: +Run Codex head-less in pipelines. See also: `docs/automation-exec.md` for a complete guide. Example GitHub Action step: ```yaml - name: Update changelog via Codex @@ -39,4 +39,4 @@ env = { "API_KEY" = "value" } ``` > [!TIP] -> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. \ No newline at end of file +> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. diff --git a/docs/automation-exec.md b/docs/automation-exec.md new file mode 100644 index 000000000000..5ef55c596753 --- /dev/null +++ b/docs/automation-exec.md @@ -0,0 +1,113 @@ +# Automation with `codex exec` + +Run Codex headless for CI, cron jobs, or local scripts. The `codex exec` subcommand processes a prompt non‑interactively, streams progress to stdout, and exits when the task completes. + +- Basic use: `codex exec "…"` +- From stdin: `echo "…" | codex exec -` +- JSONL logs: `codex exec --json "…" | tee codex.jsonl` +- Save last reply: `codex exec --output-last-message out.txt "…"` + +## Prompt input + +- Argument: `codex exec "bump CHANGELOG for next release"` +- Stdin: `echo "bump CHANGELOG" | codex exec -` + - If no argument is given and stdin is a TTY, `codex exec` exits with an error. Use `-` to force reading from stdin. + +## Exit codes + +`codex exec` exits with: +- 0 on normal completion. +- 1 for usage/config errors, e.g.: + - No prompt provided (and stdin not forced via `-`). + - Failed `-c` overrides parsing. + - Outside a Git repo without `--skip-git-repo-check`. + +Note: Non‑zero exit codes from model‑executed shell commands are surfaced in logs/events, but do not change the process exit code. In CI, use `--json` and filter events (see below) to fail your job on specific conditions. + +## Approvals and sandbox + +- Non‑interactive runs default to “never ask for approval”. +- Recommended for automation: `--full-auto` (workspace‑write sandbox, no prompts): + - `codex exec --full-auto "update CHANGELOG for next release"` +- Or pick an explicit sandbox: `--sandbox read-only|workspace-write|danger-full-access`. + - See `docs/sandbox.md` for details and policies. + +## Output modes + +- Human logs (default): timestamped, human‑readable progress and diffs. Use `--color=never` for plain logs. +- JSONL logs: `--json` prints one JSON object per line for events. Streaming deltas are suppressed; all other events are included. The first two lines are a compact config summary and the prompt you provided, then event lines follow. +- Last message file: `--output-last-message path.txt` writes the assistant’s final message to a file. + +### JSONL examples (with jq) + +Capture logs: + +``` +codex exec --json --full-auto "update CHANGELOG for next release" | tee codex.jsonl +``` + +List commands and exit codes: + +``` +rg '"type":"exec_command_end"' codex.jsonl \ + | jq -r 'fromjson | select(.msg.type=="exec_command_end") | [.msg.call_id, (.msg.exit_code // 0), (.msg.duration_ms // null)] | @tsv' +``` + +Fail if any command exits non‑zero or the run was aborted: + +``` +# exits 1 if any matching event exists +jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" +)' codex.jsonl >/dev/null +``` + +Save the last assistant message for downstream steps: + +``` +codex exec --json --output-last-message last.txt "summarize recent changes" +cat last.txt +``` + +## CI snippets + +### GitHub Actions + +``` +- name: Install Codex + run: npm install -g @openai/codex + +- name: Bump changelog via Codex (JSONL; fail on tool errors) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_KEY }} + run: | + set -euo pipefail + codex exec --json --full-auto "update CHANGELOG for next release" | tee codex.jsonl + # Fail the job if any command failed, or agent reported an error/abort + jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" + )' codex.jsonl >/dev/null && { echo "codex exec reported failures"; exit 1; } || true +``` + +Tips: +- Prefer `--full-auto` for CI to allow workspace writes under a sandbox. +- For headless machines, API key auth is simplest. See `docs/authentication.md`. +- Set `--color=never` to produce plain logs if needed. + +## Local scripts + +See `scripts/codex-exec-examples/` for runnable examples: +- `run_sync.sh`: simple synchronous run with human logs. +- `stdin_prompt.sh`: pass the prompt via stdin. +- `capture_jsonl_and_fail_on_errors.sh`: capture JSONL, validate with jq, and set an appropriate exit code. + +## Related docs + +- Sandbox & approvals: `docs/sandbox.md` +- Configuration: `docs/config.md` +- Advanced tips: `docs/advanced.md` + diff --git a/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md b/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md new file mode 100644 index 000000000000..d4d726d0e94b --- /dev/null +++ b/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md @@ -0,0 +1,4 @@ +# Dev log for docs/automation-codex-exec + +- Initial scaffold for Automation with codex exec docs and example scripts. +- Will add examples for stdin, JSONL logs, failure handling, and CI snippets. diff --git a/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh b/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh new file mode 100755 index 000000000000..e8b99e214b6a --- /dev/null +++ b/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Capture JSONL logs and fail the script if any command run by the agent +# exited non-zero, or if the agent reported an error/abort. + +require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 2; }; } +require jq + +LOG_FILE=${1:-codex.jsonl} + +codex exec --json --full-auto "update CHANGELOG for next release" | tee "$LOG_FILE" + +if jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" + )' "$LOG_FILE" >/dev/null; then + echo "codex exec reported failures; see $LOG_FILE" >&2 + exit 1 +fi + +echo "codex exec completed successfully" + diff --git a/scripts/codex-exec-examples/run_sync.sh b/scripts/codex-exec-examples/run_sync.sh new file mode 100755 index 000000000000..0041bb6b9c39 --- /dev/null +++ b/scripts/codex-exec-examples/run_sync.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple synchronous run with human-readable logs. +# Requires Codex CLI to be installed and authenticated. + +codex exec --full-auto --color=never "update CHANGELOG for next release" + diff --git a/scripts/codex-exec-examples/stdin_prompt.sh b/scripts/codex-exec-examples/stdin_prompt.sh new file mode 100755 index 000000000000..ffa5449aee19 --- /dev/null +++ b/scripts/codex-exec-examples/stdin_prompt.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Provide the prompt via stdin using '-'. + +cat <<'PROMPT' | codex exec --full-auto - +Generate release notes in CHANGELOG.md for the latest commits. +- Keep the style consistent with prior entries. +- Include a short summary and categorized changes. +PROMPT + From 9f591435de3d6aa347b6c7133ed1721af350db04 Mon Sep 17 00:00:00 2001 From: developer-agent Date: Sat, 30 Aug 2025 02:54:42 +0000 Subject: [PATCH 20/21] =?UTF-8?q?=F0=9F=A7=AD=20Start:=20plan=5Fmode=20pro?= =?UTF-8?q?tocol/config=20scaffolding\n\nBy:=20developer-agent(https://app?= =?UTF-8?q?.a5c.ai/a5c/agents/development/developer-agent)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md diff --git a/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md b/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md new file mode 100644 index 000000000000..903fc07a89fb --- /dev/null +++ b/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md @@ -0,0 +1 @@ +Started work on issue #23 at 2025-08-30T02:54:42Z. From 17b80a2b027ea64fad63c2112defe081ea6a420c Mon Sep 17 00:00:00 2001 From: developer-agent Date: Sat, 30 Aug 2025 03:14:56 +0000 Subject: [PATCH 21/21] feat(protocol/core): add plan_mode config + Plan events\n\n- Config: add boolean plan_mode with default false; CLI overrides\n- EnvironmentContext: include optional in XML\n- Protocol: add PlanProposed/PlanDecision types; expose plan_mode on SessionConfigured; add per-turn overrides\n- Update MCP server + TUI/exec to compile with new types\n- Update tests for serde + defaulting\n\nBy: developer-agent(https://app.a5c.ai/a5c/agents/development/developer-agent) --- codex-rs/core/src/codex.rs | 12 ++- codex-rs/core/src/config.rs | 17 +++++ codex-rs/core/src/config_profile.rs | 2 + codex-rs/core/src/environment_context.rs | 7 ++ codex-rs/core/tests/suite/prompt_caching.rs | 2 + .../src/event_processor_with_human_output.rs | 14 +++- codex-rs/exec/src/lib.rs | 1 + .../mcp-server/src/codex_message_processor.rs | 3 + codex-rs/mcp-server/src/codex_tool_config.rs | 1 + codex-rs/mcp-server/src/codex_tool_runner.rs | 4 + codex-rs/mcp-server/src/outgoing_message.rs | 3 + codex-rs/mcp-server/tests/suite/config.rs | 1 + codex-rs/protocol/src/config_types.rs | 4 + codex-rs/protocol/src/protocol.rs | 74 ++++++++++++++++++- codex-rs/tui/src/chatwidget.rs | 5 ++ codex-rs/tui/src/history_cell.rs | 7 +- codex-rs/tui/src/lib.rs | 1 + 17 files changed, 146 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index aed76aade242..69b170603cce 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -556,6 +556,7 @@ impl Session { Some(turn_context.approval_policy), Some(turn_context.sandbox_policy.clone()), Some(sess.user_shell.clone()), + if config.plan_mode { Some(true) } else { None }, ))); sess.record_conversation_items(&conversation_items).await; @@ -567,6 +568,7 @@ impl Session { model, history_log_id, history_entry_count, + plan_mode: config.plan_mode, }), }) .chain(post_session_configured_error_events.into_iter()); @@ -1070,6 +1072,7 @@ async fn submission_loop( model, effort, summary, + plan_mode, } => { // Recalculate the persistent turn context with provided overrides. let prev = Arc::clone(&turn_context); @@ -1138,13 +1141,19 @@ async fn submission_loop( // Install the new persistent context for subsequent tasks/turns. turn_context = Arc::new(new_turn_context); - if cwd.is_some() || approval_policy.is_some() || sandbox_policy.is_some() { + if cwd.is_some() + || approval_policy.is_some() + || sandbox_policy.is_some() + || plan_mode.is_some() + { sess.record_conversation_items(&[ResponseItem::from(EnvironmentContext::new( cwd, approval_policy, sandbox_policy, // Shell is not configurable from turn to turn None, + // Allow per-turn override of plan mode via context + plan_mode, ))]) .await; } @@ -1166,6 +1175,7 @@ async fn submission_loop( model, effort, summary, + plan_mode: _, } => { // attempt to inject input into current task if let Err(items) = sess.inject_input(items) { diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 7845f5d4ce07..ed68d2942b8c 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -185,6 +185,10 @@ pub struct Config { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: bool, + + /// When true, the agent operates in Plan Mode, proposing a plan first and + /// awaiting a decision before execution. + pub plan_mode: bool, } impl Config { @@ -497,6 +501,9 @@ pub struct ConfigToml { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option, + + /// Top-level Plan Mode toggle. Defaults to false when omitted. + pub plan_mode: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] @@ -605,6 +612,7 @@ pub struct ConfigOverrides { pub disable_response_storage: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, + pub plan_mode: Option, } impl Config { @@ -633,6 +641,7 @@ impl Config { disable_response_storage, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, + plan_mode: _override_plan_mode, } = overrides; let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { @@ -807,6 +816,11 @@ impl Config { .unwrap_or(false), include_view_image_tool, disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), + plan_mode: overrides + .plan_mode + .or(config_profile.plan_mode) + .or(cfg.plan_mode) + .unwrap_or(false), }; Ok(config) } @@ -1177,6 +1191,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }, o3_profile_config ); @@ -1235,6 +1250,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1308,6 +1324,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); diff --git a/codex-rs/core/src/config_profile.rs b/codex-rs/core/src/config_profile.rs index 54869919fbc0..340d70439c78 100644 --- a/codex-rs/core/src/config_profile.rs +++ b/codex-rs/core/src/config_profile.rs @@ -15,6 +15,8 @@ pub struct ConfigProfile { /// [`ModelProviderInfo`] to use. pub model_provider: Option, pub approval_policy: Option, + /// When true, the agent operates in Plan Mode. + pub plan_mode: Option, pub disable_response_storage: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index b7ee862517a7..ef5aad9c3621 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -29,6 +29,7 @@ pub(crate) struct EnvironmentContext { pub sandbox_mode: Option, pub network_access: Option, pub shell: Option, + pub plan_mode: Option, } impl EnvironmentContext { @@ -37,6 +38,7 @@ impl EnvironmentContext { approval_policy: Option, sandbox_policy: Option, shell: Option, + plan_mode: Option, ) -> Self { Self { cwd, @@ -60,6 +62,7 @@ impl EnvironmentContext { None => None, }, shell, + plan_mode, } } } @@ -76,6 +79,7 @@ impl EnvironmentContext { /// ... /// ... /// ... + /// ... /// /// ``` pub fn serialize_to_xml(self) -> String { @@ -101,6 +105,9 @@ impl EnvironmentContext { { lines.push(format!(" {shell_name}")); } + if let Some(plan_mode) = self.plan_mode { + lines.push(format!(" {plan_mode}")); + } lines.push(ENVIRONMENT_CONTEXT_END.to_string()); lines.join("\n") } diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 999f8072862a..46d221e03b2f 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -393,6 +393,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() { model: Some("o3".to_string()), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), + plan_mode: None, }) .await .unwrap(); @@ -521,6 +522,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() { model: "o3".to_string(), effort: ReasoningEffort::High, summary: ReasoningSummary::Detailed, + plan_mode: false, }) .await .unwrap(); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 8403bbde28bd..f23dab3aeb90 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -508,10 +508,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { } EventMsg::SessionConfigured(session_configured_event) => { let SessionConfiguredEvent { - session_id, - model, - history_log_id: _, - history_entry_count: _, + session_id, model, .. } = session_configured_event; ts_println!( @@ -529,6 +526,15 @@ impl EventProcessor for EventProcessorWithHumanOutput { ts_println!(self, "explanation: {explanation:?}"); ts_println!(self, "plan: {plan:?}"); } + EventMsg::PlanProposed(proposed) => { + ts_println!(self, "{}", "plan proposed:".style(self.magenta)); + for (i, step) in proposed.plan.steps.iter().enumerate() { + ts_println!(self, " {}. [{}] {}", i + 1, step.kind, step.description); + } + if let Some(notes) = &proposed.plan.notes { + ts_println!(self, "notes: {}", notes); + } + } EventMsg::GetHistoryEntryResponse(_) => { // Currently ignored in exec output. } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 785272a692c4..d2ff1773b742 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -148,6 +148,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any base_instructions: None, include_plan_tool: None, include_apply_patch_tool: None, + plan_mode: None, include_view_image_tool: None, disable_response_storage: oss.then_some(true), show_raw_agent_reasoning: oss.then_some(true), diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index aae463ad925b..e4bccdcbfba6 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -399,6 +399,7 @@ impl CodexMessageProcessor { codex_protocol::config_types::ConfigProfile { model: v.model, approval_policy: v.approval_policy, + plan_mode: v.plan_mode, model_reasoning_effort: v.model_reasoning_effort, }, ) @@ -539,6 +540,7 @@ impl CodexMessageProcessor { model, effort, summary, + plan_mode: self.config.plan_mode, }) .await; @@ -798,6 +800,7 @@ fn derive_config_from_params( base_instructions, include_plan_tool, include_apply_patch_tool, + plan_mode: None, include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index c29cb52c22a4..f80ddfa56864 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -160,6 +160,7 @@ impl CodexToolCallParam { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + plan_mode: None, include_apply_patch_tool: None, include_view_image_tool: None, disable_response_storage: None, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index b297dadd9b73..ca06cba7527e 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -193,6 +193,10 @@ async fn run_codex_tool_session_inner( .await; continue; } + EventMsg::PlanProposed(_) => { + // Tool currently does not handle plan review; ignore. + continue; + } EventMsg::Error(err_event) => { // Return a response to conclude the tool call when the Codex session reports an error (e.g., interruption). let result = json!({ diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index abfc542e24f9..d6f8d3a4c2f7 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -260,6 +260,7 @@ mod tests { model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, + plan_mode: false, }), }; @@ -289,6 +290,7 @@ mod tests { model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, + plan_mode: false, }; let event = Event { id: "1".to_string(), @@ -317,6 +319,7 @@ mod tests { "model": session_configured_event.model, "history_log_id": session_configured_event.history_log_id, "history_entry_count": session_configured_event.history_entry_count, + "plan_mode": session_configured_event.plan_mode, "type": "session_configured", } }); diff --git a/codex-rs/mcp-server/tests/suite/config.rs b/codex-rs/mcp-server/tests/suite/config.rs index cec41b25e002..e57e84198a64 100644 --- a/codex-rs/mcp-server/tests/suite/config.rs +++ b/codex-rs/mcp-server/tests/suite/config.rs @@ -71,6 +71,7 @@ async fn get_config_toml_returns_subset() { ConfigProfile { model: Some("gpt-4o".into()), approval_policy: Some(AskForApproval::OnRequest), + plan_mode: None, model_reasoning_effort: Some(ReasoningEffort::High), }, )])), diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 46fd9c1359b8..08d90217f5d3 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -57,5 +57,9 @@ pub enum SandboxMode { pub struct ConfigProfile { pub model: Option, pub approval_policy: Option, + /// When true, the agent operates in Plan Mode: it proposes a plan first + /// and awaits a decision (accept/edit/cancel) before execution. + #[serde(default)] + pub plan_mode: Option, pub model_reasoning_effort: Option, } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 1f6cbe07b805..1e4d8e7d1fba 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -77,6 +77,12 @@ pub enum Op { /// Will only be honored if the model is configured to use reasoning. summary: ReasoningSummaryConfig, + + /// Whether this turn should run with Plan Mode enabled. When true, the + /// agent will propose a plan first and await a decision before + /// executing. Defaults to the session-configured value. + #[serde(default)] + plan_mode: bool, }, /// Override parts of the persistent turn context for subsequent turns. @@ -109,6 +115,10 @@ pub enum Op { /// Updated reasoning summary preference (honored only for reasoning-capable models). #[serde(skip_serializing_if = "Option::is_none")] summary: Option, + + /// Updated Plan Mode setting for subsequent turns. + #[serde(skip_serializing_if = "Option::is_none")] + plan_mode: Option, }, /// Approve a command execution @@ -127,6 +137,17 @@ pub enum Op { decision: ReviewDecision, }, + /// Decision in response to a proposed plan. + PlanDecision { + /// The id of the submission correlated with the proposal. + id: String, + /// The user's decision. + decision: PlanDecision, + /// When editing, the updated plan to use. + #[serde(skip_serializing_if = "Option::is_none")] + edited_plan: Option, + }, + /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has @@ -437,6 +458,9 @@ pub enum EventMsg { /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), + /// The agent proposes a plan for the user to review and approve. + PlanProposed(PlanProposedEvent), + McpToolCallBegin(McpToolCallBeginEvent), McpToolCallEnd(McpToolCallEndEvent), @@ -839,6 +863,53 @@ pub struct SessionConfiguredEvent { /// Current number of entries in the history log. pub history_entry_count: usize, + + /// Whether the session is in Plan Mode. + #[serde(default)] + pub plan_mode: bool, +} + +/// A single step in a proposed plan. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct PlanStep { + pub kind: PlanStepKind, + /// Short, imperative description of the step. + pub description: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Display, TS)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PlanStepKind { + Edit, + Command, + Test, +} + +/// A proposed plan consisting of ordered steps plus free‑form notes. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct Plan { + pub steps: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct PlanProposedEvent { + pub plan: Plan, +} + +/// User's decision in response to a `PlanProposed` event. +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, TS)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PlanDecision { + Accept, + Edit, + Cancel, } /// User's decision in response to an ExecApprovalRequest. @@ -912,12 +983,13 @@ mod tests { model: "codex-mini-latest".to_string(), history_log_id: 0, history_entry_count: 0, + plan_mode: false, }), }; let serialized = serde_json::to_string(&event).unwrap(); assert_eq!( serialized, - r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"codex-mini-latest","history_log_id":0,"history_entry_count":0}}"# + r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"codex-mini-latest","history_log_id":0,"history_entry_count":0,"plan_mode":false}}"# ); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 17845e4dfd78..ac3fdde5523d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1008,6 +1008,9 @@ impl ChatWidget { EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev), EventMsg::ListCustomPromptsResponse(ev) => self.on_list_custom_prompts(ev), EventMsg::ShutdownComplete => self.on_shutdown_complete(), + codex_core::protocol::EventMsg::PlanProposed(_) => { + // TODO: Render plan proposal in UI overlay (future work). + } EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff), EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => { self.on_background_event(message) @@ -1098,6 +1101,7 @@ impl ChatWidget { model: Some(model_slug.clone()), effort: Some(effort), summary: None, + plan_mode: None, })); tx.send(AppEvent::UpdateModel(model_slug.clone())); tx.send(AppEvent::UpdateReasoningEffort(effort)); @@ -1139,6 +1143,7 @@ impl ChatWidget { model: None, effort: None, summary: None, + plan_mode: None, })); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone())); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index a6e10ec6a161..c4b83c1d7108 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -249,12 +249,7 @@ pub(crate) fn new_session_info( event: SessionConfiguredEvent, is_first_event: bool, ) -> PlainHistoryCell { - let SessionConfiguredEvent { - model, - session_id: _, - history_log_id: _, - history_entry_count: _, - } = event; + let SessionConfiguredEvent { model, .. } = event; if is_first_event { let cwd_str = match relativize_to_home(&config.cwd) { Some(rel) if !rel.as_os_str().is_empty() => { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 544aec272a60..6c84fa0a332d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -126,6 +126,7 @@ pub async fn run_main( base_instructions: None, include_plan_tool: Some(true), include_apply_patch_tool: None, + plan_mode: None, include_view_image_tool: None, disable_response_storage: cli.oss.then_some(true), show_raw_agent_reasoning: cli.oss.then_some(true),