Merge branch 'main' into canvrno/delegated-mcp-elicitation-routing

This commit is contained in:
canvrno-oai
2026-05-13 17:56:58 -07:00
committed by GitHub
26 changed files with 517 additions and 562 deletions
-153
View File
@@ -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"
+57 -5
View File
@@ -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
+10 -7
View File
@@ -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::<JoinHandle<()>>::new();
@@ -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(),
@@ -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(())
}
@@ -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(),
@@ -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,
-1
View File
@@ -443,7 +443,6 @@ pub struct ConfigToml {
pub experimental_instructions_file: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
pub oss_provider: Option<String>,
}
-2
View File
@@ -54,13 +54,11 @@ pub struct ConfigProfile {
#[schemars(skip)]
pub experimental_instructions_file: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub include_apply_patch_tool: Option<bool>,
pub include_permissions_instructions: Option<bool>,
pub include_apps_instructions: Option<bool>,
pub include_collaboration_mode_instructions: Option<bool>,
pub include_environment_context: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub tools: Option<ToolsToml>,
pub web_search: Option<WebSearchMode>,
pub analytics: Option<AnalyticsConfigToml>,
+162 -3
View File
@@ -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<AppInfo>,
@@ -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::<AppInfo>::new());
}
}
+87
View File
@@ -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<String> {
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"]),
}]
);
}
}
+26 -12
View File
@@ -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::<BTreeSet<_>>();
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::<Result<Vec<_>, _>>()?;
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)),
@@ -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::<String, BTreeSet<String>>::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());
}
}
@@ -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
)
}
@@ -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 {
-15
View File
@@ -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"
},
-4
View File
@@ -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(())
+1 -25
View File
@@ -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(),
+1 -7
View File
@@ -1881,7 +1881,6 @@ pub struct ConfigOverrides {
pub developer_instructions: Option<String>,
pub personality: Option<Personality>,
pub compact_prompt: Option<String>,
pub include_apply_patch_tool: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>,
pub tools_web_search_request: Option<bool>,
pub ephemeral: Option<bool>,
@@ -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,
},
+32 -220
View File
@@ -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<String> {
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<R>(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::<AppInfo>::new());
}
-1
View File
@@ -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<Toml>(
-1
View File
@@ -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),
-11
View File
@@ -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<Feature> {
#[derive(Debug, Default)]
pub(crate) struct LegacyFeatureToggles {
pub include_apply_patch_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
}
@@ -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,
-8
View File
@@ -286,7 +286,6 @@ pub struct Features {
#[derive(Debug, Clone, Default)]
pub struct FeatureOverrides {
pub include_apply_patch_tool: Option<bool>,
pub web_search_request: Option<bool>,
}
@@ -294,17 +293,11 @@ pub struct FeatureOverrides {
pub struct FeatureConfigSource<'a> {
pub features: Option<&'a FeaturesToml>,
pub include_apply_patch_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
}
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);
-1
View File
@@ -393,7 +393,6 @@ fn from_sources_applies_base_profile_and_overrides() {
},
FeatureOverrides {
web_search_request: Some(false),
..Default::default()
},
);
+1 -8
View File
@@ -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 {