Revert "fix: harden plugin feature gating" (#15102)

Reverts openai/codex#15020

I messed up the commit in my PR and accidentally merged changes that
were still under review.
This commit is contained in:
xl-openai
2026-03-18 15:19:29 -07:00
committed by GitHub
Unverified
parent e5de13644d
commit 86982ca1f9
40 changed files with 52 additions and 926 deletions
-4
View File
@@ -1033,10 +1033,6 @@ mod tests {
for (session_source, expected_source) in [
(SessionSource::Exec, ApiSessionSource::Exec),
(SessionSource::Cli, ApiSessionSource::Cli),
(
SessionSource::Custom("atlas".to_string()),
ApiSessionSource::Custom("atlas".to_string()),
),
] {
let client = start_test_client(session_source).await;
let parsed: ThreadStartResponse = client
@@ -2894,7 +2894,6 @@
"vscode",
"exec",
"appServer",
"custom",
"subAgent",
"subAgentReview",
"subAgentCompact",
@@ -1880,19 +1880,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -10984,19 +10984,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -13110,7 +13097,6 @@
"vscode",
"exec",
"appServer",
"custom",
"subAgent",
"subAgentReview",
"subAgentCompact",
@@ -8744,19 +8744,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -10870,7 +10857,6 @@
"vscode",
"exec",
"appServer",
"custom",
"subAgent",
"subAgentReview",
"subAgentCompact",
@@ -826,19 +826,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -14,7 +14,6 @@
"vscode",
"exec",
"appServer",
"custom",
"subAgent",
"subAgentReview",
"subAgentCompact",
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -826,19 +826,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -826,19 +826,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -584,19 +584,6 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SubAgentSource } from "./SubAgentSource";
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "custom": string } | { "subagent": SubAgentSource } | "unknown";
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "subagent": SubAgentSource } | "unknown";
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SubAgentSource } from "../SubAgentSource";
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "custom": string } | { "subAgent": SubAgentSource } | "unknown";
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "subAgent": SubAgentSource } | "unknown";
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadSourceKind = "cli" | "vscode" | "exec" | "appServer" | "custom" | "subAgent" | "subAgentReview" | "subAgentCompact" | "subAgentThreadSpawn" | "subAgentOther" | "unknown";
export type ThreadSourceKind = "cli" | "vscode" | "exec" | "appServer" | "subAgent" | "subAgentReview" | "subAgentCompact" | "subAgentThreadSpawn" | "subAgentOther" | "unknown";
@@ -1467,7 +1467,6 @@ pub enum SessionSource {
VsCode,
Exec,
AppServer,
Custom(String),
SubAgent(CoreSubAgentSource),
#[serde(other)]
Unknown,
@@ -1480,7 +1479,6 @@ impl From<CoreSessionSource> for SessionSource {
CoreSessionSource::VSCode => SessionSource::VsCode,
CoreSessionSource::Exec => SessionSource::Exec,
CoreSessionSource::Mcp => SessionSource::AppServer,
CoreSessionSource::Custom(source) => SessionSource::Custom(source),
CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub),
CoreSessionSource::Unknown => SessionSource::Unknown,
}
@@ -1494,7 +1492,6 @@ impl From<SessionSource> for CoreSessionSource {
SessionSource::VsCode => CoreSessionSource::VSCode,
SessionSource::Exec => CoreSessionSource::Exec,
SessionSource::AppServer => CoreSessionSource::Mcp,
SessionSource::Custom(source) => CoreSessionSource::Custom(source),
SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub),
SessionSource::Unknown => CoreSessionSource::Unknown,
}
@@ -2954,7 +2951,6 @@ pub enum ThreadSourceKind {
VsCode,
Exec,
AppServer,
Custom,
SubAgent,
SubAgentReview,
SubAgentCompact,
+1 -1
View File
@@ -256,7 +256,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
- `limit` — server defaults to a reasonable page size if unset.
- `sortKey``created_at` (default) or `updated_at`.
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`, and custom product sources).
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
@@ -423,10 +423,7 @@ impl CodexMessageProcessor {
Ok(config) => self
.thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(
&config,
&self.thread_manager.session_source(),
),
.maybe_start_curated_repo_sync_for_config(&config),
Err(err) => warn!("failed to load latest config for curated plugin sync: {err:?}"),
}
}
@@ -5305,7 +5302,6 @@ impl CodexMessageProcessor {
force_reload,
per_cwd_extra_user_roots,
} = params;
let session_source = self.thread_manager.session_source();
let cwds = if cwds.is_empty() {
vec![self.config.cwd.clone()]
} else {
@@ -5350,12 +5346,9 @@ impl CodexMessageProcessor {
let extra_roots = extra_roots_by_cwd
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let outcome = codex_core::skills::filter_skill_load_outcome_for_session_source(
skills_manager
.skills_for_cwd_with_extra_user_roots(&cwd, force_reload, extra_roots)
.await,
&session_source,
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(&cwd, force_reload, extra_roots)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
data.push(codex_app_server_protocol::SkillsListEntry {
@@ -5371,7 +5364,6 @@ impl CodexMessageProcessor {
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
let plugins_manager = self.thread_manager.plugins_manager();
let session_source = self.thread_manager.session_source();
let PluginListParams {
cwds,
force_remote_sync,
@@ -5425,13 +5417,15 @@ impl CodexMessageProcessor {
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
marketplaces
.into_iter()
.filter_map(|marketplace| {
let plugins = marketplace
.map(|marketplace| PluginMarketplaceEntry {
name: marketplace.name,
path: marketplace.path,
interface: marketplace.interface.map(|interface| MarketplaceInterface {
display_name: interface.display_name,
}),
plugins: marketplace
.plugins
.into_iter()
.filter(|plugin| {
session_source.matches_product_restriction(&plugin.policy.products)
})
.map(|plugin| PluginSummary {
id: plugin.id,
installed: plugin.installed,
@@ -5442,18 +5436,7 @@ impl CodexMessageProcessor {
auth_policy: plugin.policy.authentication.into(),
interface: plugin.interface.map(plugin_interface_to_info),
})
.collect::<Vec<_>>();
(!plugins.is_empty()).then_some(PluginMarketplaceEntry {
name: marketplace.name,
path: marketplace.path,
interface: marketplace.interface.map(|interface| {
MarketplaceInterface {
display_name: interface.display_name,
}
}),
plugins,
})
.collect(),
})
.collect(),
)
@@ -5528,11 +5511,6 @@ impl CodexMessageProcessor {
return;
}
};
let session_source = self.thread_manager.session_source();
let plugin_skills = codex_core::skills::filter_skills_for_session_source(
outcome.plugin.skills,
&session_source,
);
let app_summaries =
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
let plugin = PluginDetail {
@@ -5549,7 +5527,7 @@ impl CodexMessageProcessor {
interface: outcome.plugin.interface.map(plugin_interface_to_info),
},
description: outcome.plugin.description,
skills: plugin_skills_to_info(&plugin_skills),
skills: plugin_skills_to_info(&outcome.plugin.skills),
apps: app_summaries,
mcp_servers: outcome.plugin.mcp_server_names,
};
+7 -34
View File
@@ -1,24 +1,17 @@
use codex_app_server_protocol::ThreadSourceKind;
use codex_core::INTERACTIVE_SESSION_SOURCES;
use codex_protocol::protocol::SessionSource as CoreSessionSource;
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
fn interactive_source_kinds() -> Vec<ThreadSourceKind> {
vec![
ThreadSourceKind::Cli,
ThreadSourceKind::VsCode,
ThreadSourceKind::Custom,
]
}
pub(crate) fn compute_source_filters(
source_kinds: Option<Vec<ThreadSourceKind>>,
) -> (Vec<CoreSessionSource>, Option<Vec<ThreadSourceKind>>) {
let Some(source_kinds) = source_kinds else {
return (Vec::new(), Some(interactive_source_kinds()));
return (INTERACTIVE_SESSION_SOURCES.to_vec(), None);
};
if source_kinds.is_empty() {
return (Vec::new(), Some(interactive_source_kinds()));
return (INTERACTIVE_SESSION_SOURCES.to_vec(), None);
}
let requires_post_filter = source_kinds.iter().any(|kind| {
@@ -26,7 +19,6 @@ pub(crate) fn compute_source_filters(
kind,
ThreadSourceKind::Exec
| ThreadSourceKind::AppServer
| ThreadSourceKind::Custom
| ThreadSourceKind::SubAgent
| ThreadSourceKind::SubAgentReview
| ThreadSourceKind::SubAgentCompact
@@ -46,7 +38,6 @@ pub(crate) fn compute_source_filters(
ThreadSourceKind::VsCode => Some(CoreSessionSource::VSCode),
ThreadSourceKind::Exec
| ThreadSourceKind::AppServer
| ThreadSourceKind::Custom
| ThreadSourceKind::SubAgent
| ThreadSourceKind::SubAgentReview
| ThreadSourceKind::SubAgentCompact
@@ -65,7 +56,6 @@ pub(crate) fn source_kind_matches(source: &CoreSessionSource, filter: &[ThreadSo
ThreadSourceKind::VsCode => matches!(source, CoreSessionSource::VSCode),
ThreadSourceKind::Exec => matches!(source, CoreSessionSource::Exec),
ThreadSourceKind::AppServer => matches!(source, CoreSessionSource::Mcp),
ThreadSourceKind::Custom => matches!(source, CoreSessionSource::Custom(_)),
ThreadSourceKind::SubAgent => matches!(source, CoreSessionSource::SubAgent(_)),
ThreadSourceKind::SubAgentReview => {
matches!(
@@ -102,16 +92,16 @@ mod tests {
fn compute_source_filters_defaults_to_interactive_sources() {
let (allowed_sources, filter) = compute_source_filters(None);
assert_eq!(allowed_sources, Vec::new());
assert_eq!(filter, Some(interactive_source_kinds()));
assert_eq!(allowed_sources, INTERACTIVE_SESSION_SOURCES.to_vec());
assert_eq!(filter, None);
}
#[test]
fn compute_source_filters_empty_means_interactive_sources() {
let (allowed_sources, filter) = compute_source_filters(Some(Vec::new()));
assert_eq!(allowed_sources, Vec::new());
assert_eq!(filter, Some(interactive_source_kinds()));
assert_eq!(allowed_sources, INTERACTIVE_SESSION_SOURCES.to_vec());
assert_eq!(filter, None);
}
#[test]
@@ -135,15 +125,6 @@ mod tests {
assert_eq!(filter, Some(source_kinds));
}
#[test]
fn compute_source_filters_custom_requires_post_filtering() {
let source_kinds = vec![ThreadSourceKind::Custom];
let (allowed_sources, filter) = compute_source_filters(Some(source_kinds.clone()));
assert_eq!(allowed_sources, Vec::new());
assert_eq!(filter, Some(source_kinds));
}
#[test]
fn source_kind_matches_distinguishes_subagent_variants() {
let parent_thread_id =
@@ -173,12 +154,4 @@ mod tests {
&[ThreadSourceKind::SubAgentReview]
));
}
#[test]
fn source_kind_matches_custom_sources() {
let custom = CoreSessionSource::Custom("atlas".to_string());
assert!(source_kind_matches(&custom, &[ThreadSourceKind::Custom]));
assert!(!source_kind_matches(&custom, &[ThreadSourceKind::Cli]));
}
}
-4
View File
@@ -808,10 +808,6 @@ mod tests {
for (requested_source, expected_source) in [
(SessionSource::Cli, ApiSessionSource::Cli),
(SessionSource::Exec, ApiSessionSource::Exec),
(
SessionSource::Custom("atlas".to_string()),
ApiSessionSource::Custom("atlas".to_string()),
),
] {
let client = start_test_client(requested_source).await;
let response = client
+1 -3
View File
@@ -336,7 +336,6 @@ pub async fn run_main(
loader_overrides,
default_analytics_enabled,
AppServerTransport::Stdio,
SessionSource::VSCode,
)
.await
}
@@ -347,7 +346,6 @@ pub async fn run_main_with_transport(
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
transport: AppServerTransport,
session_source: SessionSource,
) -> IoResult<()> {
let (transport_event_tx, mut transport_event_rx) =
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
@@ -623,7 +621,7 @@ pub async fn run_main_with_transport(
feedback: feedback.clone(),
log_db,
config_warnings,
session_source,
session_source: SessionSource::VSCode,
enable_codex_api_key_env: false,
});
let mut thread_created_rx = processor.thread_created_receiver();
-15
View File
@@ -4,7 +4,6 @@ use codex_app_server::run_main_with_transport;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_core::config_loader::LoaderOverrides;
use codex_protocol::protocol::SessionSource;
use codex_utils_cli::CliConfigOverrides;
use std::path::PathBuf;
@@ -22,17 +21,6 @@ struct AppServerArgs {
default_value = AppServerTransport::DEFAULT_LISTEN_URL
)]
listen: AppServerTransport,
/// Session source stamped into new threads started by this app-server.
///
/// Known values such as `vscode`, `cli`, `exec`, and `mcp` map to built-in
/// sources. Any other non-empty value is recorded as a custom source.
#[arg(
long = "session-source",
value_name = "SOURCE",
default_value = "vscode"
)]
session_source: String,
}
fn main() -> anyhow::Result<()> {
@@ -44,8 +32,6 @@ fn main() -> anyhow::Result<()> {
..Default::default()
};
let transport = args.listen;
let session_source = SessionSource::from_startup_arg(args.session_source.as_str())
.map_err(|err| anyhow::anyhow!("invalid --session-source: {err}"))?;
run_main_with_transport(
arg0_paths,
@@ -53,7 +39,6 @@ fn main() -> anyhow::Result<()> {
loader_overrides,
/*default_analytics_enabled*/ false,
transport,
session_source,
)
.await?;
Ok(())
+1 -1
View File
@@ -231,7 +231,7 @@ impl MessageProcessor {
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config, &thread_manager.session_source());
.maybe_start_curated_repo_sync_for_config(&config);
let cloud_requirements = Arc::new(RwLock::new(cloud_requirements));
let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs {
auth_manager: auth_manager.clone(),
@@ -95,11 +95,7 @@ pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
impl McpProcess {
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
Self::new_with_env_and_args(codex_home, &[], &[]).await
}
pub async fn new_with_args(codex_home: &Path, args: &[&str]) -> anyhow::Result<Self> {
Self::new_with_env_and_args(codex_home, &[], args).await
Self::new_with_env(codex_home, &[]).await
}
/// Creates a new MCP process, allowing tests to override or remove
@@ -110,14 +106,6 @@ impl McpProcess {
pub async fn new_with_env(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
) -> anyhow::Result<Self> {
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
}
pub async fn new_with_env_and_args(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
args: &[&str],
) -> anyhow::Result<Self> {
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
.context("should find binary for codex-app-server")?;
@@ -130,7 +118,6 @@ impl McpProcess {
cmd.env("CODEX_HOME", codex_home);
cmd.env("RUST_LOG", "info");
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
cmd.args(args);
for (k, v) in env_overrides {
match v {
@@ -25,8 +25,6 @@ use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SkillsListParams;
use codex_app_server_protocol::SkillsListResponse;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -478,92 +476,6 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn plugin_install_filters_product_restricted_plugin_skills() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
write_plugins_enabled_config(codex_home.path())?;
write_plugin_marketplace(
repo_root.path(),
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
let plugin_root = repo_root.path().join("sample-plugin");
write_plugin_skill(
&plugin_root,
"all-products",
"Visible to every product",
&[],
)?;
write_plugin_skill(
&plugin_root,
"chatgpt-only",
"Visible to ChatGPT",
&["CHATGPT"],
)?;
write_plugin_skill(&plugin_root, "atlas-only", "Visible to Atlas", &["ATLAS"])?;
let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let mut mcp =
McpProcess::new_with_args(codex_home.path(), &["--session-source", "chatgpt"]).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path,
plugin_name: "sample-plugin".to_string(),
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginInstallResponse = to_response(response)?;
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![codex_home.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: SkillsListResponse = to_response(response)?;
let mut skills = response
.data
.into_iter()
.flat_map(|entry| entry.skills.into_iter())
.map(|skill| skill.name)
.filter(|name| name.starts_with("sample-plugin:"))
.collect::<Vec<_>>();
skills.sort_unstable();
assert_eq!(
skills,
vec![
"sample-plugin:all-products".to_string(),
"sample-plugin:chatgpt-only".to_string(),
]
);
Ok(())
}
#[derive(Clone)]
struct AppsServerState {
response: Arc<StdMutex<serde_json::Value>>,
@@ -735,16 +647,6 @@ plugins = true
)
}
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
r#"
[features]
plugins = true
"#,
)
}
fn write_plugin_marketplace(
repo_root: &std::path::Path,
marketplace_name: &str,
@@ -814,32 +716,3 @@ fn write_plugin_source(
)?;
Ok(())
}
fn write_plugin_skill(
plugin_root: &std::path::Path,
skill_name: &str,
description: &str,
products: &[&str],
) -> Result<()> {
let skill_dir = plugin_root.join("skills").join(skill_name);
std::fs::create_dir_all(&skill_dir)?;
std::fs::write(
skill_dir.join("SKILL.md"),
format!("---\ndescription: {description}\n---\n\n# {skill_name}\n"),
)?;
if !products.is_empty() {
let products = products
.iter()
.map(|product| format!(" - {product}"))
.collect::<Vec<_>>()
.join("\n");
std::fs::create_dir_all(skill_dir.join("agents"))?;
std::fs::write(
skill_dir.join("agents/openai.yaml"),
format!("policy:\n products:\n{products}\n"),
)?;
}
Ok(())
}
@@ -377,179 +377,6 @@ enabled = false
Ok(())
}
#[tokio::test]
async fn plugin_list_filters_plugins_for_custom_session_source_products() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "all-products",
"source": {
"source": "local",
"path": "./all-products"
}
},
{
"name": "chatgpt-only",
"source": {
"source": "local",
"path": "./chatgpt-only"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["CHATGPT"]
}
},
{
"name": "atlas-only",
"source": {
"source": "local",
"path": "./atlas-only"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["ATLAS"]
}
},
{
"name": "codex-only",
"source": {
"source": "local",
"path": "./codex-only"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["CODEX"]
}
}
]
}"#,
)?;
let mut mcp =
McpProcess::new_with_args(codex_home.path(), &["--session-source", "chatgpt"]).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
let marketplace = response
.marketplaces
.into_iter()
.find(|marketplace| marketplace.name == "codex-curated")
.expect("expected marketplace entry");
assert_eq!(
marketplace
.plugins
.into_iter()
.map(|plugin| plugin.name)
.collect::<Vec<_>>(),
vec!["all-products".to_string(), "chatgpt-only".to_string()]
);
Ok(())
}
#[tokio::test]
async fn plugin_list_defaults_non_custom_session_source_to_codex_products() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "all-products",
"source": {
"source": "local",
"path": "./all-products"
}
},
{
"name": "chatgpt-only",
"source": {
"source": "local",
"path": "./chatgpt-only"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["CHATGPT"]
}
},
{
"name": "codex-only",
"source": {
"source": "local",
"path": "./codex-only"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["CODEX"]
}
}
]
}"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
let marketplace = response
.marketplaces
.into_iter()
.find(|marketplace| marketplace.name == "codex-curated")
.expect("expected marketplace entry");
assert_eq!(
marketplace
.plugins
.into_iter()
.map(|plugin| plugin.name)
.collect::<Vec<_>>(),
vec!["all-products".to_string(), "codex-only".to_string()]
);
Ok(())
}
#[tokio::test]
async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Result<()> {
let codex_home = TempDir::new()?;
-24
View File
@@ -331,17 +331,6 @@ struct AppServerCommand {
)]
listen: codex_app_server::AppServerTransport,
/// Session source stamped into new threads started by this app-server.
///
/// Known values such as `vscode`, `cli`, `exec`, and `mcp` map to built-in
/// sources. Any other non-empty value is recorded as a custom source.
#[arg(
long = "session-source",
value_name = "SOURCE",
default_value = "vscode"
)]
session_source: String,
/// Controls whether analytics are enabled by default.
///
/// Analytics are disabled by default for app-server. Users have to explicitly opt in
@@ -654,17 +643,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
None => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app-server")?;
let transport = app_server_cli.listen;
let session_source = codex_protocol::protocol::SessionSource::from_startup_arg(
app_server_cli.session_source.as_str(),
)
.map_err(|err| anyhow::anyhow!("invalid --session-source: {err}"))?;
codex_app_server::run_main_with_transport(
arg0_paths.clone(),
root_config_overrides,
codex_core::config_loader::LoaderOverrides::default(),
app_server_cli.analytics_default_enabled,
transport,
session_source,
)
.await?;
}
@@ -1631,7 +1615,6 @@ mod tests {
app_server.listen,
codex_app_server::AppServerTransport::Stdio
);
assert_eq!(app_server.session_source, "vscode");
}
#[test]
@@ -1641,13 +1624,6 @@ mod tests {
assert!(app_server.analytics_default_enabled);
}
#[test]
fn app_server_session_source_accepts_custom_value() {
let app_server =
app_server_from_args(["codex", "app-server", "--session-source", "atlas"].as_ref());
assert_eq!(app_server.session_source, "atlas");
}
#[test]
fn remote_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "ws://127.0.0.1:4500"])
+6 -14
View File
@@ -2385,12 +2385,11 @@ impl Session {
&per_turn_config,
)
.await;
let skills_outcome = Arc::new(crate::skills::filter_skill_load_outcome_for_session_source(
let skills_outcome = Arc::new(
self.services
.skills_manager
.skills_for_config(&per_turn_config),
&session_configuration.session_source,
));
);
let mut turn_context: TurnContext = Self::make_turn_context(
Some(Arc::clone(&self.services.auth_manager)),
&self.services.session_telemetry,
@@ -4774,24 +4773,17 @@ mod handlers {
cwds: Vec<PathBuf>,
force_reload: bool,
) {
let (cwds, session_source) = if cwds.is_empty() {
let cwds = if cwds.is_empty() {
let state = sess.state.lock().await;
(
vec![state.session_configuration.cwd.clone()],
state.session_configuration.session_source.clone(),
)
vec![state.session_configuration.cwd.clone()]
} else {
let state = sess.state.lock().await;
(cwds, state.session_configuration.session_source.clone())
cwds
};
let skills_manager = &sess.services.skills_manager;
let mut skills = Vec::new();
for cwd in cwds {
let outcome = crate::skills::filter_skill_load_outcome_for_session_source(
skills_manager.skills_for_cwd(&cwd, force_reload).await,
&session_source,
);
let outcome = skills_manager.skills_for_cwd(&cwd, force_reload).await;
let errors = super::errors_to_info(&outcome.errors);
let skills_metadata = super::skills_to_info(&outcome.skills, &outcome.disabled_paths);
skills.push(SkillsListEntry {
+5 -35
View File
@@ -44,7 +44,6 @@ use crate::skills::loader::SkillRoot;
use crate::skills::loader::load_skills_from_roots;
use codex_app_server_protocol::ConfigValueWriteParams;
use codex_app_server_protocol::MergeStrategy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
@@ -939,11 +938,7 @@ impl PluginsManager {
})
}
pub fn maybe_start_curated_repo_sync_for_config(
self: &Arc<Self>,
config: &Config,
session_source: &SessionSource,
) {
pub fn maybe_start_curated_repo_sync_for_config(self: &Arc<Self>, config: &Config) {
if plugins_feature_enabled_from_stack(&config.config_layer_stack) {
let mut configured_curated_plugin_ids =
configured_plugins_from_stack(&config.config_layer_stack)
@@ -966,15 +961,11 @@ impl PluginsManager {
})
.collect::<Vec<_>>();
configured_curated_plugin_ids.sort_unstable_by_key(super::store::PluginId::as_key);
self.start_curated_repo_sync(configured_curated_plugin_ids, session_source.clone());
self.start_curated_repo_sync(configured_curated_plugin_ids);
}
}
fn start_curated_repo_sync(
self: &Arc<Self>,
configured_curated_plugin_ids: Vec<PluginId>,
session_source: SessionSource,
) {
fn start_curated_repo_sync(self: &Arc<Self>, configured_curated_plugin_ids: Vec<PluginId>) {
if CURATED_REPO_SYNC_STARTED.swap(true, Ordering::SeqCst) {
return;
}
@@ -989,7 +980,6 @@ impl PluginsManager {
codex_home.as_path(),
&curated_plugin_version,
&configured_curated_plugin_ids,
&session_source,
) {
Ok(cache_refreshed) => {
if cache_refreshed {
@@ -1215,7 +1205,6 @@ fn refresh_curated_plugin_cache(
codex_home: &Path,
plugin_version: &str,
configured_curated_plugin_ids: &[PluginId],
session_source: &SessionSource,
) -> Result<bool, String> {
let store = PluginStore::new(codex_home.to_path_buf());
let curated_marketplace_path = AbsolutePathBuf::try_from(
@@ -1226,12 +1215,9 @@ fn refresh_curated_plugin_cache(
.map_err(|err| format!("failed to load curated marketplace for cache refresh: {err}"))?;
let mut plugin_sources = HashMap::<String, AbsolutePathBuf>::new();
let mut product_restricted_plugin_names = HashSet::<String>::new();
for plugin in curated_marketplace.plugins {
let plugin_name = plugin.name;
if plugin_sources.contains_key(&plugin_name)
|| product_restricted_plugin_names.contains(&plugin_name)
{
if plugin_sources.contains_key(&plugin_name) {
warn!(
plugin = plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
@@ -1242,32 +1228,16 @@ fn refresh_curated_plugin_cache(
let source_path = match plugin.source {
MarketplacePluginSource::Local { path } => path,
};
if session_source.matches_product_restriction(&plugin.policy.products) {
plugin_sources.insert(plugin_name, source_path);
} else {
product_restricted_plugin_names.insert(plugin_name);
}
plugin_sources.insert(plugin_name, source_path);
}
let mut cache_refreshed = false;
for plugin_id in configured_curated_plugin_ids {
// Curated plugin cache entries are intentionally sticky across session source changes.
// Product restrictions gate refresh for this source, but do not retroactively evict an
// already-active cache entry from a shared CODEX_HOME.
if store.active_plugin_version(plugin_id).as_deref() == Some(plugin_version) {
continue;
}
let Some(source_path) = plugin_sources.get(&plugin_id.plugin_name).cloned() else {
if product_restricted_plugin_names.contains(&plugin_id.plugin_name) {
info!(
plugin = plugin_id.plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
session_source = %session_source,
"skipping curated plugin cache refresh for product-restricted plugin"
);
continue;
}
warn!(
plugin = plugin_id.plugin_name,
marketplace = OPENAI_CURATED_MARKETPLACE_NAME,
+6 -80
View File
@@ -9,12 +9,10 @@ use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::plugins::MarketplacePluginInstallPolicy;
use crate::plugins::test_support::TEST_CURATED_PLUGIN_SHA;
use crate::plugins::test_support::write_curated_plugin;
use crate::plugins::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha;
use crate::plugins::test_support::write_file;
use crate::plugins::test_support::write_openai_curated_marketplace;
use codex_app_server_protocol::ConfigLayerSource;
use codex_protocol::protocol::SessionSource;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::TempDir;
@@ -1034,12 +1032,6 @@ async fn list_marketplaces_includes_curated_repo_marketplace() {
r#"{"name":"linear"}"#,
)
.unwrap();
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
"#,
);
let config = load_config(tmp.path(), tmp.path()).await;
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
@@ -1635,13 +1627,8 @@ fn refresh_curated_plugin_cache_replaces_existing_local_version_with_sha() {
);
assert!(
refresh_curated_plugin_cache(
tmp.path(),
TEST_CURATED_PLUGIN_SHA,
&[plugin_id],
&SessionSource::Cli,
)
.expect("cache refresh should succeed")
refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
.expect("cache refresh should succeed")
);
assert!(
@@ -1671,13 +1658,8 @@ fn refresh_curated_plugin_cache_reinstalls_missing_configured_plugin_with_curren
.unwrap();
assert!(
refresh_curated_plugin_cache(
tmp.path(),
TEST_CURATED_PLUGIN_SHA,
&[plugin_id],
&SessionSource::Cli,
)
.expect("cache refresh should recreate missing configured plugin")
refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
.expect("cache refresh should recreate missing configured plugin")
);
assert!(
@@ -1706,64 +1688,8 @@ fn refresh_curated_plugin_cache_returns_false_when_configured_plugins_are_curren
);
assert!(
!refresh_curated_plugin_cache(
tmp.path(),
TEST_CURATED_PLUGIN_SHA,
&[plugin_id],
&SessionSource::Cli,
)
.expect("cache refresh should be a no-op when configured plugins are current")
);
}
#[test]
fn refresh_curated_plugin_cache_skips_product_restricted_plugins_for_session_source() {
let tmp = tempfile::tempdir().unwrap();
let curated_root = curated_plugins_repo_path(tmp.path());
write_file(
&curated_root.join(".agents/plugins/marketplace.json"),
&format!(
r#"{{
"name": "{OPENAI_CURATED_MARKETPLACE_NAME}",
"plugins": [
{{
"name": "chatgpt-plugin",
"source": {{
"source": "local",
"path": "./plugins/chatgpt-plugin"
}},
"policy": {{
"products": ["CHATGPT"]
}}
}}
]
}}"#
),
);
write_curated_plugin(&curated_root, "chatgpt-plugin");
write_curated_plugin_sha(tmp.path(), TEST_CURATED_PLUGIN_SHA);
let plugin_id = PluginId::new(
"chatgpt-plugin".to_string(),
OPENAI_CURATED_MARKETPLACE_NAME.to_string(),
)
.unwrap();
assert!(
!refresh_curated_plugin_cache(
tmp.path(),
TEST_CURATED_PLUGIN_SHA,
&[plugin_id],
&SessionSource::Cli,
)
.expect("cache refresh should skip disallowed product plugin")
);
assert!(
!tmp.path()
.join(format!(
"plugins/cache/openai-curated/chatgpt-plugin/{TEST_CURATED_PLUGIN_SHA}"
))
.exists()
!refresh_curated_plugin_cache(tmp.path(), TEST_CURATED_PLUGIN_SHA, &[plugin_id])
.expect("cache refresh should be a no-op when configured plugins are current")
);
}
+2
View File
@@ -54,6 +54,8 @@ pub enum MarketplacePluginSource {
pub struct MarketplacePluginPolicy {
pub installation: MarketplacePluginInstallPolicy,
pub authentication: MarketplacePluginAuthPolicy,
// TODO: Surface or enforce product gating at the Codex/plugin consumer boundary instead of
// only carrying it through core marketplace metadata.
pub products: Vec<Product>,
}
-2
View File
@@ -20,6 +20,4 @@ pub use model::SkillError;
pub use model::SkillLoadOutcome;
pub use model::SkillMetadata;
pub use model::SkillPolicy;
pub use model::filter_skill_load_outcome_for_session_source;
pub use model::filter_skills_for_session_source;
pub use render::render_skills_section;
+2 -44
View File
@@ -5,7 +5,6 @@ use std::sync::Arc;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillScope;
use serde::Deserialize;
@@ -43,18 +42,13 @@ impl SkillMetadata {
.and_then(|policy| policy.allow_implicit_invocation)
.unwrap_or(true)
}
pub fn matches_product_restriction(&self, session_source: &SessionSource) -> bool {
match &self.policy {
Some(policy) => session_source.matches_product_restriction(&policy.products),
None => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SkillPolicy {
pub allow_implicit_invocation: Option<bool>,
// TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and
// storing this metadata.
pub products: Vec<Product>,
}
@@ -121,39 +115,3 @@ impl SkillLoadOutcome {
.map(|skill| (skill, self.is_skill_enabled(skill)))
}
}
pub fn filter_skill_load_outcome_for_session_source(
mut outcome: SkillLoadOutcome,
session_source: &SessionSource,
) -> SkillLoadOutcome {
outcome
.skills
.retain(|skill| skill.matches_product_restriction(session_source));
outcome.implicit_skills_by_scripts_dir = Arc::new(
outcome
.implicit_skills_by_scripts_dir
.iter()
.filter(|(_, skill)| skill.matches_product_restriction(session_source))
.map(|(path, skill)| (path.clone(), skill.clone()))
.collect(),
);
outcome.implicit_skills_by_doc_path = Arc::new(
outcome
.implicit_skills_by_doc_path
.iter()
.filter(|(_, skill)| skill.matches_product_restriction(session_source))
.map(|(path, skill)| (path.clone(), skill.clone()))
.collect(),
);
outcome
}
pub fn filter_skills_for_session_source(
skills: Vec<SkillMetadata>,
session_source: &SessionSource,
) -> Vec<SkillMetadata> {
skills
.into_iter()
.filter(|skill| skill.matches_product_restriction(session_source))
.collect()
}
@@ -276,7 +276,7 @@ impl SessionTelemetry {
account_email,
originator: sanitize_metric_tag_value(originator.as_str()),
service_name: None,
session_source: sanitize_metric_tag_value(session_source.to_string().as_str()),
session_source: session_source.to_string(),
model: model.to_owned(),
slug: slug.to_owned(),
log_user_prompts,
-136
View File
@@ -2272,7 +2272,6 @@ pub enum SessionSource {
VSCode,
Exec,
Mcp,
Custom(String),
SubAgent(SubAgentSource),
#[serde(other)]
Unknown,
@@ -2303,7 +2302,6 @@ impl fmt::Display for SessionSource {
SessionSource::VSCode => f.write_str("vscode"),
SessionSource::Exec => f.write_str("exec"),
SessionSource::Mcp => f.write_str("mcp"),
SessionSource::Custom(source) => f.write_str(source),
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
SessionSource::Unknown => f.write_str("unknown"),
}
@@ -2311,23 +2309,6 @@ impl fmt::Display for SessionSource {
}
impl SessionSource {
pub fn from_startup_arg(value: &str) -> Result<Self, &'static str> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err("session source must not be empty");
}
let normalized = trimmed.to_ascii_lowercase();
Ok(match normalized.as_str() {
"cli" => SessionSource::Cli,
"vscode" => SessionSource::VSCode,
"exec" => SessionSource::Exec,
"mcp" | "appserver" | "app-server" | "app_server" => SessionSource::Mcp,
"unknown" => SessionSource::Unknown,
_ => SessionSource::Custom(trimmed.to_string()),
})
}
pub fn get_nickname(&self) -> Option<String> {
match self {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
@@ -2351,25 +2332,6 @@ impl SessionSource {
_ => None,
}
}
pub fn restriction_product(&self) -> Option<Product> {
match self {
SessionSource::Custom(source) => Product::from_session_source_name(source),
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::SubAgent(_)
| SessionSource::Unknown => Some(Product::Codex),
}
}
pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
products.is_empty()
|| self
.restriction_product()
.is_some_and(|product| products.contains(&product))
}
}
impl fmt::Display for SubAgentSource {
@@ -2961,18 +2923,6 @@ pub enum Product {
#[serde(alias = "ATLAS")]
Atlas,
}
impl Product {
pub fn from_session_source_name(value: &str) -> Option<Self> {
let normalized = value.trim().to_ascii_lowercase();
match normalized.as_str() {
"chatgpt" => Some(Self::Chatgpt),
"codex" => Some(Self::Codex),
"atlas" => Some(Self::Atlas),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
@@ -3473,92 +3423,6 @@ mod tests {
.any(|root| root.is_path_writable(path))
}
#[test]
fn session_source_from_startup_arg_maps_known_values() {
assert_eq!(
SessionSource::from_startup_arg("vscode").unwrap(),
SessionSource::VSCode
);
assert_eq!(
SessionSource::from_startup_arg("app-server").unwrap(),
SessionSource::Mcp
);
}
#[test]
fn session_source_from_startup_arg_preserves_custom_values() {
assert_eq!(
SessionSource::from_startup_arg("atlas").unwrap(),
SessionSource::Custom("atlas".to_string())
);
}
#[test]
fn session_source_restriction_product_defaults_non_custom_sources_to_codex() {
assert_eq!(
SessionSource::Cli.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::VSCode.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Exec.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Mcp.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::SubAgent(SubAgentSource::Review).restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Unknown.restriction_product(),
Some(Product::Codex)
);
}
#[test]
fn session_source_restriction_product_maps_custom_sources_to_products() {
assert_eq!(
SessionSource::Custom("chatgpt".to_string()).restriction_product(),
Some(Product::Chatgpt)
);
assert_eq!(
SessionSource::Custom("ATLAS".to_string()).restriction_product(),
Some(Product::Atlas)
);
assert_eq!(
SessionSource::Custom("codex".to_string()).restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Custom("atlas-dev".to_string()).restriction_product(),
None
);
}
#[test]
fn session_source_matches_product_restriction() {
assert!(
SessionSource::Custom("chatgpt".to_string())
.matches_product_restriction(&[Product::Chatgpt])
);
assert!(
!SessionSource::Custom("chatgpt".to_string())
.matches_product_restriction(&[Product::Codex])
);
assert!(SessionSource::VSCode.matches_product_restriction(&[Product::Codex]));
assert!(
!SessionSource::Custom("atlas-dev".to_string())
.matches_product_restriction(&[Product::Atlas])
);
assert!(SessionSource::Custom("atlas-dev".to_string()).matches_product_restriction(&[]));
}
fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
let mut paths = vec![cwd.to_path_buf()];
paths.extend(
+1 -1
View File
@@ -2018,7 +2018,7 @@ impl App {
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config, &SessionSource::Cli);
.maybe_start_curated_repo_sync_for_config(&config);
let mut model = thread_manager
.get_models_manager()
.get_default_model(&config.model, RefreshStrategy::Offline)
@@ -3131,7 +3131,6 @@ class ThreadSourceKind(Enum):
vscode = "vscode"
exec = "exec"
app_server = "appServer"
custom = "custom"
sub_agent = "subAgent"
sub_agent_review = "subAgentReview"
sub_agent_compact = "subAgentCompact"
@@ -5811,19 +5810,11 @@ class SubAgentSessionSource(BaseModel):
sub_agent: Annotated[SubAgentSource, Field(alias="subAgent")]
class CustomSessionSource(BaseModel):
model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
)
custom: str
class SessionSource(RootModel[SessionSourceValue | CustomSessionSource | SubAgentSessionSource]):
class SessionSource(RootModel[SessionSourceValue | SubAgentSessionSource]):
model_config = ConfigDict(
populate_by_name=True,
)
root: SessionSourceValue | CustomSessionSource | SubAgentSessionSource
root: SessionSourceValue | SubAgentSessionSource
class Thread(BaseModel):