Skip to content

fix: harden plugin feature gating#15020

Merged
xl-openai merged 1 commit intomainfrom
xl/plugins2
Mar 18, 2026
Merged

fix: harden plugin feature gating#15020
xl-openai merged 1 commit intomainfrom
xl/plugins2

Conversation

@xl-openai
Copy link
Collaborator

@xl-openai xl-openai commented Mar 18, 2026

BAD PR
I messed up the commits in this PR and accidentally merged incorrect changes.

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65980a9847

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

force_reload: bool,
) -> PluginLoadOutcome {
if !plugins_feature_enabled_from_stack(config_layer_stack) {
if !config.features.enabled(Feature::Plugins) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict plugin feature checks to trusted user config

plugins_for_layer_stack now gates on config.features.enabled(Feature::Plugins), which is computed from merged layers. A project .codex/config.toml can set [features].plugins = true and override a user-disabled flag, re-enabling persisted user plugins for that checkout. This breaks the prior user-opt-in boundary and lets untrusted project config influence plugin activation.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that‘s ok.

let loaded_plugins =
self.plugins_manager
.plugins_for_layer_stack(cwd, &config_layer_stack, force_reload);
let config_toml: ConfigToml = match config_layer_stack.effective_config().try_into() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deserialize merged config with base path before skill load

skills_for_cwd_with_extra_user_roots uses effective_config().try_into::<ConfigToml>() directly. This skips deserialize_config_toml_with_base, so any relative AbsolutePathBuf field fails deserialization (AbsolutePathBuf deserialized without a base path). The method then returns an error and skips loading skills, even though normal config loading accepts those configs.

Useful? React with 👍 / 👎.

@xl-openai
Copy link
Collaborator Author

@codex review again

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36c76cbc66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


impl PluginsManager {
pub fn new(codex_home: PathBuf) -> Self {
pub fn new(codex_home: PathBuf, plugins_enabled: bool) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update all PluginsManager::new call sites

Changing PluginsManager::new to require plugins_enabled breaks callers that still use the old 1-arg API. In this commit, chatgpt/src/connectors.rs:119, cli/src/mcp_cmd.rs:392, and tui_app_server/src/chatwidget.rs:8851 still call PluginsManager::new(config.codex_home.clone()), which causes a compile-time arity error (E0061) for those crates.

Useful? React with 👍 / 👎.

Comment on lines +178 to +181
let plugins_manager = Arc::new(PluginsManager::new(
codex_home.clone(),
config.features.enabled(crate::features::Feature::Plugins),
));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this instance is reused for all threads/sessions over the lifetime of the process. so for app-server, changing config after the process is started wouldn't update the value, and passing thread-level config that changes the value wouldn't be respected.

lmk if im misunderstanding, but this would basically make it static until you restart app-server

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call rollback to original approach.

@xl-openai xl-openai force-pushed the xl/plugins2 branch 2 times, most recently from 5334dff to 07c90b2 Compare March 18, 2026 06:44
@sayan-oai
Copy link
Collaborator

@codex review this

force_reload: bool,
) -> PluginLoadOutcome {
if !plugins_feature_enabled_from_stack(config_layer_stack) {
if !config.features.enabled(Feature::Plugins) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this reads cloud requirements, yes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct.

Comment on lines 1247 to 1249
let Some(plugins_value) = user_layer.config.get("plugins") else {
return HashMap::new();
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we discussed offline, this just reads the current enablement state of the plugins directly from config, without considering whether features.plugins is enabled. do we not want to gate this as well?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looked a bit more, configured_plugins_from_stack seems like the main way existing functionality loads plugins. gating here seems like it would allow us to avoid gating on many of the higher level functions you added the gating to, as long as those work correctly when no plugins are returned (empty HashMap).

it seems that the only logic that needs to keep its own gate is the marketplace-reading logic in list_marketplaces_for_config.

im not sure if code-organization-wise pushing down the gate check is better (less random gate checks in higher-level functions, dont have to remember to add those checks yourself) or worse (less clear that those functions will return empty when features.plugin = false).

LMK what you think.

Copy link
Collaborator Author

@xl-openai xl-openai Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think configured_plugins_from_stack is a low-level help function and it makes sense to parse the config stack directly (no depends on config). But I do cleanup the configured_plugin_states more to get rid of the plugins_for_config call.

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee78106b04

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 447 to 448
if let Some(outcome) = self.cached_enabled_outcome() {
return outcome;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore force_reload semantics for plugin skill loading

skills_for_cwd*_ still accepts force_reload, but plugin resolution now calls plugins_for_config, which unconditionally returns cached_enabled_outcome when present. After plugin config/state changes, force_reload=true no longer refreshes plugin-provided roots, so stale plugin skills can persist until an unrelated clear_cache() runs.

Useful? React with 👍 / 👎.

Comment on lines +245 to +253
match load_marketplace(&marketplace_path) {
Ok(marketplace) => marketplaces.push(marketplace),
Err(err) => {
warn!(
path = %marketplace_path,
error = %err,
"skipping marketplace that failed to load"
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow plugin/list fail-open handling to malformed files only

list_marketplaces_with_home now suppresses every load_marketplace failure and only logs a warning. This causes plugin/list to return partial/empty success responses for IO/permission/other hard failures, masking actionable errors. The intended behavior was fail-open for malformed marketplace data, not all marketplace load errors.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is OK.

Copy link
Collaborator

@sayan-oai sayan-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving to unblock as some of these fixes are urgent, but I think we should think about this comment to see which option is cleaner (it was already hard to debug this because of messy code 🙃).

and i know this is tedious/a nit, but it would be great if we could split this pr based on the 4 points in the description. the changes are all small but high impact on functionality, and I'm worried about rolling back 3 good fixes because they were bundled with one bug.

@xl-openai xl-openai changed the title fix: read plugin feature flag from requirement-resolved config. fix: harden plugin feature gating Mar 18, 2026
@xl-openai
Copy link
Collaborator Author

Removing plugins_for_layer_stack means a cwd-scoped plugin cache no longer makes sense, so I update it to a global cache. Also to fully respect the feature flag, we also need to add gating at the remaining callsites.

The fail-open change for marketplace.json is a bit orthogonal, but it is small, so I’m keeping it in this PR.

@xl-openai xl-openai merged commit 580f32a into main Mar 18, 2026
54 of 56 checks passed
@xl-openai xl-openai deleted the xl/plugins2 branch March 18, 2026 17:11
@github-actions github-actions bot locked and limited conversation to collaborators Mar 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants