respect workspace option for disabling plugins (#18907)

Respects the workspace setting for plugins in Codex

Plugins menu disappears
Plugins do not load
Plugins do not load in composer

no plugins loaded
<img width="809" height="226" alt="Screenshot 2026-04-23 at 3 20 45 PM"
src="https://github.com/user-attachments/assets/3a4dba8e-69c3-4046-a77e-f13ab77f84b4"
/>


no plugins in menu
<img width="293" height="204" alt="Screenshot 2026-04-23 at 3 20 35 PM"
src="https://github.com/user-attachments/assets/5cb9bf52-ad72-488f-b90c-5eb457da09a3"
/>
This commit is contained in:
Alex Zamoshchin
2026-04-24 13:38:45 -04:00
committed by GitHub
parent f802f0a391
commit bcc1caa920
11 changed files with 851 additions and 7 deletions

View File

@@ -151,6 +151,68 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()> {
let connectors = vec![AppInfo {
id: "beta".to_string(),
name: "Beta".to_string(),
description: Some("Beta connector".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) = start_apps_server_with_workspace_plugins_enabled(
connectors, tools, /*workspace_plugins_enabled*/ false,
)
.await?;
let codex_home = TempDir::new()?;
write_connectors_config(codex_home.path(), &server_url)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123")
.plan_type("team"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_apps_list_request(AppsListParams {
limit: Some(50),
cursor: None,
thread_id: None,
force_refetch: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let AppsListResponse { data, next_cursor } = to_response(response)?;
assert!(data.is_empty());
assert!(next_cursor.is_none());
server_handle.abort();
let _ = server_handle.await;
Ok(())
}
#[tokio::test]
async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Result<()> {
let connectors = vec![AppInfo {
@@ -1329,6 +1391,7 @@ struct AppsServerState {
expected_account_id: String,
response: Arc<StdMutex<serde_json::Value>>,
directory_delay: Duration,
workspace_plugins_enabled: bool,
}
#[derive(Clone)]
@@ -1412,11 +1475,45 @@ async fn start_apps_server_with_delays(
Ok((server_url, server_handle))
}
async fn start_apps_server_with_workspace_plugins_enabled(
connectors: Vec<AppInfo>,
tools: Vec<Tool>,
workspace_plugins_enabled: bool,
) -> Result<(String, JoinHandle<()>)> {
let (server_url, server_handle, _server_control) =
start_apps_server_with_delays_and_control_inner(
connectors,
tools,
Duration::ZERO,
Duration::ZERO,
workspace_plugins_enabled,
)
.await?;
Ok((server_url, server_handle))
}
async fn start_apps_server_with_delays_and_control(
connectors: Vec<AppInfo>,
tools: Vec<Tool>,
directory_delay: Duration,
tools_delay: Duration,
) -> Result<(String, JoinHandle<()>, AppsServerControl)> {
start_apps_server_with_delays_and_control_inner(
connectors,
tools,
directory_delay,
tools_delay,
/*workspace_plugins_enabled*/ true,
)
.await
}
async fn start_apps_server_with_delays_and_control_inner(
connectors: Vec<AppInfo>,
tools: Vec<Tool>,
directory_delay: Duration,
tools_delay: Duration,
workspace_plugins_enabled: bool,
) -> Result<(String, JoinHandle<()>, AppsServerControl)> {
let response = Arc::new(StdMutex::new(
json!({ "apps": connectors, "next_token": null }),
@@ -1427,6 +1524,7 @@ async fn start_apps_server_with_delays_and_control(
expected_account_id: "account-123".to_string(),
response: response.clone(),
directory_delay,
workspace_plugins_enabled,
};
let state = Arc::new(state);
let server_control = AppsServerControl {
@@ -1452,6 +1550,10 @@ async fn start_apps_server_with_delays_and_control(
"/connectors/directory/list_workspace",
get(list_directory_connectors),
)
.route(
"/accounts/account-123/settings",
get(workspace_settings_response),
)
.with_state(state)
.nest_service("/api/codex/apps", mcp_service);
@@ -1462,6 +1564,30 @@ async fn start_apps_server_with_delays_and_control(
Ok((format!("http://{addr}"), handle, server_control))
}
async fn workspace_settings_response(
State(state): State<Arc<AppsServerState>>,
headers: HeaderMap,
) -> Result<impl axum::response::IntoResponse, StatusCode> {
let bearer_ok = headers
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == state.expected_bearer);
let account_ok = headers
.get("chatgpt-account-id")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == state.expected_account_id);
if !bearer_ok || !account_ok {
Err(StatusCode::UNAUTHORIZED)
} else {
Ok(Json(json!({
"beta_settings": {
"plugins": state.workspace_plugins_enabled
}
})))
}
}
async fn list_directory_connectors(
State(state): State<Arc<AppsServerState>>,
headers: HeaderMap,