mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
e5de13644d
commit
86982ca1f9
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user