Skip to content

Commit 3f18602

Browse files
committed
feat: load from plugins
1 parent cbbf302 commit 3f18602

File tree

21 files changed

+1158
-104
lines changed

21 files changed

+1158
-104
lines changed

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ use codex_core::find_thread_names_by_ids;
221221
use codex_core::find_thread_path_by_id_str;
222222
use codex_core::git_info::git_diff_to_remote;
223223
use codex_core::mcp::collect_mcp_snapshot;
224+
use codex_core::mcp::configured_mcp_servers;
224225
use codex_core::mcp::group_tools_by_server;
225226
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
226227
use codex_core::parse_cursor;
@@ -4050,7 +4051,9 @@ impl CodexMessageProcessor {
40504051
}
40514052
};
40524053

4053-
let mcp_servers = match serde_json::to_value(config.mcp_servers.get()) {
4054+
let configured_servers =
4055+
configured_mcp_servers(&config, self.thread_manager.plugins_manager().as_ref());
4056+
let mcp_servers = match serde_json::to_value(configured_servers) {
40544057
Ok(value) => value,
40554058
Err(err) => {
40564059
let error = JSONRPCErrorError {
@@ -4111,7 +4114,9 @@ impl CodexMessageProcessor {
41114114
timeout_secs,
41124115
} = params;
41134116

4114-
let Some(server) = config.mcp_servers.get().get(&name) else {
4117+
let configured_servers =
4118+
configured_mcp_servers(&config, self.thread_manager.plugins_manager().as_ref());
4119+
let Some(server) = configured_servers.get(&name) else {
41154120
let error = JSONRPCErrorError {
41164121
code: INVALID_REQUEST_ERROR_CODE,
41174122
message: format!("No MCP server named '{name}' found."),

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use codex_core::config::types::McpServerTransportConfig;
1414
use codex_core::mcp::auth::McpOAuthLoginSupport;
1515
use codex_core::mcp::auth::compute_auth_statuses;
1616
use codex_core::mcp::auth::oauth_login_support;
17+
use codex_core::mcp::effective_mcp_servers;
18+
use codex_core::plugins::PluginsManager;
1719
use codex_protocol::protocol::McpAuthStatus;
1820
use codex_rmcp_client::delete_oauth_tokens;
1921
use codex_rmcp_client::perform_oauth_login;
@@ -327,10 +329,12 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
327329
let config = Config::load_with_cli_overrides(overrides)
328330
.await
329331
.context("failed to load configuration")?;
332+
let plugins_manager = PluginsManager::new(config.codex_home.clone());
333+
let mcp_servers = effective_mcp_servers(&config, None, &plugins_manager);
330334

331335
let LoginArgs { name, scopes } = login_args;
332336

333-
let Some(server) = config.mcp_servers.get().get(&name) else {
337+
let Some(server) = mcp_servers.get(&name) else {
334338
bail!("No MCP server named '{name}' found.");
335339
};
336340

@@ -371,12 +375,12 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
371375
let config = Config::load_with_cli_overrides(overrides)
372376
.await
373377
.context("failed to load configuration")?;
378+
let plugins_manager = PluginsManager::new(config.codex_home.clone());
379+
let mcp_servers = effective_mcp_servers(&config, None, &plugins_manager);
374380

375381
let LogoutArgs { name } = logout_args;
376382

377-
let server = config
378-
.mcp_servers
379-
.get()
383+
let server = mcp_servers
380384
.get(&name)
381385
.ok_or_else(|| anyhow!("No MCP server named '{name}' found in configuration."))?;
382386

@@ -401,14 +405,13 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
401405
let config = Config::load_with_cli_overrides(overrides)
402406
.await
403407
.context("failed to load configuration")?;
408+
let plugins_manager = PluginsManager::new(config.codex_home.clone());
409+
let mcp_servers = effective_mcp_servers(&config, None, &plugins_manager);
404410

405-
let mut entries: Vec<_> = config.mcp_servers.iter().collect();
411+
let mut entries: Vec<_> = mcp_servers.iter().collect();
406412
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
407-
let auth_statuses = compute_auth_statuses(
408-
config.mcp_servers.iter(),
409-
config.mcp_oauth_credentials_store_mode,
410-
)
411-
.await;
413+
let auth_statuses =
414+
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
412415

413416
if list_args.json {
414417
let json_entries: Vec<_> = entries
@@ -651,8 +654,10 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
651654
let config = Config::load_with_cli_overrides(overrides)
652655
.await
653656
.context("failed to load configuration")?;
657+
let plugins_manager = PluginsManager::new(config.codex_home.clone());
658+
let mcp_servers = effective_mcp_servers(&config, None, &plugins_manager);
654659

655-
let Some(server) = config.mcp_servers.get().get(&get_args.name) else {
660+
let Some(server) = mcp_servers.get(&get_args.name) else {
656661
bail!("No MCP server named '{name}' found.", name = get_args.name);
657662
};
658663

codex-rs/core/config.schema.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,22 @@
10641064
],
10651065
"type": "string"
10661066
},
1067+
"PluginConfig": {
1068+
"additionalProperties": false,
1069+
"properties": {
1070+
"enabled": {
1071+
"default": true,
1072+
"type": "boolean"
1073+
},
1074+
"path": {
1075+
"$ref": "#/definitions/AbsolutePathBuf"
1076+
}
1077+
},
1078+
"required": [
1079+
"path"
1080+
],
1081+
"type": "object"
1082+
},
10671083
"ProjectConfig": {
10681084
"additionalProperties": false,
10691085
"properties": {
@@ -1960,6 +1976,14 @@
19601976
"plan_mode_reasoning_effort": {
19611977
"$ref": "#/definitions/ReasoningEffort"
19621978
},
1979+
"plugins": {
1980+
"additionalProperties": {
1981+
"$ref": "#/definitions/PluginConfig"
1982+
},
1983+
"default": {},
1984+
"description": "User-level plugin config entries keyed by plugin name.",
1985+
"type": "object"
1986+
},
19631987
"profile": {
19641988
"description": "Profile to use from the `profiles` map.",
19651989
"type": "string"

codex-rs/core/src/codex.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ use crate::mentions::build_skill_name_counts;
182182
use crate::mentions::collect_explicit_app_ids;
183183
use crate::mentions::collect_tool_mentions_from_messages;
184184
use crate::network_policy_decision::execpolicy_network_rule_amendment;
185+
use crate::plugins::PluginsManager;
185186
use crate::project_doc::get_user_instructions;
186187
use crate::protocol::AgentMessageContentDeltaEvent;
187188
use crate::protocol::AgentReasoningSectionBreakEvent;
@@ -315,6 +316,7 @@ impl Codex {
315316
auth_manager: Arc<AuthManager>,
316317
models_manager: Arc<ModelsManager>,
317318
skills_manager: Arc<SkillsManager>,
319+
plugins_manager: Arc<PluginsManager>,
318320
file_watcher: Arc<FileWatcher>,
319321
conversation_history: InitialHistory,
320322
session_source: SessionSource,
@@ -326,6 +328,7 @@ impl Codex {
326328
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
327329
let (tx_event, rx_event) = async_channel::unbounded();
328330

331+
let loaded_plugins = plugins_manager.plugins_for_config(&config);
329332
let loaded_skills = skills_manager.skills_for_config(&config);
330333

331334
for err in &loaded_skills.errors {
@@ -335,6 +338,19 @@ impl Codex {
335338
err.message
336339
);
337340
}
341+
for plugin in loaded_plugins
342+
.plugins
343+
.iter()
344+
.filter(|plugin| plugin.error.is_some())
345+
{
346+
if let Some(error) = plugin.error.as_deref() {
347+
warn!(
348+
plugin = plugin.config_name,
349+
path = %plugin.root.display(),
350+
"failed to load plugin: {error}"
351+
);
352+
}
353+
}
338354

339355
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source
340356
&& depth >= config.agent_max_depth
@@ -462,6 +478,7 @@ impl Codex {
462478
conversation_history,
463479
session_source_clone,
464480
skills_manager,
481+
plugins_manager,
465482
file_watcher,
466483
agent_control,
467484
)
@@ -1062,6 +1079,7 @@ impl Session {
10621079
initial_history: InitialHistory,
10631080
session_source: SessionSource,
10641081
skills_manager: Arc<SkillsManager>,
1082+
plugins_manager: Arc<PluginsManager>,
10651083
file_watcher: Arc<FileWatcher>,
10661084
agent_control: AgentControl,
10671085
) -> anyhow::Result<Arc<Self>> {
@@ -1144,9 +1162,11 @@ impl Session {
11441162
let history_meta_fut = crate::message_history::history_metadata(&config);
11451163
let auth_manager_clone = Arc::clone(&auth_manager);
11461164
let config_for_mcp = Arc::clone(&config);
1165+
let plugins_manager_for_mcp = Arc::clone(&plugins_manager);
11471166
let auth_and_mcp_fut = async move {
11481167
let auth = auth_manager_clone.auth().await;
1149-
let mcp_servers = effective_mcp_servers(&config_for_mcp, auth.as_ref());
1168+
let mcp_servers =
1169+
effective_mcp_servers(&config_for_mcp, auth.as_ref(), &plugins_manager_for_mcp);
11501170
let auth_statuses = compute_auth_statuses(
11511171
mcp_servers.iter(),
11521172
config_for_mcp.mcp_oauth_credentials_store_mode,
@@ -1394,6 +1414,7 @@ impl Session {
13941414
tool_approvals: Mutex::new(ApprovalStore::default()),
13951415
execve_session_approvals: RwLock::new(HashMap::new()),
13961416
skills_manager,
1417+
plugins_manager,
13971418
file_watcher,
13981419
agent_control,
13991420
network_proxy,
@@ -2307,6 +2328,8 @@ impl Session {
23072328
.config_layer_stack
23082329
.with_user_config(&config_toml_path, user_config);
23092330
state.session_configuration.original_config_do_not_use = Arc::new(config);
2331+
self.services.skills_manager.clear_cache();
2332+
self.services.plugins_manager.clear_cache();
23102333
}
23112334

23122335
pub(crate) async fn new_default_turn_with_sub_id(&self, sub_id: String) -> Arc<TurnContext> {
@@ -4120,7 +4143,11 @@ mod handlers {
41204143
pub async fn list_mcp_tools(sess: &Session, config: &Arc<Config>, sub_id: String) {
41214144
let mcp_connection_manager = sess.services.mcp_connection_manager.read().await;
41224145
let auth = sess.services.auth_manager.auth().await;
4123-
let mcp_servers = effective_mcp_servers(config, auth.as_ref());
4146+
let mcp_servers = effective_mcp_servers(
4147+
config,
4148+
auth.as_ref(),
4149+
sess.services.plugins_manager.as_ref(),
4150+
);
41244151
let snapshot = collect_mcp_snapshot_from_manager(
41254152
&mcp_connection_manager,
41264153
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode)
@@ -8280,6 +8307,11 @@ mod tests {
82808307

82818308
let (tx_event, _rx_event) = async_channel::unbounded();
82828309
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
8310+
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.clone()));
8311+
let skills_manager = Arc::new(SkillsManager::new(
8312+
config.codex_home.clone(),
8313+
Arc::clone(&plugins_manager),
8314+
));
82838315
let result = Session::new(
82848316
session_configuration,
82858317
Arc::clone(&config),
@@ -8290,7 +8322,8 @@ mod tests {
82908322
agent_status_tx,
82918323
InitialHistory::New,
82928324
SessionSource::Exec,
8293-
Arc::new(SkillsManager::new(config.codex_home.clone())),
8325+
skills_manager,
8326+
plugins_manager,
82948327
Arc::new(FileWatcher::noop()),
82958328
AgentControl::default(),
82968329
)
@@ -8371,7 +8404,11 @@ mod tests {
83718404
);
83728405

83738406
let state = SessionState::new(session_configuration.clone());
8374-
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone()));
8407+
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.clone()));
8408+
let skills_manager = Arc::new(SkillsManager::new(
8409+
config.codex_home.clone(),
8410+
Arc::clone(&plugins_manager),
8411+
));
83758412
let network_approval = Arc::new(NetworkApprovalService::default());
83768413

83778414
let file_watcher = Arc::new(FileWatcher::noop());
@@ -8405,6 +8442,7 @@ mod tests {
84058442
tool_approvals: Mutex::new(ApprovalStore::default()),
84068443
execve_session_approvals: RwLock::new(HashMap::new()),
84078444
skills_manager,
8445+
plugins_manager,
84088446
file_watcher,
84098447
agent_control,
84108448
network_proxy: None,
@@ -8530,7 +8568,11 @@ mod tests {
85308568
);
85318569

85328570
let state = SessionState::new(session_configuration.clone());
8533-
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone()));
8571+
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.clone()));
8572+
let skills_manager = Arc::new(SkillsManager::new(
8573+
config.codex_home.clone(),
8574+
Arc::clone(&plugins_manager),
8575+
));
85348576
let network_approval = Arc::new(NetworkApprovalService::default());
85358577

85368578
let file_watcher = Arc::new(FileWatcher::noop());
@@ -8564,6 +8606,7 @@ mod tests {
85648606
tool_approvals: Mutex::new(ApprovalStore::default()),
85658607
execve_session_approvals: RwLock::new(HashMap::new()),
85668608
skills_manager,
8609+
plugins_manager,
85678610
file_watcher,
85688611
agent_control,
85698612
network_proxy: None,

codex-rs/core/src/codex_delegate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub(crate) async fn run_codex_thread_interactive(
5353
auth_manager,
5454
models_manager,
5555
Arc::clone(&parent_session.services.skills_manager),
56+
Arc::clone(&parent_session.services.plugins_manager),
5657
Arc::clone(&parent_session.services.file_watcher),
5758
initial_history.unwrap_or(InitialHistory::New),
5859
SessionSource::SubAgent(SubAgentSource::Review),

codex-rs/core/src/config/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::config::types::Notifications;
1515
use crate::config::types::OtelConfig;
1616
use crate::config::types::OtelConfigToml;
1717
use crate::config::types::OtelExporterKind;
18+
use crate::config::types::PluginConfig;
1819
use crate::config::types::SandboxWorkspaceWrite;
1920
use crate::config::types::ShellEnvironmentPolicy;
2021
use crate::config::types::ShellEnvironmentPolicyToml;
@@ -1203,6 +1204,10 @@ pub struct ConfigToml {
12031204
/// User-level skill config entries keyed by SKILL.md path.
12041205
pub skills: Option<SkillsConfig>,
12051206

1207+
/// User-level plugin config entries keyed by plugin name.
1208+
#[serde(default)]
1209+
pub plugins: HashMap<String, PluginConfig>,
1210+
12061211
/// Centralized feature flags (new). Prefer this over individual toggles.
12071212
#[serde(default)]
12081213
// Injects known feature keys into the schema and forbids unknown keys.

codex-rs/core/src/config/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,14 @@ pub struct SkillConfig {
727727
pub enabled: bool,
728728
}
729729

730+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
731+
#[schemars(deny_unknown_fields)]
732+
pub struct PluginConfig {
733+
pub path: AbsolutePathBuf,
734+
#[serde(default = "default_enabled")]
735+
pub enabled: bool,
736+
}
737+
730738
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
731739
#[schemars(deny_unknown_fields)]
732740
pub struct SkillsConfig {

codex-rs/core/src/file_watcher.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use tokio::time::sleep_until;
2323
use tracing::warn;
2424

2525
use crate::config::Config;
26-
use crate::skills::loader::skill_roots_from_layer_stack_with_agents;
26+
use crate::skills::loader::skill_roots;
2727

2828
#[derive(Debug, Clone, PartialEq, Eq)]
2929
pub enum FileWatcherEvent {
@@ -145,7 +145,7 @@ impl FileWatcher {
145145

146146
pub(crate) fn register_config(self: &Arc<Self>, config: &Config) -> WatchRegistration {
147147
let deduped_roots: HashSet<PathBuf> =
148-
skill_roots_from_layer_stack_with_agents(&config.config_layer_stack, &config.cwd)
148+
skill_roots(&config.config_layer_stack, &config.cwd, Vec::new())
149149
.into_iter()
150150
.map(|root| root.path)
151151
.collect();

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mod message_history;
5656
mod model_provider_info;
5757
pub mod path_utils;
5858
pub mod personality_migration;
59+
pub mod plugins;
5960
mod sandbox_tags;
6061
pub mod sandboxing;
6162
mod session_prefix;

0 commit comments

Comments
 (0)