diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml deleted file mode 100644 index a0bba631cd..0000000000 --- a/.github/workflows/issue-labeler.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: Issue Labeler - -on: - issues: - types: - - opened - - labeled - -jobs: - gather-labels: - name: Generate label suggestions - # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes) - if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-label')) - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - codex_output: ${{ steps.codex.outputs.final-message }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - id: codex - uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02 # v1.7 - with: - openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }} - allow-users: "*" - prompt: | - You are an assistant that reviews GitHub issues for the repository. - - Your job is to choose the most appropriate labels for the issue described later in this prompt. - Follow these rules: - - - Add one (and only one) of the following three labels to distinguish the type of issue. Default to "bug" if unsure. - 1. bug — Reproducible defects in Codex products (CLI, VS Code extension, web, auth). - 2. enhancement — Feature requests or usability improvements that ask for new capabilities, better ergonomics, or quality-of-life tweaks. - 3. documentation — Updates or corrections needed in docs/README/config references (broken links, missing examples, outdated keys, clarification requests). - - - If applicable, add one of the following labels to specify which sub-product or product surface the issue relates to. - 1. CLI — the Codex command line interface. - 2. extension — VS Code (or other IDE) extension-specific issues. - 3. app - Issues related to the Codex desktop application. - 4. codex-web — Issues targeting the Codex web UI/Cloud experience. - 5. github-action — Issues with the Codex GitHub action. - 6. iOS — Issues with the Codex iOS app. - - - Additionally add zero or more of the following labels that are relevant to the issue content. Prefer a small set of precise labels over many broad ones. - - For agent-area issues, prefer the most specific applicable label. Use "agent" only as a fallback for agent-related issues that do not fit a more specific agent-area label. Prefer "app-server" over "session" or "config" when the issue is about app-server protocol, API, RPC, schema, launch, or bridge behavior. Use "memory" for agentic memory storage/retrieval and "performance" for high process memory utilization or memory leaks. - 1. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures). - 2. mcp — Topics involving Model Context Protocol servers/clients. - 3. mcp-server — Problems related to the codex mcp-server command, where codex runs as an MCP server. - 4. azure — Problems or requests tied to Azure OpenAI deployments. - 5. model-behavior — Undesirable LLM behavior: forgetting goals, refusing work, hallucinating environment details, quota misreports, or other reasoning/performance anomalies. - 6. code-review — Issues related to the code review feature or functionality. - 7. safety-check - Issues related to cyber risk detection or trusted access verification. - 8. auth - Problems related to authentication, login, or access tokens. - 9. exec - Problems related to the "codex exec" command or functionality. - 10. hooks - Problems related to event hooks - 11. context - Problems related to compaction, context windows, or available context reporting. - 12. skills - Problems related to skills or plugins - 13. custom-model - Problems that involve using custom model providers, local models, or OSS models. - 14. rate-limits - Problems related to token limits, rate limits, or token usage reporting. - 15. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions. - 16. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs. - 17. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues. - 18. app-server - Issues involving the app-server protocol or interfaces, including SDK/API payloads, thread/* and turn/* RPCs, app-server launch behavior, external app/controller bridges, and app-server protocol/schema behavior. - 19. connectivity - Network connectivity or endpoint issues, including reconnecting messages, stream dropped/disconnected errors, websocket/SSE/transport failures, timeout/network/VPN/proxy/API endpoint failures, and related retry behavior. - 20. subagent - Issues involving subagents, sub-agents, or multi-agent behavior, including spawn_agent, wait_agent, close_agent, worker/explorer roles, delegation, agent teams, lifecycle, model/config inheritance, quotas, and orchestration. - 21. session - Issues involving session or thread management, including resume, fork, archive, rename/title, thread history, rollout persistence, compaction, checkpoints, retention, and cross-session state. - 22. config - Issues involving config.toml, config keys, config key merging, config updates, profiles, hooks config, project config, agent role TOMLs, instruction/personality config, and config schema behavior. - 23. plan - Issues involving plan mode, planning workflows, or plan-specific tools/behavior. - 24. computer-use - Issues involving agentic computer use or SkyComputerUseService. - 25. browser - Issues involving agentic browser use, IAB, or the built-in browser within the Codex app. - 26. memory - Issues involving agentic memory storage and retrieval. - 27. imagen - Issues involving image generation. - 28. remote - Issues involving remote access, remote control, or SSH. - 29. performance - Issues involving slow, laggy performance, high memory utilization, or memory leaks. - 30. automations - Issues involving scheduled automation tasks or heartbeats. - 31. pets - Issues involving pets avatars and animations. - 32. agent - Fallback only for core agent loop or agent-related issues that do not fit app-server, connectivity, subagent, session, config, plan, computer-use, browser, memory, imagen, remote, performance, automations, or pets. - - Issue number: ${{ github.event.issue.number }} - - Issue title: - ${{ github.event.issue.title }} - - Issue body: - ${{ github.event.issue.body }} - - Repository full name: - ${{ github.repository }} - - output-schema: | - { - "type": "object", - "properties": { - "labels": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["labels"], - "additionalProperties": false - } - - apply-labels: - name: Apply labels from Codex output - needs: gather-labels - if: ${{ needs.gather-labels.result != 'skipped' }} - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - CODEX_OUTPUT: ${{ needs.gather-labels.outputs.codex_output }} - steps: - - name: Apply labels - run: | - json=${CODEX_OUTPUT//$'\r'/} - if [ -z "$json" ]; then - echo "Codex produced no output. Skipping label application." - exit 0 - fi - - if ! printf '%s' "$json" | jq -e 'type == "object" and (.labels | type == "array")' >/dev/null 2>&1; then - echo "Codex output did not include a labels array. Raw output: $json" - exit 0 - fi - - labels=$(printf '%s' "$json" | jq -r '.labels[] | tostring') - if [ -z "$labels" ]; then - echo "Codex returned an empty array. Nothing to do." - exit 0 - fi - - cmd=(gh issue edit "$ISSUE_NUMBER") - while IFS= read -r label; do - cmd+=(--add-label "$label") - done <<< "$labels" - - "${cmd[@]}" || true - - - name: Remove codex-label trigger - if: ${{ always() && github.event.action == 'labeled' && github.event.label.name == 'codex-label' }} - run: | - gh issue edit "$ISSUE_NUMBER" --remove-label codex-label || true - echo "Attempted to remove label: codex-label" diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 6dd751b7f6..9518675f76 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -10,6 +10,13 @@ on: push: tags: - "rust-v*.*.*" + workflow_dispatch: + inputs: + sign_macos: + description: "Sign and notarize macOS release artifacts." + required: false + type: boolean + default: true concurrency: group: ${{ github.workflow }} @@ -25,10 +32,17 @@ jobs: - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 - name: Validate tag matches Cargo.toml version shell: bash + env: + SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.sign_macos }} run: | set -euo pipefail echo "::group::Tag validation" + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${SIGN_MACOS}" == "true" ]]; then + echo "❌ Manual rust-release runs must set sign_macos=false" + exit 1 + fi + # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } @@ -64,6 +78,7 @@ jobs: # 2026-03-04: temporarily change releases to use thin LTO because # Ubuntu ARM is timing out at 60 minutes. CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} + SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.sign_macos }} strategy: fail-fast: false @@ -295,6 +310,39 @@ jobs: path: codex-rs/target/**/cargo-timings/cargo-timing.html if-no-files-found: warn + - if: ${{ runner.os == 'macOS' }} + name: Stage unsigned macOS artifacts + shell: bash + run: | + set -euo pipefail + + target="${{ matrix.target }}" + release_dir="target/${target}/release" + dest="unsigned-dist/${target}" + mkdir -p "$dest" + + for binary in ${{ matrix.binaries }}; do + binary_path="${release_dir}/${binary}" + unsigned_name="${binary}-${target}-unsigned" + unsigned_path="${dest}/${unsigned_name}" + if [[ ! -f "${binary_path}" ]]; then + echo "Binary ${binary_path} not found" + exit 1 + fi + + cp "${binary_path}" "${unsigned_path}" + tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" + zstd -T0 -19 --rm "${unsigned_path}" + done + + - if: ${{ runner.os == 'macOS' }} + name: Upload unsigned macOS artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ matrix.artifact_name }}-unsigned + path: codex-rs/unsigned-dist/${{ matrix.target }}/* + if-no-files-found: error + - if: ${{ contains(matrix.target, 'linux') }} name: Cosign Linux artifacts uses: ./.github/actions/linux-code-sign @@ -303,7 +351,7 @@ jobs: artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release binaries: ${{ matrix.binaries }} - - if: ${{ runner.os == 'macOS' }} + - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: @@ -317,7 +365,7 @@ jobs: apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' }} + - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} name: Build macOS dmg shell: bash run: | @@ -357,7 +405,7 @@ jobs: exit 1 fi - - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' }} + - if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} name: MacOS code signing (dmg) uses: ./.github/actions/macos-code-sign with: @@ -371,6 +419,7 @@ jobs: apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - name: Stage artifacts + if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" @@ -400,7 +449,7 @@ jobs: fi - name: Build Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} + if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} shell: bash run: | set -euo pipefail @@ -451,7 +500,7 @@ jobs: "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - name: Upload Python runtime wheel - if: ${{ matrix.bundle == 'primary' }} + if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: python-runtime-wheel-${{ matrix.target }} @@ -459,6 +508,7 @@ jobs: if-no-files-found: error - name: Compress artifacts + if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | # Path that contains the uncompressed binaries for the current @@ -495,6 +545,7 @@ jobs: done - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} with: name: ${{ matrix.artifact_name }} # Upload the per-binary .zst files, .tar.gz equivalents, and any @@ -522,6 +573,7 @@ jobs: uses: ./.github/workflows/rust-release-zsh.yml release: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.sign_macos }} needs: - build - build-windows diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index b4e5f9a04d..9178cc01cb 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -514,9 +514,16 @@ pub async fn run_main_with_transport_options( })?; codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME); codex_core::otel_init::install_sqlite_telemetry(otel.as_ref(), OTEL_SERVICE_NAME); - let state_db_result = rollout_state_db::try_init(&config).await; - let state_db_init_error = state_db_result.as_ref().err().map(ToString::to_string); - let state_db = state_db_result.ok(); + let state_db = match rollout_state_db::try_init(&config).await { + Ok(state_db) => Some(state_db), + Err(err) => { + let state_db_path = codex_state::state_db_path(config.sqlite_home.as_path()); + return Err(std::io::Error::other(format!( + "failed to initialize sqlite state db at {}: {err}", + state_db_path.display() + ))); + } + }; if should_run_personality_migration { let effective_toml = config.config_layer_stack.effective_config(); @@ -633,10 +640,6 @@ pub async fn run_main_with_transport_options( } } let installation_id = resolve_installation_id(&config.codex_home).await?; - if let Some(err) = &state_db_init_error { - error!("failed to initialize sqlite state db: {err}"); - } - let transport_shutdown_token = CancellationToken::new(); let mut transport_accept_handles = Vec::>::new(); diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 0b2200a9d5..6fccdca9d7 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -1823,6 +1823,18 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { /*enabled*/ None, ))?; shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null; + let shared_unlisted_body: serde_json::Value = + serde_json::from_str(&workspace_remote_plugin_page_body( + "plugins~Plugin_44444444444444444444444444444444", + "shared-unlisted-linear", + "Shared Unlisted Linear", + "UNLISTED", + /*enabled*/ None, + ))?; + shared_plugin_body["plugins"] + .as_array_mut() + .expect("shared plugins should be an array") + .push(shared_unlisted_body["plugins"][0].clone()); let shared_plugin_body = serde_json::to_string(&shared_plugin_body)?; let mut workspace_installed_body: serde_json::Value = serde_json::from_str(&workspace_remote_plugin_page_body( @@ -1878,10 +1890,10 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { .and_then(|interface| interface.display_name.as_deref()), Some("Shared with me") ); - assert_eq!(marketplace.plugins.len(), 1); + assert_eq!(marketplace.plugins.len(), 2); assert_eq!( marketplace.plugins[0].id, - "shared-linear@workspace-shared-with-me-private" + "shared-linear@workspace-shared-with-me" ); assert_eq!( marketplace.plugins[0].remote_plugin_id.as_deref(), @@ -1913,6 +1925,29 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { Some("https://chatgpt.example/plugins/share/share-key-1") ); assert_eq!(share_context.share_principals, None); + assert_eq!( + marketplace.plugins[1].id, + "shared-unlisted-linear@workspace-shared-with-me" + ); + assert_eq!( + marketplace.plugins[1].remote_plugin_id.as_deref(), + Some("plugins~Plugin_44444444444444444444444444444444") + ); + assert_eq!(marketplace.plugins[1].name, "shared-unlisted-linear"); + assert_eq!(marketplace.plugins[1].installed, false); + assert_eq!(marketplace.plugins[1].enabled, false); + let share_context = marketplace.plugins[1] + .share_context + .as_ref() + .expect("expected share context"); + assert_eq!( + share_context.remote_plugin_id, + "plugins~Plugin_44444444444444444444444444444444" + ); + assert_eq!( + share_context.discoverability, + Some(PluginShareDiscoverability::Unlisted) + ); let marketplace = response .marketplaces @@ -1929,7 +1964,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { assert_eq!(marketplace.plugins.len(), 1); assert_eq!( marketplace.plugins[0].id, - "unlisted-linear@workspace-shared-with-me-unlisted" + "unlisted-linear@workspace-shared-with-me" ); assert_eq!( marketplace.plugins[0].remote_plugin_id.as_deref(), diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 525a4234aa..812f884a55 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -300,74 +300,76 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result< let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - let request_id = mcp - .send_plugin_read_request(PluginReadParams { - marketplace_path: None, - remote_marketplace_name: Some("workspace-shared-with-me-private".to_string()), - plugin_name: "plugins~Plugin_11111111111111111111111111111111".to_string(), - }) - .await?; + for remote_marketplace_name in [ + "workspace-shared-with-me-private", + "workspace-shared-with-me", + ] { + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: None, + remote_marketplace_name: Some(remote_marketplace_name.to_string()), + plugin_name: "plugins~Plugin_11111111111111111111111111111111".to_string(), + }) + .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginReadResponse = to_response(response)?; + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginReadResponse = to_response(response)?; - assert_eq!( - response.plugin.marketplace_name, - "workspace-shared-with-me-private" - ); - assert_eq!( - response.plugin.summary.id, - "shared-linear@workspace-shared-with-me-private" - ); - assert_eq!( - response.plugin.summary.remote_plugin_id.as_deref(), - Some("plugins~Plugin_11111111111111111111111111111111") - ); - let share_context = response - .plugin - .summary - .share_context - .as_ref() - .expect("expected share context"); - assert_eq!( - share_context.remote_plugin_id, - "plugins~Plugin_11111111111111111111111111111111" - ); - assert_eq!(share_context.remote_version.as_deref(), Some("2.3.4")); - assert_eq!( - share_context.discoverability, - Some(PluginShareDiscoverability::Private) - ); - assert_eq!( - share_context.creator_account_user_id.as_deref(), - Some("user-gavin__account-123") - ); - assert_eq!(share_context.creator_name.as_deref(), Some("Gavin")); - assert_eq!( - share_context.share_url.as_deref(), - Some("https://chatgpt.example/plugins/share/share-key-1") - ); - assert_eq!( - share_context.share_principals, - Some(vec![ - PluginSharePrincipal { - principal_type: PluginSharePrincipalType::User, - principal_id: "user-gavin__account-123".to_string(), - role: PluginSharePrincipalRole::Owner, - name: "Gavin".to_string(), - }, - PluginSharePrincipal { - principal_type: PluginSharePrincipalType::User, - principal_id: "user-ada__account-123".to_string(), - role: PluginSharePrincipalRole::Reader, - name: "Ada".to_string(), - }, - ]) - ); + assert_eq!(response.plugin.marketplace_name, "workspace-shared-with-me"); + assert_eq!( + response.plugin.summary.id, + "shared-linear@workspace-shared-with-me" + ); + assert_eq!( + response.plugin.summary.remote_plugin_id.as_deref(), + Some("plugins~Plugin_11111111111111111111111111111111") + ); + let share_context = response + .plugin + .summary + .share_context + .as_ref() + .expect("expected share context"); + assert_eq!( + share_context.remote_plugin_id, + "plugins~Plugin_11111111111111111111111111111111" + ); + assert_eq!(share_context.remote_version.as_deref(), Some("2.3.4")); + assert_eq!( + share_context.discoverability, + Some(PluginShareDiscoverability::Private) + ); + assert_eq!( + share_context.creator_account_user_id.as_deref(), + Some("user-gavin__account-123") + ); + assert_eq!(share_context.creator_name.as_deref(), Some("Gavin")); + assert_eq!( + share_context.share_url.as_deref(), + Some("https://chatgpt.example/plugins/share/share-key-1") + ); + assert_eq!( + share_context.share_principals, + Some(vec![ + PluginSharePrincipal { + principal_type: PluginSharePrincipalType::User, + principal_id: "user-gavin__account-123".to_string(), + role: PluginSharePrincipalRole::Owner, + name: "Gavin".to_string(), + }, + PluginSharePrincipal { + principal_type: PluginSharePrincipalType::User, + principal_id: "user-ada__account-123".to_string(), + role: PluginSharePrincipalRole::Reader, + name: "Ada".to_string(), + }, + ]) + ); + } Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs index efe3d291a4..43f652c1e0 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -169,7 +169,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "demo-plugin@workspace-shared-with-me-private".to_string(), + id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), local_version: None, name: "demo-plugin".to_string(), @@ -573,7 +573,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "demo-plugin@workspace-shared-with-me-private".to_string(), + id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), local_version: None, name: "demo-plugin".to_string(), @@ -1123,7 +1123,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "demo-plugin@workspace-shared-with-me-private".to_string(), + id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), local_version: None, name: "demo-plugin".to_string(), diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 5d04dc2a36..976bd4c768 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -596,8 +596,8 @@ fn codex_apps_tools_cache_filters_disallowed_connectors() { create_test_tool_with_connector( CODEX_APPS_MCP_SERVER_NAME, "blocked_tool", - "connector_openai_hidden", - Some("Hidden"), + "connector_2b0a9009c9c64bf9933a3dae3f2b1254", + Some("Blocked"), ), create_test_tool_with_connector( CODEX_APPS_MCP_SERVER_NAME, diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index b627a83726..591b259676 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -443,7 +443,6 @@ pub struct ConfigToml { pub experimental_instructions_file: Option, pub experimental_compact_prompt_file: Option, pub experimental_use_unified_exec_tool: Option, - pub experimental_use_freeform_apply_patch: Option, /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama". pub oss_provider: Option, } diff --git a/codex-rs/config/src/profile_toml.rs b/codex-rs/config/src/profile_toml.rs index ed036d1b4e..e8e6320490 100644 --- a/codex-rs/config/src/profile_toml.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -54,13 +54,11 @@ pub struct ConfigProfile { #[schemars(skip)] pub experimental_instructions_file: Option, pub experimental_compact_prompt_file: Option, - pub include_apply_patch_tool: Option, pub include_permissions_instructions: Option, pub include_apps_instructions: Option, pub include_collaboration_mode_instructions: Option, pub include_environment_context: Option, pub experimental_use_unified_exec_tool: Option, - pub experimental_use_freeform_apply_patch: Option, pub tools: Option, pub web_search: Option, pub analytics: Option, diff --git a/codex-rs/connectors/src/filter.rs b/codex-rs/connectors/src/filter.rs index 82c334f82d..e26291794f 100644 --- a/codex-rs/connectors/src/filter.rs +++ b/codex-rs/connectors/src/filter.rs @@ -37,7 +37,6 @@ const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ ]; const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] = &["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"]; -const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; pub fn filter_disallowed_connectors( connectors: Vec, @@ -63,6 +62,166 @@ fn is_connector_id_allowed(connector_id: &str, first_party_chat_originator: bool DISALLOWED_CONNECTOR_IDS }; - !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX) - && !disallowed_connector_ids.contains(&connector_id) + !disallowed_connector_ids.contains(&connector_id) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metadata::connector_install_url; + use pretty_assertions::assert_eq; + + fn app(id: &str) -> AppInfo { + AppInfo { + id: id.to_string(), + name: id.to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + install_url: None, + branding: None, + app_metadata: None, + labels: None, + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + } + } + + fn named_app(id: &str, name: &str) -> AppInfo { + AppInfo { + id: id.to_string(), + name: name.to_string(), + install_url: Some(connector_install_url(name, id)), + ..app(id) + } + } + + #[test] + fn filter_disallowed_connectors_allows_non_disallowed_connectors() { + let filtered = + filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")], "codex_cli"); + assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]); + } + + #[test] + fn filter_disallowed_connectors_allows_openai_prefix() { + let filtered = filter_disallowed_connectors( + vec![ + app("connector_openai_foo"), + app("connector_openai_bar"), + app("gamma"), + ], + "codex_cli", + ); + assert_eq!( + filtered, + vec![ + app("connector_openai_foo"), + app("connector_openai_bar"), + app("gamma") + ] + ); + } + + #[test] + fn filter_disallowed_connectors_filters_disallowed_connector_ids() { + let filtered = filter_disallowed_connectors( + vec![ + app("asdk_app_6938a94a61d881918ef32cb999ff937c"), + app("connector_3f8d1a79f27c4c7ba1a897ab13bf37dc"), + app("delta"), + ], + "codex_cli", + ); + assert_eq!(filtered, vec![app("delta")]); + } + + #[test] + fn first_party_chat_originator_filters_target_connector_ids() { + let filtered = filter_disallowed_connectors( + vec![ + app("connector_openai_foo"), + app("asdk_app_6938a94a61d881918ef32cb999ff937c"), + app("connector_0f9c9d4592e54d0a9a12b3f44a1e2010"), + ], + "codex_atlas", + ); + assert_eq!( + filtered, + vec![ + app("connector_openai_foo"), + app("asdk_app_6938a94a61d881918ef32cb999ff937c") + ] + ); + } + + #[test] + fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() { + let filtered = filter_tool_suggest_discoverable_connectors( + vec![ + named_app( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + ), + named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"), + named_app("connector_other", "Other"), + ], + &[AppInfo { + is_accessible: true, + ..named_app( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + ) + }], + &HashSet::from([ + "connector_2128aebfecb84f64a069897515042a44".to_string(), + "connector_68df038e0ba48191908c8434991bbac2".to_string(), + ]), + "codex_cli", + ); + + assert_eq!( + filtered, + vec![named_app( + "connector_68df038e0ba48191908c8434991bbac2", + "Gmail", + )] + ); + } + + #[test] + fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled() { + let filtered = filter_tool_suggest_discoverable_connectors( + vec![ + named_app( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + ), + named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"), + ], + &[ + AppInfo { + is_accessible: true, + ..named_app( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + ) + }, + AppInfo { + is_accessible: true, + is_enabled: false, + ..named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail") + }, + ], + &HashSet::from([ + "connector_2128aebfecb84f64a069897515042a44".to_string(), + "connector_68df038e0ba48191908c8434991bbac2".to_string(), + ]), + "codex_cli", + ); + + assert_eq!(filtered, Vec::::new()); + } } diff --git a/codex-rs/connectors/src/merge.rs b/codex-rs/connectors/src/merge.rs index b41ee63add..a8bee18737 100644 --- a/codex-rs/connectors/src/merge.rs +++ b/codex-rs/connectors/src/merge.rs @@ -117,3 +117,90 @@ pub fn plugin_connector_to_app_info(connector_id: String) -> AppInfo { plugin_display_names: Vec::new(), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::metadata::connector_install_url; + use crate::metadata::connector_mention_slug; + use pretty_assertions::assert_eq; + + fn plugin_names(names: &[&str]) -> Vec { + names.iter().map(ToString::to_string).collect() + } + + fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInfo { + AppInfo { + id: "calendar".to_string(), + name: "Google Calendar".to_string(), + description: Some("Plan events".to_string()), + logo_url: Some("https://example.com/logo.png".to_string()), + logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + distribution_channel: Some("workspace".to_string()), + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible: true, + is_enabled: true, + plugin_display_names: plugin_names(plugin_display_names), + } + } + + #[test] + fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() { + let plugin = plugin_connector_to_app_info("calendar".to_string()); + let accessible = google_calendar_accessible_connector(&[]); + + let merged = merge_connectors(vec![plugin], vec![accessible]); + + assert_eq!( + merged, + vec![AppInfo { + id: "calendar".to_string(), + name: "Google Calendar".to_string(), + description: Some("Plan events".to_string()), + logo_url: Some("https://example.com/logo.png".to_string()), + logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + distribution_channel: Some("workspace".to_string()), + branding: None, + app_metadata: None, + labels: None, + install_url: Some(connector_install_url("calendar", "calendar")), + is_accessible: true, + is_enabled: true, + plugin_display_names: Vec::new(), + }] + ); + assert_eq!(connector_mention_slug(&merged[0]), "google-calendar"); + } + + #[test] + fn merge_connectors_unions_and_dedupes_plugin_display_names() { + let mut plugin = plugin_connector_to_app_info("calendar".to_string()); + plugin.plugin_display_names = plugin_names(&["sample", "alpha", "sample"]); + + let accessible = google_calendar_accessible_connector(&["beta", "alpha"]); + + let merged = merge_connectors(vec![plugin], vec![accessible]); + + assert_eq!( + merged, + vec![AppInfo { + id: "calendar".to_string(), + name: "Google Calendar".to_string(), + description: Some("Plan events".to_string()), + logo_url: Some("https://example.com/logo.png".to_string()), + logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + distribution_channel: Some("workspace".to_string()), + branding: None, + app_metadata: None, + labels: None, + install_url: Some(connector_install_url("calendar", "calendar")), + is_accessible: true, + is_enabled: true, + plugin_display_names: plugin_names(&["alpha", "beta", "sample"]), + }] + ); + } +} diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index fc6fedbe23..4dde224039 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -48,6 +48,7 @@ pub use share::update_remote_plugin_share_targets; pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global"; pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "workspace-directory"; +pub const REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME: &str = "workspace-shared-with-me"; pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME: &str = "workspace-shared-with-me-private"; pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME: &str = @@ -296,6 +297,7 @@ impl RemotePluginScope { match name { REMOTE_GLOBAL_MARKETPLACE_NAME => Some(Self::Global), REMOTE_WORKSPACE_MARKETPLACE_NAME + | REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME | REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME | REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME => Some(Self::Workspace), _ => None, @@ -397,11 +399,9 @@ fn remote_plugin_canonical_marketplace_name( RemotePluginScope::Global => Ok(REMOTE_GLOBAL_MARKETPLACE_NAME), RemotePluginScope::Workspace => match workspace_plugin_discoverability(plugin)? { RemotePluginShareDiscoverability::Listed => Ok(REMOTE_WORKSPACE_MARKETPLACE_NAME), - RemotePluginShareDiscoverability::Unlisted => { - Ok(REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME) - } - RemotePluginShareDiscoverability::Private => { - Ok(REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME) + RemotePluginShareDiscoverability::Private + | RemotePluginShareDiscoverability::Unlisted => { + Ok(REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME) } }, } @@ -505,20 +505,29 @@ pub async fn fetch_remote_marketplaces( } } RemoteMarketplaceSource::SharedWithMe => { - let private_plugins = fetch_shared_workspace_plugins(config, auth) - .await? + // The shared endpoint is the source of truth for plugins explicitly shared + // with the user. Installed unlisted plugins that are not returned there are + // link-installed and stay in the separate unlisted bucket. + let shared_plugins = fetch_shared_workspace_plugins(config, auth).await?; + let shared_plugin_ids = shared_plugins + .iter() + .map(|plugin| plugin.id.clone()) + .collect::>(); + let directly_shared_plugins = shared_plugins .into_iter() .filter_map(|plugin| match workspace_plugin_discoverability(&plugin) { - Ok(RemotePluginShareDiscoverability::Private) => Some(Ok(plugin)), - Ok(RemotePluginShareDiscoverability::Listed) - | Ok(RemotePluginShareDiscoverability::Unlisted) => None, + Ok( + RemotePluginShareDiscoverability::Private + | RemotePluginShareDiscoverability::Unlisted, + ) => Some(Ok(plugin)), + Ok(RemotePluginShareDiscoverability::Listed) => None, Err(err) => Some(Err(err)), }) .collect::, _>>()?; if let Some(marketplace) = build_remote_marketplace( REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME, REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME, - private_plugins, + directly_shared_plugins, workspace_installed_plugins.clone().unwrap_or_default(), /*include_installed_only*/ false, )? { @@ -531,7 +540,12 @@ pub async fn fetch_remote_marketplaces( .into_iter() .filter_map( |plugin| match workspace_plugin_discoverability(&plugin.plugin) { - Ok(RemotePluginShareDiscoverability::Unlisted) => Some(Ok(plugin)), + Ok(RemotePluginShareDiscoverability::Unlisted) + if !shared_plugin_ids.contains(&plugin.plugin.id) => + { + Some(Ok(plugin)) + } + Ok(RemotePluginShareDiscoverability::Unlisted) => None, Ok(RemotePluginShareDiscoverability::Listed) | Ok(RemotePluginShareDiscoverability::Private) => None, Err(err) => Some(Err(err)), diff --git a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs index c94eba428e..b13ca7a630 100644 --- a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs +++ b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs @@ -1,5 +1,6 @@ use super::REMOTE_GLOBAL_MARKETPLACE_NAME; use super::REMOTE_WORKSPACE_MARKETPLACE_NAME; +use super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME; use super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME; use super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME; use super::RemotePluginCatalogError; @@ -153,6 +154,10 @@ pub async fn sync_remote_installed_plugin_bundles_once( REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), ), + ( + REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(), + BTreeSet::new(), + ), ( REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), @@ -303,6 +308,7 @@ fn remove_stale_remote_plugin_caches( for marketplace_name in [ REMOTE_GLOBAL_MARKETPLACE_NAME, REMOTE_WORKSPACE_MARKETPLACE_NAME, + REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME, REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME, REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME, ] { @@ -510,7 +516,7 @@ mod tests { } #[test] - fn stale_remote_plugin_cleanup_removes_private_shared_with_me_cache() { + fn stale_remote_plugin_cleanup_removes_old_shared_with_me_cache_and_keeps_canonical_cache() { let codex_home = tempfile::tempdir().expect("create codex home"); let cached_manifest = codex_home .path() @@ -524,6 +530,18 @@ mod tests { .expect("create cached plugin manifest parent"); std::fs::write(&cached_manifest, r#"{"name":"private-plugin"}"#) .expect("write cached plugin manifest"); + let canonical_cached_manifest = codex_home + .path() + .join(PLUGINS_CACHE_DIR) + .join(REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME) + .join("shared-plugin") + .join("1.2.3") + .join(".codex-plugin") + .join("plugin.json"); + std::fs::create_dir_all(canonical_cached_manifest.parent().expect("manifest parent")) + .expect("create canonical cached plugin manifest parent"); + std::fs::write(&canonical_cached_manifest, r#"{"name":"shared-plugin"}"#) + .expect("write canonical cached plugin manifest"); let installed_plugin_names_by_marketplace = BTreeMap::>::from_iter([ (REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()), @@ -531,6 +549,10 @@ mod tests { REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), ), + ( + REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(), + BTreeSet::from(["shared-plugin".to_string()]), + ), ( REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), @@ -552,5 +574,6 @@ mod tests { vec!["private-plugin@workspace-shared-with-me-private".to_string()] ); assert!(!cached_manifest.exists()); + assert!(canonical_cached_manifest.is_file()); } } diff --git a/codex-rs/core-plugins/src/remote/share/checkout.rs b/codex-rs/core-plugins/src/remote/share/checkout.rs index 9ccdbee851..8aabccff8b 100644 --- a/codex-rs/core-plugins/src/remote/share/checkout.rs +++ b/codex-rs/core-plugins/src/remote/share/checkout.rs @@ -1,3 +1,4 @@ +use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME; use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME; use super::super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME; use super::super::RemotePluginCatalogError; @@ -162,7 +163,8 @@ pub async fn checkout_remote_plugin_share( fn is_checkout_supported_share_marketplace(marketplace_name: &str) -> bool { matches!( marketplace_name, - REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME + REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME + | REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME | REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME ) } diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index 0b8f8f2fb5..f62051374e 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -586,7 +586,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { vec![ RemotePluginShareSummary { summary: RemotePluginSummary { - id: "demo-plugin@workspace-shared-with-me-private".to_string(), + id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: "plugins_123".to_string(), name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { @@ -625,7 +625,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { }, RemotePluginShareSummary { summary: RemotePluginSummary { - id: "demo-plugin@workspace-shared-with-me-private".to_string(), + id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: "plugins_456".to_string(), name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 560e87b058..d8c64a0d13 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -347,9 +347,6 @@ "experimental_compact_prompt_file": { "$ref": "#/definitions/AbsolutePathBuf" }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, "experimental_use_unified_exec_tool": { "type": "boolean" }, @@ -430,9 +427,6 @@ "exec_permission_approvals": { "type": "boolean" }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, "experimental_use_unified_exec_tool": { "type": "boolean" }, @@ -619,9 +613,6 @@ }, "type": "object" }, - "include_apply_patch_tool": { - "type": "boolean" - }, "include_apps_instructions": { "type": "boolean" }, @@ -4090,9 +4081,6 @@ ], "description": "Experimental / do not use. Selects the thread store implementation." }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, "experimental_use_unified_exec_tool": { "type": "boolean" }, @@ -4173,9 +4161,6 @@ "exec_permission_approvals": { "type": "boolean" }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, "experimental_use_unified_exec_tool": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 5f060414ed..e0be0bda68 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4237,7 +4237,6 @@ async fn legacy_toggles_map_to_features() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { experimental_use_unified_exec_tool: Some(true), - experimental_use_freeform_apply_patch: Some(true), ..Default::default() }; @@ -4248,11 +4247,8 @@ async fn legacy_toggles_map_to_features() -> std::io::Result<()> { ) .await?; - assert!(config.features.enabled(Feature::ApplyPatchFreeform)); assert!(config.features.enabled(Feature::UnifiedExec)); - assert!(config.include_apply_patch_tool); - assert!(config.use_experimental_unified_exec_tool); Ok(()) diff --git a/codex-rs/core/src/config/managed_features.rs b/codex-rs/core/src/config/managed_features.rs index a575b0a45b..78b88d2f3a 100644 --- a/codex-rs/core/src/config/managed_features.rs +++ b/codex-rs/core/src/config/managed_features.rs @@ -269,13 +269,6 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature enabled, )); } - if let Some(enabled) = cfg.experimental_use_freeform_apply_patch { - explicit_settings.push(( - "experimental_use_freeform_apply_patch".to_string(), - Feature::ApplyPatchFreeform, - enabled, - )); - } for (profile_name, profile) in &cfg.profiles { if let Some(features) = profile.features.as_ref() { for (key, enabled) in features.entries() { @@ -288,13 +281,6 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature } } } - if let Some(enabled) = profile.include_apply_patch_tool { - explicit_settings.push(( - format!("profiles.{profile_name}.include_apply_patch_tool"), - Feature::ApplyPatchFreeform, - enabled, - )); - } if let Some(enabled) = profile.experimental_use_unified_exec_tool { explicit_settings.push(( format!("profiles.{profile_name}.experimental_use_unified_exec_tool"), @@ -302,13 +288,6 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature enabled, )); } - if let Some(enabled) = profile.experimental_use_freeform_apply_patch { - explicit_settings.push(( - format!("profiles.{profile_name}.experimental_use_freeform_apply_patch"), - Feature::ApplyPatchFreeform, - enabled, - )); - } } explicit_settings @@ -370,14 +349,11 @@ pub(crate) fn validate_feature_requirements_in_config_toml( FeatureConfigSource { features: cfg.features.as_ref(), include_apply_patch_tool: None, - experimental_use_freeform_apply_patch: cfg.experimental_use_freeform_apply_patch, experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool, }, FeatureConfigSource { features: profile.features.as_ref(), - include_apply_patch_tool: profile.include_apply_patch_tool, - experimental_use_freeform_apply_patch: profile - .experimental_use_freeform_apply_patch, + include_apply_patch_tool: None, experimental_use_unified_exec_tool: profile.experimental_use_unified_exec_tool, }, FeatureOverrides::default(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 3c50880590..2046d50a9d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1881,7 +1881,6 @@ pub struct ConfigOverrides { pub developer_instructions: Option, pub personality: Option, pub compact_prompt: Option, - pub include_apply_patch_tool: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, pub ephemeral: Option, @@ -2196,7 +2195,6 @@ impl Config { developer_instructions, personality, compact_prompt, - include_apply_patch_tool: include_apply_patch_tool_override, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, ephemeral, @@ -2250,7 +2248,6 @@ impl Config { }; let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack); let feature_overrides = FeatureOverrides { - include_apply_patch_tool: include_apply_patch_tool_override, web_search_request: override_tools_web_search_request, }; @@ -2258,14 +2255,11 @@ impl Config { FeatureConfigSource { features: cfg.features.as_ref(), include_apply_patch_tool: None, - experimental_use_freeform_apply_patch: cfg.experimental_use_freeform_apply_patch, experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool, }, FeatureConfigSource { features: config_profile.features.as_ref(), - include_apply_patch_tool: config_profile.include_apply_patch_tool, - experimental_use_freeform_apply_patch: config_profile - .experimental_use_freeform_apply_patch, + include_apply_patch_tool: None, experimental_use_unified_exec_tool: config_profile .experimental_use_unified_exec_tool, }, diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index c5798d7c14..cce2491679 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -13,12 +13,8 @@ use codex_config::types::AppConfig; use codex_config::types::AppToolConfig; use codex_config::types::AppToolsConfig; use codex_config::types::AppsDefaultConfig; -use codex_connectors::filter::filter_disallowed_connectors; -use codex_connectors::filter::filter_tool_suggest_discoverable_connectors; -use codex_connectors::merge::merge_connectors; use codex_connectors::merge::plugin_connector_to_app_info; use codex_connectors::metadata::connector_install_url; -use codex_connectors::metadata::connector_mention_slug; use codex_connectors::metadata::sanitize_name; use codex_features::Feature; use codex_login::CodexAuth; @@ -62,15 +58,6 @@ fn app(id: &str) -> AppInfo { } } -fn named_app(id: &str, name: &str) -> AppInfo { - AppInfo { - id: id.to_string(), - name: name.to_string(), - install_url: Some(connector_install_url(name, id)), - ..app(id) - } -} - fn plugin_names(names: &[&str]) -> Vec { names.iter().map(ToString::to_string).collect() } @@ -89,24 +76,6 @@ fn test_tool_definition(tool_name: &str) -> Tool { } } -fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInfo { - AppInfo { - id: "calendar".to_string(), - name: "Google Calendar".to_string(), - description: Some("Plan events".to_string()), - logo_url: Some("https://example.com/logo.png".to_string()), - logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), - distribution_channel: Some("workspace".to_string()), - branding: None, - app_metadata: None, - labels: None, - install_url: None, - is_accessible: true, - is_enabled: true, - plugin_display_names: plugin_names(plugin_display_names), - } -} - fn codex_app_tool( tool_name: &str, connector_id: &str, @@ -147,34 +116,6 @@ fn with_accessible_connectors_cache_cleared(f: impl FnOnce() -> R) -> R { result } -#[test] -fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() { - let plugin = plugin_connector_to_app_info("calendar".to_string()); - let accessible = google_calendar_accessible_connector(&[]); - - let merged = merge_connectors(vec![plugin], vec![accessible]); - - assert_eq!( - merged, - vec![AppInfo { - id: "calendar".to_string(), - name: "Google Calendar".to_string(), - description: Some("Plan events".to_string()), - logo_url: Some("https://example.com/logo.png".to_string()), - logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), - distribution_channel: Some("workspace".to_string()), - branding: None, - app_metadata: None, - labels: None, - install_url: Some(connector_install_url("calendar", "calendar")), - is_accessible: true, - is_enabled: true, - plugin_display_names: Vec::new(), - }] - ); - assert_eq!(connector_mention_slug(&merged[0]), "google-calendar"); -} - #[test] fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() { let tools = vec![ @@ -258,50 +199,38 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal assert_eq!( cached, - vec![AppInfo { - id: "calendar".to_string(), - name: "Google Calendar".to_string(), - description: None, - logo_url: None, - logo_url_dark: None, - distribution_channel: None, - install_url: Some(connector_install_url("Google Calendar", "calendar")), - branding: None, - app_metadata: None, - labels: None, - is_accessible: true, - is_enabled: true, - plugin_display_names: plugin_names(&["calendar-plugin"]), - }] - ); -} - -#[test] -fn merge_connectors_unions_and_dedupes_plugin_display_names() { - let mut plugin = plugin_connector_to_app_info("calendar".to_string()); - plugin.plugin_display_names = plugin_names(&["sample", "alpha", "sample"]); - - let accessible = google_calendar_accessible_connector(&["beta", "alpha"]); - - let merged = merge_connectors(vec![plugin], vec![accessible]); - - assert_eq!( - merged, - vec![AppInfo { - id: "calendar".to_string(), - name: "Google Calendar".to_string(), - description: Some("Plan events".to_string()), - logo_url: Some("https://example.com/logo.png".to_string()), - logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), - distribution_channel: Some("workspace".to_string()), - branding: None, - app_metadata: None, - labels: None, - install_url: Some(connector_install_url("calendar", "calendar")), - is_accessible: true, - is_enabled: true, - plugin_display_names: plugin_names(&["alpha", "beta", "sample"]), - }] + vec![ + AppInfo { + id: "calendar".to_string(), + name: "Google Calendar".to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + install_url: Some(connector_install_url("Google Calendar", "calendar")), + branding: None, + app_metadata: None, + labels: None, + is_accessible: true, + is_enabled: true, + plugin_display_names: plugin_names(&["calendar-plugin"]), + }, + AppInfo { + id: "connector_openai_hidden".to_string(), + name: "Hidden".to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + install_url: Some(connector_install_url("Hidden", "connector_openai_hidden")), + branding: None, + app_metadata: None, + labels: None, + is_accessible: true, + is_enabled: true, + plugin_display_names: Vec::new(), + } + ] ); } @@ -1245,55 +1174,6 @@ fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { ); } -#[test] -fn filter_disallowed_connectors_allows_non_disallowed_connectors() { - let filtered = - filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")], "codex_cli"); - assert_eq!(filtered, vec![app("asdk_app_hidden"), app("alpha")]); -} - -#[test] -fn filter_disallowed_connectors_filters_openai_prefix() { - let filtered = filter_disallowed_connectors( - vec![ - app("connector_openai_foo"), - app("connector_openai_bar"), - app("gamma"), - ], - "codex_cli", - ); - assert_eq!(filtered, vec![app("gamma")]); -} - -#[test] -fn filter_disallowed_connectors_filters_disallowed_connector_ids() { - let filtered = filter_disallowed_connectors( - vec![ - app("asdk_app_6938a94a61d881918ef32cb999ff937c"), - app("connector_3f8d1a79f27c4c7ba1a897ab13bf37dc"), - app("delta"), - ], - "codex_cli", - ); - assert_eq!(filtered, vec![app("delta")]); -} - -#[test] -fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() { - let filtered = filter_disallowed_connectors( - vec![ - app("connector_openai_foo"), - app("asdk_app_6938a94a61d881918ef32cb999ff937c"), - app("connector_0f9c9d4592e54d0a9a12b3f44a1e2010"), - ], - "codex_atlas", - ); - assert_eq!( - filtered, - vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c")] - ); -} - #[tokio::test] async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() { let codex_home = tempdir().expect("tempdir should succeed"); @@ -1385,71 +1265,3 @@ discoverables = [ ))] ); } - -#[test] -fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() { - let filtered = filter_tool_suggest_discoverable_connectors( - vec![ - named_app( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - ), - named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"), - named_app("connector_other", "Other"), - ], - &[AppInfo { - is_accessible: true, - ..named_app( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - ) - }], - &HashSet::from([ - "connector_2128aebfecb84f64a069897515042a44".to_string(), - "connector_68df038e0ba48191908c8434991bbac2".to_string(), - ]), - "codex_cli", - ); - - assert_eq!( - filtered, - vec![named_app( - "connector_68df038e0ba48191908c8434991bbac2", - "Gmail", - )] - ); -} - -#[test] -fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled() { - let filtered = filter_tool_suggest_discoverable_connectors( - vec![ - named_app( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - ), - named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"), - ], - &[ - AppInfo { - is_accessible: true, - ..named_app( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - ) - }, - AppInfo { - is_accessible: true, - is_enabled: false, - ..named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail") - }, - ], - &HashSet::from([ - "connector_2128aebfecb84f64a069897515042a44".to_string(), - "connector_68df038e0ba48191908c8434991bbac2".to_string(), - ]), - "codex_cli", - ); - - assert_eq!(filtered, Vec::::new()); -} diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index 99137f4db0..1e632ba0cf 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -194,7 +194,6 @@ fn drop_lockfile_inputs(lock_config: &mut ConfigToml) { lock_config.default_permissions = None; lock_config.permissions = None; lock_config.experimental_use_unified_exec_tool = None; - lock_config.experimental_use_freeform_apply_patch = None; } fn resolved_config_to_toml( diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 7825d163c9..23f87685c6 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -423,7 +423,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result developer_instructions: None, personality: None, compact_prompt: None, - include_apply_patch_tool: None, show_raw_agent_reasoning: oss.then_some(true), tools_web_search_request: None, ephemeral: ephemeral.then_some(true), diff --git a/codex-rs/features/src/legacy.rs b/codex-rs/features/src/legacy.rs index e87f14f590..c3dcb089a5 100644 --- a/codex-rs/features/src/legacy.rs +++ b/codex-rs/features/src/legacy.rs @@ -21,10 +21,6 @@ const ALIASES: &[Alias] = &[ legacy_key: "experimental_use_unified_exec_tool", feature: Feature::UnifiedExec, }, - Alias { - legacy_key: "experimental_use_freeform_apply_patch", - feature: Feature::ApplyPatchFreeform, - }, Alias { legacy_key: "include_apply_patch_tool", feature: Feature::ApplyPatchFreeform, @@ -72,7 +68,6 @@ pub(crate) fn feature_for_key(key: &str) -> Option { #[derive(Debug, Default)] pub(crate) struct LegacyFeatureToggles { pub include_apply_patch_tool: Option, - pub experimental_use_freeform_apply_patch: Option, pub experimental_use_unified_exec_tool: Option, } @@ -84,12 +79,6 @@ impl LegacyFeatureToggles { self.include_apply_patch_tool, "include_apply_patch_tool", ); - set_if_some( - features, - Feature::ApplyPatchFreeform, - self.experimental_use_freeform_apply_patch, - "experimental_use_freeform_apply_patch", - ); set_if_some( features, Feature::UnifiedExec, diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index d8a0a598e7..6756ce7af0 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -286,7 +286,6 @@ pub struct Features { #[derive(Debug, Clone, Default)] pub struct FeatureOverrides { - pub include_apply_patch_tool: Option, pub web_search_request: Option, } @@ -294,17 +293,11 @@ pub struct FeatureOverrides { pub struct FeatureConfigSource<'a> { pub features: Option<&'a FeaturesToml>, pub include_apply_patch_tool: Option, - pub experimental_use_freeform_apply_patch: Option, pub experimental_use_unified_exec_tool: Option, } impl FeatureOverrides { fn apply(self, features: &mut Features) { - LegacyFeatureToggles { - include_apply_patch_tool: self.include_apply_patch_tool, - ..Default::default() - } - .apply(features); if let Some(enabled) = self.web_search_request { if enabled { features.enable(Feature::WebSearchRequest); @@ -470,7 +463,6 @@ impl Features { for source in [base, profile] { LegacyFeatureToggles { include_apply_patch_tool: source.include_apply_patch_tool, - experimental_use_freeform_apply_patch: source.experimental_use_freeform_apply_patch, experimental_use_unified_exec_tool: source.experimental_use_unified_exec_tool, } .apply(&mut features); diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index e7aab9cec9..567baed2c2 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -393,7 +393,6 @@ fn from_sources_applies_base_profile_and_overrides() { }, FeatureOverrides { web_search_request: Some(false), - ..Default::default() }, ); diff --git a/codex-rs/utils/plugins/src/mcp_connector.rs b/codex-rs/utils/plugins/src/mcp_connector.rs index 0f02258c16..0c293936c9 100644 --- a/codex-rs/utils/plugins/src/mcp_connector.rs +++ b/codex-rs/utils/plugins/src/mcp_connector.rs @@ -11,8 +11,6 @@ const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ ]; const FIRST_PARTY_CHAT_DISALLOWED_CONNECTOR_IDS: &[&str] = &["connector_0f9c9d4592e54d0a9a12b3f44a1e2010"]; -const ALLOWED_OPENAI_CONNECTOR_IDS: &[&str] = &["connector_openai_library"]; -const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; pub fn is_connector_id_allowed(connector_id: &str) -> bool { is_connector_id_allowed_for_originator(connector_id, originator().value.as_str()) @@ -25,12 +23,7 @@ fn is_connector_id_allowed_for_originator(connector_id: &str, originator_value: DISALLOWED_CONNECTOR_IDS }; - if ALLOWED_OPENAI_CONNECTOR_IDS.contains(&connector_id) { - return true; - } - - !connector_id.starts_with(DISALLOWED_CONNECTOR_PREFIX) - && !disallowed_connector_ids.contains(&connector_id) + !disallowed_connector_ids.contains(&connector_id) } pub fn sanitize_name(name: &str) -> String {