mirror of
https://github.com/openai/codex.git
synced 2026-11-10 00:42:33 +00:00
feat: support product-scoped plugins. (#15041)
1. Added SessionSource::Custom(String) and --session-source. 2. Enforced plugin and skill products by session_source. 3. Applied the same filtering to curated background refresh.
This commit is contained in:
1 parent
01df50cf42
commit
db5781a088
35 files changed
+652
-38
No files matched your search
@@ -1900,6 +1900,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -11035,6 +11035,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -8795,6 +8795,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -835,6 +835,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -835,6 +835,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -835,6 +835,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -593,6 +593,19 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"custom": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"custom"
|
||||||
|
],
|
||||||
|
"title": "CustomSessionSource",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"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.
|
// 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";
|
import type { SubAgentSource } from "./SubAgentSource";
|
||||||
|
|
||||||
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "subagent": SubAgentSource } | "unknown";
|
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "custom": string } | { "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.
|
// 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";
|
import type { SubAgentSource } from "../SubAgentSource";
|
||||||
|
|
||||||
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "subAgent": SubAgentSource } | "unknown";
|
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "custom": string } | { "subAgent": SubAgentSource } | "unknown";
|
||||||
@@ -1469,6 +1469,7 @@ pub enum SessionSource {
|
|||||||
VsCode,
|
VsCode,
|
||||||
Exec,
|
Exec,
|
||||||
AppServer,
|
AppServer,
|
||||||
|
Custom(String),
|
||||||
SubAgent(CoreSubAgentSource),
|
SubAgent(CoreSubAgentSource),
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -1481,6 +1482,7 @@ impl From<CoreSessionSource> for SessionSource {
|
|||||||
CoreSessionSource::VSCode => SessionSource::VsCode,
|
CoreSessionSource::VSCode => SessionSource::VsCode,
|
||||||
CoreSessionSource::Exec => SessionSource::Exec,
|
CoreSessionSource::Exec => SessionSource::Exec,
|
||||||
CoreSessionSource::Mcp => SessionSource::AppServer,
|
CoreSessionSource::Mcp => SessionSource::AppServer,
|
||||||
|
CoreSessionSource::Custom(source) => SessionSource::Custom(source),
|
||||||
CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub),
|
CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub),
|
||||||
CoreSessionSource::Unknown => SessionSource::Unknown,
|
CoreSessionSource::Unknown => SessionSource::Unknown,
|
||||||
}
|
}
|
||||||
@@ -1494,6 +1496,7 @@ impl From<SessionSource> for CoreSessionSource {
|
|||||||
SessionSource::VsCode => CoreSessionSource::VSCode,
|
SessionSource::VsCode => CoreSessionSource::VSCode,
|
||||||
SessionSource::Exec => CoreSessionSource::Exec,
|
SessionSource::Exec => CoreSessionSource::Exec,
|
||||||
SessionSource::AppServer => CoreSessionSource::Mcp,
|
SessionSource::AppServer => CoreSessionSource::Mcp,
|
||||||
|
SessionSource::Custom(source) => CoreSessionSource::Custom(source),
|
||||||
SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub),
|
SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub),
|
||||||
SessionSource::Unknown => CoreSessionSource::Unknown,
|
SessionSource::Unknown => CoreSessionSource::Unknown,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5631,6 +5631,17 @@ impl CodexMessageProcessor {
|
|||||||
};
|
};
|
||||||
let app_summaries =
|
let app_summaries =
|
||||||
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
|
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
|
||||||
|
let visible_skills = outcome
|
||||||
|
.plugin
|
||||||
|
.skills
|
||||||
|
.iter()
|
||||||
|
.filter(|skill| {
|
||||||
|
skill.matches_product_restriction_for_product(
|
||||||
|
self.thread_manager.session_source().restriction_product(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let plugin = PluginDetail {
|
let plugin = PluginDetail {
|
||||||
marketplace_name: outcome.marketplace_name,
|
marketplace_name: outcome.marketplace_name,
|
||||||
marketplace_path: outcome.marketplace_path,
|
marketplace_path: outcome.marketplace_path,
|
||||||
@@ -5645,7 +5656,7 @@ impl CodexMessageProcessor {
|
|||||||
interface: outcome.plugin.interface.map(plugin_interface_to_info),
|
interface: outcome.plugin.interface.map(plugin_interface_to_info),
|
||||||
},
|
},
|
||||||
description: outcome.plugin.description,
|
description: outcome.plugin.description,
|
||||||
skills: plugin_skills_to_info(&outcome.plugin.skills),
|
skills: plugin_skills_to_info(&visible_skills),
|
||||||
apps: app_summaries,
|
apps: app_summaries,
|
||||||
mcp_servers: outcome.plugin.mcp_server_names,
|
mcp_servers: outcome.plugin.mcp_server_names,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ pub async fn run_main(
|
|||||||
loader_overrides,
|
loader_overrides,
|
||||||
default_analytics_enabled,
|
default_analytics_enabled,
|
||||||
AppServerTransport::Stdio,
|
AppServerTransport::Stdio,
|
||||||
|
SessionSource::VSCode,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -346,6 +347,7 @@ pub async fn run_main_with_transport(
|
|||||||
loader_overrides: LoaderOverrides,
|
loader_overrides: LoaderOverrides,
|
||||||
default_analytics_enabled: bool,
|
default_analytics_enabled: bool,
|
||||||
transport: AppServerTransport,
|
transport: AppServerTransport,
|
||||||
|
session_source: SessionSource,
|
||||||
) -> IoResult<()> {
|
) -> IoResult<()> {
|
||||||
let (transport_event_tx, mut transport_event_rx) =
|
let (transport_event_tx, mut transport_event_rx) =
|
||||||
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
|
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
|
||||||
@@ -621,7 +623,7 @@ pub async fn run_main_with_transport(
|
|||||||
feedback: feedback.clone(),
|
feedback: feedback.clone(),
|
||||||
log_db,
|
log_db,
|
||||||
config_warnings,
|
config_warnings,
|
||||||
session_source: SessionSource::VSCode,
|
session_source,
|
||||||
enable_codex_api_key_env: false,
|
enable_codex_api_key_env: false,
|
||||||
});
|
});
|
||||||
let mut thread_created_rx = processor.thread_created_receiver();
|
let mut thread_created_rx = processor.thread_created_receiver();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use codex_app_server::run_main_with_transport;
|
|||||||
use codex_arg0::Arg0DispatchPaths;
|
use codex_arg0::Arg0DispatchPaths;
|
||||||
use codex_arg0::arg0_dispatch_or_else;
|
use codex_arg0::arg0_dispatch_or_else;
|
||||||
use codex_core::config_loader::LoaderOverrides;
|
use codex_core::config_loader::LoaderOverrides;
|
||||||
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -21,6 +22,15 @@ struct AppServerArgs {
|
|||||||
default_value = AppServerTransport::DEFAULT_LISTEN_URL
|
default_value = AppServerTransport::DEFAULT_LISTEN_URL
|
||||||
)]
|
)]
|
||||||
listen: AppServerTransport,
|
listen: AppServerTransport,
|
||||||
|
|
||||||
|
/// Session source used to derive product restrictions and metadata.
|
||||||
|
#[arg(
|
||||||
|
long = "session-source",
|
||||||
|
value_name = "SOURCE",
|
||||||
|
default_value = "vscode",
|
||||||
|
value_parser = SessionSource::from_startup_arg
|
||||||
|
)]
|
||||||
|
session_source: SessionSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
@@ -32,6 +42,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let transport = args.listen;
|
let transport = args.listen;
|
||||||
|
let session_source = args.session_source;
|
||||||
|
|
||||||
run_main_with_transport(
|
run_main_with_transport(
|
||||||
arg0_paths,
|
arg0_paths,
|
||||||
@@ -39,6 +50,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
loader_overrides,
|
loader_overrides,
|
||||||
/*default_analytics_enabled*/ false,
|
/*default_analytics_enabled*/ false,
|
||||||
transport,
|
transport,
|
||||||
|
session_source,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
|||||||
|
|
||||||
impl McpProcess {
|
impl McpProcess {
|
||||||
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
|
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
|
||||||
Self::new_with_env(codex_home, &[]).await
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new MCP process, allowing tests to override or remove
|
/// Creates a new MCP process, allowing tests to override or remove
|
||||||
@@ -107,6 +111,14 @@ impl McpProcess {
|
|||||||
pub async fn new_with_env(
|
pub async fn new_with_env(
|
||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
env_overrides: &[(&str, Option<&str>)],
|
env_overrides: &[(&str, Option<&str>)],
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new_with_env_and_args(
|
||||||
|
codex_home: &Path,
|
||||||
|
env_overrides: &[(&str, Option<&str>)],
|
||||||
|
args: &[&str],
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||||
.context("should find binary for codex-app-server")?;
|
.context("should find binary for codex-app-server")?;
|
||||||
@@ -119,6 +131,7 @@ impl McpProcess {
|
|||||||
cmd.env("CODEX_HOME", codex_home);
|
cmd.env("CODEX_HOME", codex_home);
|
||||||
cmd.env("RUST_LOG", "info");
|
cmd.env("RUST_LOG", "info");
|
||||||
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
|
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
|
||||||
|
cmd.args(args);
|
||||||
|
|
||||||
for (k, v) in env_overrides {
|
for (k, v) in env_overrides {
|
||||||
match v {
|
match v {
|
||||||
|
|||||||
@@ -146,6 +146,56 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let repo_root = TempDir::new()?;
|
||||||
|
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||||
|
std::fs::write(
|
||||||
|
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||||
|
r#"{
|
||||||
|
"name": "debug",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "sample-plugin",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./sample-plugin"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"products": ["CHATGPT"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)?;
|
||||||
|
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
|
||||||
|
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", "atlas"]).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 err = timeout(
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
assert_eq!(err.error.code, -32600);
|
||||||
|
assert!(err.error.message.contains("not available for install"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_install() -> Result<()>
|
async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_install() -> Result<()>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
|
|||||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||||
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?;
|
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?;
|
||||||
|
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only"))?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||||
r#"{
|
r#"{
|
||||||
@@ -79,6 +80,32 @@ description: Summarize email threads
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Thread Summarizer
|
# Thread Summarizer
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join("skills/chatgpt-only/SKILL.md"),
|
||||||
|
r#"---
|
||||||
|
name: chatgpt-only
|
||||||
|
description: Visible only for ChatGPT
|
||||||
|
---
|
||||||
|
|
||||||
|
# ChatGPT Only
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer/agents"))?;
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join("skills/thread-summarizer/agents/openai.yaml"),
|
||||||
|
r#"policy:
|
||||||
|
products:
|
||||||
|
- CODEX
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only/agents"))?;
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join("skills/chatgpt-only/agents/openai.yaml"),
|
||||||
|
r#"policy:
|
||||||
|
products:
|
||||||
|
- CHATGPT
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
|||||||
@@ -649,6 +649,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
codex_core::config_loader::LoaderOverrides::default(),
|
codex_core::config_loader::LoaderOverrides::default(),
|
||||||
app_server_cli.analytics_default_enabled,
|
app_server_cli.analytics_default_enabled,
|
||||||
transport,
|
transport,
|
||||||
|
codex_protocol::protocol::SessionSource::VSCode,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use crate::skills::loader::SkillRoot;
|
|||||||
use crate::skills::loader::load_skills_from_roots;
|
use crate::skills::loader::load_skills_from_roots;
|
||||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||||
use codex_app_server_protocol::MergeStrategy;
|
use codex_app_server_protocol::MergeStrategy;
|
||||||
|
use codex_protocol::protocol::Product;
|
||||||
use codex_protocol::protocol::SkillScope;
|
use codex_protocol::protocol::SkillScope;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -461,16 +462,32 @@ pub struct PluginsManager {
|
|||||||
store: PluginStore,
|
store: PluginStore,
|
||||||
featured_plugin_ids_cache: RwLock<Option<CachedFeaturedPluginIds>>,
|
featured_plugin_ids_cache: RwLock<Option<CachedFeaturedPluginIds>>,
|
||||||
cached_enabled_outcome: RwLock<Option<PluginLoadOutcome>>,
|
cached_enabled_outcome: RwLock<Option<PluginLoadOutcome>>,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,
|
analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PluginsManager {
|
impl PluginsManager {
|
||||||
pub fn new(codex_home: PathBuf) -> Self {
|
pub fn new(codex_home: PathBuf) -> Self {
|
||||||
|
Self::new_with_restriction_product(codex_home, Some(Product::Codex))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_restriction_product(
|
||||||
|
codex_home: PathBuf,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
|
) -> Self {
|
||||||
|
// Product restrictions are enforced at marketplace admission time for a given CODEX_HOME:
|
||||||
|
// listing, install, and curated refresh all consult this restriction context before new
|
||||||
|
// plugins enter local config or cache. After admission, runtime plugin loading trusts the
|
||||||
|
// contents of that CODEX_HOME and does not re-filter configured plugins by product, so
|
||||||
|
// already-admitted plugins may continue exposing MCP servers/tools from shared local state.
|
||||||
|
//
|
||||||
|
// This assumes a single CODEX_HOME is only used by one product.
|
||||||
Self {
|
Self {
|
||||||
codex_home: codex_home.clone(),
|
codex_home: codex_home.clone(),
|
||||||
store: PluginStore::new(codex_home),
|
store: PluginStore::new(codex_home),
|
||||||
featured_plugin_ids_cache: RwLock::new(None),
|
featured_plugin_ids_cache: RwLock::new(None),
|
||||||
cached_enabled_outcome: RwLock::new(None),
|
cached_enabled_outcome: RwLock::new(None),
|
||||||
|
restriction_product,
|
||||||
analytics_events_client: RwLock::new(None),
|
analytics_events_client: RwLock::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,6 +500,13 @@ impl PluginsManager {
|
|||||||
*stored_client = Some(analytics_events_client);
|
*stored_client = Some(analytics_events_client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn restriction_product_matches(&self, products: &[Product]) -> bool {
|
||||||
|
products.is_empty()
|
||||||
|
|| self
|
||||||
|
.restriction_product
|
||||||
|
.is_some_and(|product| product.matches_product_restriction(products))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn plugins_for_config(&self, config: &Config) -> PluginLoadOutcome {
|
pub fn plugins_for_config(&self, config: &Config) -> PluginLoadOutcome {
|
||||||
self.plugins_for_config_with_force_reload(config, /*force_reload*/ false)
|
self.plugins_for_config_with_force_reload(config, /*force_reload*/ false)
|
||||||
}
|
}
|
||||||
@@ -600,7 +624,11 @@ impl PluginsManager {
|
|||||||
&self,
|
&self,
|
||||||
request: PluginInstallRequest,
|
request: PluginInstallRequest,
|
||||||
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
||||||
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
let resolved = resolve_marketplace_plugin(
|
||||||
|
&request.marketplace_path,
|
||||||
|
&request.plugin_name,
|
||||||
|
self.restriction_product,
|
||||||
|
)?;
|
||||||
self.install_resolved_plugin(resolved).await
|
self.install_resolved_plugin(resolved).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +638,11 @@ impl PluginsManager {
|
|||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
request: PluginInstallRequest,
|
request: PluginInstallRequest,
|
||||||
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
||||||
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
let resolved = resolve_marketplace_plugin(
|
||||||
|
&request.marketplace_path,
|
||||||
|
&request.plugin_name,
|
||||||
|
self.restriction_product,
|
||||||
|
)?;
|
||||||
let plugin_id = resolved.plugin_id.as_key();
|
let plugin_id = resolved.plugin_id.as_key();
|
||||||
// This only forwards the backend mutation before the local install flow. We rely on
|
// This only forwards the backend mutation before the local install flow. We rely on
|
||||||
// `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra
|
// `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra
|
||||||
@@ -775,6 +807,7 @@ impl PluginsManager {
|
|||||||
AbsolutePathBuf,
|
AbsolutePathBuf,
|
||||||
Option<bool>,
|
Option<bool>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
|
bool,
|
||||||
)>::new();
|
)>::new();
|
||||||
let mut local_plugin_names = HashSet::new();
|
let mut local_plugin_names = HashSet::new();
|
||||||
for plugin in curated_marketplace.plugins {
|
for plugin in curated_marketplace.plugins {
|
||||||
@@ -797,12 +830,14 @@ impl PluginsManager {
|
|||||||
.get(&plugin_key)
|
.get(&plugin_key)
|
||||||
.map(|plugin| plugin.enabled);
|
.map(|plugin| plugin.enabled);
|
||||||
let installed_version = self.store.active_plugin_version(&plugin_id);
|
let installed_version = self.store.active_plugin_version(&plugin_id);
|
||||||
|
let product_allowed = self.restriction_product_matches(&plugin.policy.products);
|
||||||
local_plugins.push((
|
local_plugins.push((
|
||||||
plugin_name,
|
plugin_name,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
source_path,
|
source_path,
|
||||||
current_enabled,
|
current_enabled,
|
||||||
installed_version,
|
installed_version,
|
||||||
|
product_allowed,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -841,11 +876,20 @@ impl PluginsManager {
|
|||||||
let remote_plugin_count = remote_installed_plugin_names.len();
|
let remote_plugin_count = remote_installed_plugin_names.len();
|
||||||
let local_plugin_count = local_plugins.len();
|
let local_plugin_count = local_plugins.len();
|
||||||
|
|
||||||
for (plugin_name, plugin_id, source_path, current_enabled, installed_version) in
|
for (
|
||||||
local_plugins
|
plugin_name,
|
||||||
|
plugin_id,
|
||||||
|
source_path,
|
||||||
|
current_enabled,
|
||||||
|
installed_version,
|
||||||
|
product_allowed,
|
||||||
|
) in local_plugins
|
||||||
{
|
{
|
||||||
let plugin_key = plugin_id.as_key();
|
let plugin_key = plugin_id.as_key();
|
||||||
let is_installed = installed_version.is_some();
|
let is_installed = installed_version.is_some();
|
||||||
|
if !product_allowed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if remote_installed_plugin_names.contains(&plugin_name) {
|
if remote_installed_plugin_names.contains(&plugin_name) {
|
||||||
if !is_installed {
|
if !is_installed {
|
||||||
installs.push((
|
installs.push((
|
||||||
@@ -947,6 +991,9 @@ impl PluginsManager {
|
|||||||
if !seen_plugin_keys.insert(plugin_key.clone()) {
|
if !seen_plugin_keys.insert(plugin_key.clone()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if !self.restriction_product_matches(&plugin.policy.products) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
Some(ConfiguredMarketplacePlugin {
|
Some(ConfiguredMarketplacePlugin {
|
||||||
// Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate
|
// Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate
|
||||||
@@ -994,6 +1041,12 @@ impl PluginsManager {
|
|||||||
marketplace_name,
|
marketplace_name,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
if !self.restriction_product_matches(&plugin.policy.products) {
|
||||||
|
return Err(MarketplaceError::PluginNotFound {
|
||||||
|
plugin_name: request.plugin_name.clone(),
|
||||||
|
marketplace_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let plugin_id = PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(
|
let plugin_id = PluginId::new(plugin.name.clone(), marketplace.name.clone()).map_err(
|
||||||
|err| match err {
|
|err| match err {
|
||||||
@@ -1017,7 +1070,10 @@ impl PluginsManager {
|
|||||||
path,
|
path,
|
||||||
scope: SkillScope::User,
|
scope: SkillScope::User,
|
||||||
}))
|
}))
|
||||||
.skills;
|
.skills
|
||||||
|
.into_iter()
|
||||||
|
.filter(|skill| skill.matches_product_restriction_for_product(self.restriction_product))
|
||||||
|
.collect();
|
||||||
let apps = load_plugin_apps(source_path.as_path());
|
let apps = load_plugin_apps(source_path.as_path());
|
||||||
let mcp_config_paths = plugin_mcp_config_paths(source_path.as_path(), manifest_paths);
|
let mcp_config_paths = plugin_mcp_config_paths(source_path.as_path(), manifest_paths);
|
||||||
let mut mcp_server_names = Vec::new();
|
let mut mcp_server_names = Vec::new();
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ impl MarketplaceError {
|
|||||||
pub fn resolve_marketplace_plugin(
|
pub fn resolve_marketplace_plugin(
|
||||||
marketplace_path: &AbsolutePathBuf,
|
marketplace_path: &AbsolutePathBuf,
|
||||||
plugin_name: &str,
|
plugin_name: &str,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
|
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
|
||||||
let marketplace = load_raw_marketplace_manifest(marketplace_path)?;
|
let marketplace = load_raw_marketplace_manifest(marketplace_path)?;
|
||||||
let marketplace_name = marketplace.name;
|
let marketplace_name = marketplace.name;
|
||||||
@@ -168,7 +169,10 @@ pub fn resolve_marketplace_plugin(
|
|||||||
..
|
..
|
||||||
} = plugin;
|
} = plugin;
|
||||||
let install_policy = policy.installation;
|
let install_policy = policy.installation;
|
||||||
if install_policy == MarketplacePluginInstallPolicy::NotAvailable {
|
let product_allowed = policy.products.is_empty()
|
||||||
|
|| restriction_product
|
||||||
|
.is_some_and(|product| product.matches_product_restriction(&policy.products));
|
||||||
|
if install_policy == MarketplacePluginInstallPolicy::NotAvailable || !product_allowed {
|
||||||
return Err(MarketplaceError::PluginNotAvailable {
|
return Err(MarketplaceError::PluginNotAvailable {
|
||||||
plugin_name: name,
|
plugin_name: name,
|
||||||
marketplace_name,
|
marketplace_name,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ fn resolve_marketplace_plugin_finds_repo_marketplace_plugin() {
|
|||||||
let resolved = resolve_marketplace_plugin(
|
let resolved = resolve_marketplace_plugin(
|
||||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||||
"local-plugin",
|
"local-plugin",
|
||||||
|
Some(Product::Codex),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ fn resolve_marketplace_plugin_reports_missing_plugin() {
|
|||||||
let err = resolve_marketplace_plugin(
|
let err = resolve_marketplace_plugin(
|
||||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||||
"missing",
|
"missing",
|
||||||
|
Some(Product::Codex),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
@@ -297,6 +299,7 @@ fn list_marketplaces_keeps_distinct_entries_for_same_name() {
|
|||||||
let resolved = resolve_marketplace_plugin(
|
let resolved = resolve_marketplace_plugin(
|
||||||
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
|
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
|
||||||
"local-plugin",
|
"local-plugin",
|
||||||
|
Some(Product::Codex),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -687,6 +690,7 @@ fn resolve_marketplace_plugin_rejects_non_relative_local_paths() {
|
|||||||
let err = resolve_marketplace_plugin(
|
let err = resolve_marketplace_plugin(
|
||||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||||
"local-plugin",
|
"local-plugin",
|
||||||
|
Some(Product::Codex),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
@@ -732,6 +736,7 @@ fn resolve_marketplace_plugin_uses_first_duplicate_entry() {
|
|||||||
let resolved = resolve_marketplace_plugin(
|
let resolved = resolve_marketplace_plugin(
|
||||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||||
"local-plugin",
|
"local-plugin",
|
||||||
|
Some(Product::Codex),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -740,3 +745,42 @@ fn resolve_marketplace_plugin_uses_first_duplicate_entry() {
|
|||||||
AbsolutePathBuf::try_from(repo_root.join("first")).unwrap()
|
AbsolutePathBuf::try_from(repo_root.join("first")).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_marketplace_plugin_rejects_disallowed_product() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let repo_root = tmp.path().join("repo");
|
||||||
|
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||||
|
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
repo_root.join(".agents/plugins/marketplace.json"),
|
||||||
|
r#"{
|
||||||
|
"name": "codex-curated",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "chatgpt-plugin",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugin"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"products": ["CHATGPT"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = resolve_marketplace_plugin(
|
||||||
|
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||||
|
"chatgpt-plugin",
|
||||||
|
Some(Product::Atlas),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"plugin `chatgpt-plugin` is not available for install in marketplace `codex-curated`"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
//! Rollout module: persistence and discovery of session rollout files.
|
//! Rollout module: persistence and discovery of session rollout files.
|
||||||
|
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
|
||||||
pub const SESSIONS_SUBDIR: &str = "sessions";
|
pub const SESSIONS_SUBDIR: &str = "sessions";
|
||||||
pub const ARCHIVED_SESSIONS_SUBDIR: &str = "archived_sessions";
|
pub const ARCHIVED_SESSIONS_SUBDIR: &str = "archived_sessions";
|
||||||
pub const INTERACTIVE_SESSION_SOURCES: &[SessionSource] =
|
pub static INTERACTIVE_SESSION_SOURCES: LazyLock<Vec<SessionSource>> = LazyLock::new(|| {
|
||||||
&[SessionSource::Cli, SessionSource::VSCode];
|
vec![
|
||||||
|
SessionSource::Cli,
|
||||||
|
SessionSource::VSCode,
|
||||||
|
SessionSource::Custom("atlas".to_string()),
|
||||||
|
SessionSource::Custom("chatgpt".to_string()),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
10,
|
10,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -665,7 +665,7 @@ async fn test_pagination_cursor() {
|
|||||||
2,
|
2,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -733,7 +733,7 @@ async fn test_pagination_cursor() {
|
|||||||
2,
|
2,
|
||||||
page1.next_cursor.as_ref(),
|
page1.next_cursor.as_ref(),
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -801,7 +801,7 @@ async fn test_pagination_cursor() {
|
|||||||
2,
|
2,
|
||||||
page2.next_cursor.as_ref(),
|
page2.next_cursor.as_ref(),
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -854,7 +854,7 @@ async fn test_list_threads_scans_past_head_for_user_event() {
|
|||||||
10,
|
10,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -880,7 +880,7 @@ async fn test_get_thread_contents() {
|
|||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -970,7 +970,7 @@ async fn test_base_instructions_missing_in_meta_defaults_to_null() {
|
|||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1013,7 +1013,7 @@ async fn test_base_instructions_present_in_meta_is_preserved() {
|
|||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1064,7 +1064,7 @@ async fn test_created_at_sort_uses_file_mtime_for_updated_at() -> Result<()> {
|
|||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1148,7 +1148,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
|||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::UpdatedAt,
|
ThreadSortKey::UpdatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1188,7 +1188,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
|||||||
2,
|
2,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1256,7 +1256,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
|||||||
2,
|
2,
|
||||||
page1.next_cursor.as_ref(),
|
page1.next_cursor.as_ref(),
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
@@ -1325,7 +1325,7 @@ async fn test_source_filter_excludes_non_matching_sessions() {
|
|||||||
10,
|
10,
|
||||||
None,
|
None,
|
||||||
ThreadSortKey::CreatedAt,
|
ThreadSortKey::CreatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
TEST_PROVIDER,
|
TEST_PROVIDER,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::sync::Arc;
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use codex_app_server_protocol::ConfigLayerSource;
|
use codex_app_server_protocol::ConfigLayerSource;
|
||||||
|
use codex_protocol::protocol::Product;
|
||||||
use codex_protocol::protocol::SkillScope;
|
use codex_protocol::protocol::SkillScope;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
@@ -30,6 +31,7 @@ use crate::skills::system::uninstall_system_skills;
|
|||||||
pub struct SkillsManager {
|
pub struct SkillsManager {
|
||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
plugins_manager: Arc<PluginsManager>,
|
plugins_manager: Arc<PluginsManager>,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
cache_by_cwd: RwLock<HashMap<PathBuf, SkillLoadOutcome>>,
|
cache_by_cwd: RwLock<HashMap<PathBuf, SkillLoadOutcome>>,
|
||||||
cache_by_config: RwLock<HashMap<ConfigSkillsCacheKey, SkillLoadOutcome>>,
|
cache_by_config: RwLock<HashMap<ConfigSkillsCacheKey, SkillLoadOutcome>>,
|
||||||
}
|
}
|
||||||
@@ -39,10 +41,25 @@ impl SkillsManager {
|
|||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
plugins_manager: Arc<PluginsManager>,
|
plugins_manager: Arc<PluginsManager>,
|
||||||
bundled_skills_enabled: bool,
|
bundled_skills_enabled: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self::new_with_restriction_product(
|
||||||
|
codex_home,
|
||||||
|
plugins_manager,
|
||||||
|
bundled_skills_enabled,
|
||||||
|
Some(Product::Codex),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_restriction_product(
|
||||||
|
codex_home: PathBuf,
|
||||||
|
plugins_manager: Arc<PluginsManager>,
|
||||||
|
bundled_skills_enabled: bool,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let manager = Self {
|
let manager = Self {
|
||||||
codex_home,
|
codex_home,
|
||||||
plugins_manager,
|
plugins_manager,
|
||||||
|
restriction_product,
|
||||||
cache_by_cwd: RwLock::new(HashMap::new()),
|
cache_by_cwd: RwLock::new(HashMap::new()),
|
||||||
cache_by_config: RwLock::new(HashMap::new()),
|
cache_by_config: RwLock::new(HashMap::new()),
|
||||||
};
|
};
|
||||||
@@ -69,8 +86,10 @@ impl SkillsManager {
|
|||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome =
|
let outcome = crate::skills::filter_skill_load_outcome_for_product(
|
||||||
finalize_skill_outcome(load_skills_from_roots(roots), &config.config_layer_stack);
|
finalize_skill_outcome(load_skills_from_roots(roots), &config.config_layer_stack),
|
||||||
|
self.restriction_product,
|
||||||
|
);
|
||||||
let mut cache = self
|
let mut cache = self
|
||||||
.cache_by_config
|
.cache_by_config
|
||||||
.write()
|
.write()
|
||||||
@@ -173,8 +192,7 @@ impl SkillsManager {
|
|||||||
scope: SkillScope::User,
|
scope: SkillScope::User,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let outcome = load_skills_from_roots(roots);
|
let outcome = self.build_skill_outcome(roots, &config_layer_stack);
|
||||||
let outcome = finalize_skill_outcome(outcome, &config_layer_stack);
|
|
||||||
let mut cache = self
|
let mut cache = self
|
||||||
.cache_by_cwd
|
.cache_by_cwd
|
||||||
.write()
|
.write()
|
||||||
@@ -183,6 +201,17 @@ impl SkillsManager {
|
|||||||
outcome
|
outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_skill_outcome(
|
||||||
|
&self,
|
||||||
|
roots: Vec<SkillRoot>,
|
||||||
|
config_layer_stack: &crate::config_loader::ConfigLayerStack,
|
||||||
|
) -> SkillLoadOutcome {
|
||||||
|
crate::skills::filter_skill_load_outcome_for_product(
|
||||||
|
finalize_skill_outcome(load_skills_from_roots(roots), config_layer_stack),
|
||||||
|
self.restriction_product,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_cache(&self) {
|
pub fn clear_cache(&self) {
|
||||||
let cleared_cwd = {
|
let cleared_cwd = {
|
||||||
let mut cache = self
|
let mut cache = self
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ pub use model::SkillError;
|
|||||||
pub use model::SkillLoadOutcome;
|
pub use model::SkillLoadOutcome;
|
||||||
pub use model::SkillMetadata;
|
pub use model::SkillMetadata;
|
||||||
pub use model::SkillPolicy;
|
pub use model::SkillPolicy;
|
||||||
|
pub use model::filter_skill_load_outcome_for_product;
|
||||||
pub use render::render_skills_section;
|
pub use render::render_skills_section;
|
||||||
@@ -42,6 +42,21 @@ impl SkillMetadata {
|
|||||||
.and_then(|policy| policy.allow_implicit_invocation)
|
.and_then(|policy| policy.allow_implicit_invocation)
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches_product_restriction_for_product(
|
||||||
|
&self,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
|
) -> bool {
|
||||||
|
match &self.policy {
|
||||||
|
Some(policy) => {
|
||||||
|
policy.products.is_empty()
|
||||||
|
|| restriction_product.is_some_and(|product| {
|
||||||
|
product.matches_product_restriction(&policy.products)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
@@ -115,3 +130,29 @@ impl SkillLoadOutcome {
|
|||||||
.map(|skill| (skill, self.is_skill_enabled(skill)))
|
.map(|skill| (skill, self.is_skill_enabled(skill)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filter_skill_load_outcome_for_product(
|
||||||
|
mut outcome: SkillLoadOutcome,
|
||||||
|
restriction_product: Option<Product>,
|
||||||
|
) -> SkillLoadOutcome {
|
||||||
|
outcome
|
||||||
|
.skills
|
||||||
|
.retain(|skill| skill.matches_product_restriction_for_product(restriction_product));
|
||||||
|
outcome.implicit_skills_by_scripts_dir = Arc::new(
|
||||||
|
outcome
|
||||||
|
.implicit_skills_by_scripts_dir
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product))
|
||||||
|
.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_for_product(restriction_product))
|
||||||
|
.map(|(path, skill)| (path.clone(), skill.clone()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
outcome
|
||||||
|
}
|
||||||
@@ -169,18 +169,23 @@ impl ThreadManager {
|
|||||||
collaboration_modes_config: CollaborationModesConfig,
|
collaboration_modes_config: CollaborationModesConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let codex_home = config.codex_home.clone();
|
let codex_home = config.codex_home.clone();
|
||||||
|
let restriction_product = session_source.restriction_product();
|
||||||
let openai_models_provider = config
|
let openai_models_provider = config
|
||||||
.model_providers
|
.model_providers
|
||||||
.get(OPENAI_PROVIDER_ID)
|
.get(OPENAI_PROVIDER_ID)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| ModelProviderInfo::create_openai_provider(/*base_url*/ None));
|
.unwrap_or_else(|| ModelProviderInfo::create_openai_provider(/*base_url*/ None));
|
||||||
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
||||||
let plugins_manager = Arc::new(PluginsManager::new(codex_home.clone()));
|
let plugins_manager = Arc::new(PluginsManager::new_with_restriction_product(
|
||||||
|
codex_home.clone(),
|
||||||
|
restriction_product,
|
||||||
|
));
|
||||||
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
||||||
let skills_manager = Arc::new(SkillsManager::new(
|
let skills_manager = Arc::new(SkillsManager::new_with_restriction_product(
|
||||||
codex_home.clone(),
|
codex_home.clone(),
|
||||||
Arc::clone(&plugins_manager),
|
Arc::clone(&plugins_manager),
|
||||||
config.bundled_skills_enabled(),
|
config.bundled_skills_enabled(),
|
||||||
|
restriction_product,
|
||||||
));
|
));
|
||||||
let file_watcher = build_file_watcher(codex_home.clone(), Arc::clone(&skills_manager));
|
let file_watcher = build_file_watcher(codex_home.clone(), Arc::clone(&skills_manager));
|
||||||
Self {
|
Self {
|
||||||
@@ -236,12 +241,17 @@ impl ThreadManager {
|
|||||||
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
||||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||||
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY);
|
||||||
let plugins_manager = Arc::new(PluginsManager::new(codex_home.clone()));
|
let restriction_product = SessionSource::Exec.restriction_product();
|
||||||
|
let plugins_manager = Arc::new(PluginsManager::new_with_restriction_product(
|
||||||
|
codex_home.clone(),
|
||||||
|
restriction_product,
|
||||||
|
));
|
||||||
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
|
||||||
let skills_manager = Arc::new(SkillsManager::new(
|
let skills_manager = Arc::new(SkillsManager::new_with_restriction_product(
|
||||||
codex_home.clone(),
|
codex_home.clone(),
|
||||||
Arc::clone(&plugins_manager),
|
Arc::clone(&plugins_manager),
|
||||||
/*bundled_skills_enabled*/ true,
|
/*bundled_skills_enabled*/ true,
|
||||||
|
restriction_product,
|
||||||
));
|
));
|
||||||
let file_watcher = build_file_watcher(codex_home.clone(), Arc::clone(&skills_manager));
|
let file_watcher = build_file_watcher(codex_home.clone(), Arc::clone(&skills_manager));
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -2272,6 +2272,7 @@ pub enum SessionSource {
|
|||||||
VSCode,
|
VSCode,
|
||||||
Exec,
|
Exec,
|
||||||
Mcp,
|
Mcp,
|
||||||
|
Custom(String),
|
||||||
SubAgent(SubAgentSource),
|
SubAgent(SubAgentSource),
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -2302,6 +2303,7 @@ impl fmt::Display for SessionSource {
|
|||||||
SessionSource::VSCode => f.write_str("vscode"),
|
SessionSource::VSCode => f.write_str("vscode"),
|
||||||
SessionSource::Exec => f.write_str("exec"),
|
SessionSource::Exec => f.write_str("exec"),
|
||||||
SessionSource::Mcp => f.write_str("mcp"),
|
SessionSource::Mcp => f.write_str("mcp"),
|
||||||
|
SessionSource::Custom(source) => f.write_str(source),
|
||||||
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
|
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
|
||||||
SessionSource::Unknown => f.write_str("unknown"),
|
SessionSource::Unknown => f.write_str("unknown"),
|
||||||
}
|
}
|
||||||
@@ -2309,6 +2311,23 @@ impl fmt::Display for SessionSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(normalized),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_nickname(&self) -> Option<String> {
|
pub fn get_nickname(&self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
|
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
|
||||||
@@ -2332,6 +2351,24 @@ impl SessionSource {
|
|||||||
_ => None,
|
_ => 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::Unknown => Some(Product::Codex),
|
||||||
|
SessionSource::SubAgent(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
|
||||||
|
products.is_empty()
|
||||||
|
|| self
|
||||||
|
.restriction_product()
|
||||||
|
.is_some_and(|product| product.matches_product_restriction(products))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SubAgentSource {
|
impl fmt::Display for SubAgentSource {
|
||||||
@@ -2923,6 +2960,21 @@ pub enum Product {
|
|||||||
#[serde(alias = "ATLAS")]
|
#[serde(alias = "ATLAS")]
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
|
||||||
|
products.is_empty() || products.contains(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[ts(rename_all = "snake_case")]
|
#[ts(rename_all = "snake_case")]
|
||||||
@@ -3423,6 +3475,100 @@ mod tests {
|
|||||||
.any(|root| root.is_path_writable(path))
|
.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_normalizes_custom_values() {
|
||||||
|
assert_eq!(
|
||||||
|
SessionSource::from_startup_arg("atlas").unwrap(),
|
||||||
|
SessionSource::Custom("atlas".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SessionSource::from_startup_arg(" Atlas ").unwrap(),
|
||||||
|
SessionSource::Custom("atlas".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_source_restriction_product_defaults_non_subagent_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::Unknown.restriction_product(),
|
||||||
|
Some(Product::Codex)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_source_restriction_product_does_not_guess_subagent_products() {
|
||||||
|
assert_eq!(
|
||||||
|
SessionSource::SubAgent(SubAgentSource::Review).restriction_product(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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> {
|
fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
|
||||||
let mut paths = vec![cwd.to_path_buf()];
|
let mut paths = vec![cwd.to_path_buf()];
|
||||||
paths.extend(
|
paths.extend(
|
||||||
|
|||||||
@@ -735,7 +735,7 @@ async fn run_ratatui_app(
|
|||||||
/*page_size*/ 1,
|
/*page_size*/ 1,
|
||||||
/*cursor*/ None,
|
/*cursor*/ None,
|
||||||
ThreadSortKey::UpdatedAt,
|
ThreadSortKey::UpdatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
&config.model_provider_id,
|
&config.model_provider_id,
|
||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
@@ -835,7 +835,7 @@ async fn run_ratatui_app(
|
|||||||
/*page_size*/ 1,
|
/*page_size*/ 1,
|
||||||
/*cursor*/ None,
|
/*cursor*/ None,
|
||||||
ThreadSortKey::UpdatedAt,
|
ThreadSortKey::UpdatedAt,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
&config.model_provider_id,
|
&config.model_provider_id,
|
||||||
filter_cwd,
|
filter_cwd,
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ async fn run_session_picker(
|
|||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
request.cursor.as_ref(),
|
request.cursor.as_ref(),
|
||||||
request.sort_key,
|
request.sort_key,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
Some(provider_filter.as_slice()),
|
Some(provider_filter.as_slice()),
|
||||||
request.default_provider.as_str(),
|
request.default_provider.as_str(),
|
||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ fn spawn_rollout_page_loader(
|
|||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
cursor,
|
cursor,
|
||||||
request.sort_key,
|
request.sort_key,
|
||||||
INTERACTIVE_SESSION_SOURCES,
|
INTERACTIVE_SESSION_SOURCES.as_slice(),
|
||||||
default_provider.as_ref().map(std::slice::from_ref),
|
default_provider.as_ref().map(std::slice::from_ref),
|
||||||
default_provider.as_deref().unwrap_or_default(),
|
default_provider.as_deref().unwrap_or_default(),
|
||||||
/*search_term*/ None,
|
/*search_term*/ None,
|
||||||
|
|||||||
Reference in new issue
Block a user