mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Merge branch 'abhinav/hooks-updated-input' into abhinav/hooks-updated-input-permission-request
This commit is contained in:
@@ -11,11 +11,11 @@
|
||||
"path": "codex"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"regex": "^codex-x86_64-unknown-linux-musl\\.zst$",
|
||||
"regex": "^codex-x86_64-unknown-linux-musl-bundle\\.tar\\.zst$",
|
||||
"path": "codex"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"regex": "^codex-aarch64-unknown-linux-musl\\.zst$",
|
||||
"regex": "^codex-aarch64-unknown-linux-musl-bundle\\.tar\\.zst$",
|
||||
"path": "codex"
|
||||
},
|
||||
"windows-x86_64": {
|
||||
|
||||
@@ -379,6 +379,17 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then
|
||||
bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle"
|
||||
rm -rf "$bundle_root"
|
||||
mkdir -p "$bundle_root/codex-resources"
|
||||
cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex"
|
||||
cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap"
|
||||
chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap"
|
||||
tar -C "$bundle_root" -cf - codex codex-resources/bwrap |
|
||||
zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst"
|
||||
fi
|
||||
|
||||
if [[ "${{ matrix.build_dmg }}" == "true" ]]; then
|
||||
cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg"
|
||||
fi
|
||||
@@ -402,7 +413,7 @@ jobs:
|
||||
base="$(basename "$f")"
|
||||
# Skip files that are already archives (shouldn't happen, but be
|
||||
# safe).
|
||||
if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then
|
||||
if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -422,8 +433,8 @@ jobs:
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
# Upload the per-binary .zst files as well as the new .tar.gz
|
||||
# equivalents we generated in the previous step.
|
||||
# Upload the per-binary .zst files, .tar.gz equivalents, and any
|
||||
# prebuilt archives staged above.
|
||||
path: |
|
||||
codex-rs/dist/${{ matrix.target }}/*
|
||||
|
||||
|
||||
Generated
+29
@@ -1944,6 +1944,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
@@ -2125,6 +2126,17 @@ dependencies = [
|
||||
"serde_with",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-builtin-mcps"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-config",
|
||||
"codex-memories-mcp",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-bwrap"
|
||||
version = "0.0.0"
|
||||
@@ -2170,6 +2182,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-test-client",
|
||||
"codex-arg0",
|
||||
"codex-builtin-mcps",
|
||||
"codex-chatgpt",
|
||||
"codex-cloud-tasks",
|
||||
"codex-config",
|
||||
@@ -2997,6 +3010,7 @@ dependencies = [
|
||||
"async-channel",
|
||||
"codex-api",
|
||||
"codex-async-utils",
|
||||
"codex-builtin-mcps",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
@@ -3118,6 +3132,19 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-message-history"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-config",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-model-provider"
|
||||
version = "0.0.0"
|
||||
@@ -3660,6 +3687,7 @@ dependencies = [
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-message-history",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -4247,6 +4275,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"agent-graph-store",
|
||||
"agent-identity",
|
||||
"backend-client",
|
||||
"builtin-mcps",
|
||||
"bwrap",
|
||||
"ansi-escape",
|
||||
"async-utils",
|
||||
@@ -138,6 +139,7 @@ codex-apply-patch = { path = "apply-patch" }
|
||||
codex-arg0 = { path = "arg0" }
|
||||
codex-async-utils = { path = "async-utils" }
|
||||
codex-backend-client = { path = "backend-client" }
|
||||
codex-builtin-mcps = { path = "builtin-mcps" }
|
||||
codex-chatgpt = { path = "chatgpt" }
|
||||
codex-cli = { path = "cli" }
|
||||
codex-client = { path = "codex-client" }
|
||||
@@ -170,6 +172,7 @@ codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
codex-login = { path = "login" }
|
||||
codex-message-history = { path = "message-history" }
|
||||
codex-memories-mcp = { path = "memories/mcp" }
|
||||
codex-memories-read = { path = "memories/read" }
|
||||
codex-memories-write = { path = "memories/write" }
|
||||
|
||||
@@ -128,6 +128,7 @@ fn sample_thread_with_metadata(
|
||||
) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral,
|
||||
|
||||
@@ -76,6 +76,7 @@ fn sample_thread_archive_request() -> ClientRequest {
|
||||
fn sample_thread(thread_id: &str) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral: false,
|
||||
|
||||
@@ -33,4 +33,5 @@ url = { workspace = true }
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
@@ -72,12 +72,9 @@ pub mod legacy_core {
|
||||
pub use codex_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::LOCAL_AGENTS_MD_FILENAME;
|
||||
pub use codex_core::McpManager;
|
||||
pub use codex_core::append_message_history_entry;
|
||||
pub use codex_core::check_execpolicy_for_warnings;
|
||||
pub use codex_core::format_exec_policy_error_with_source;
|
||||
pub use codex_core::grant_read_root_non_elevated;
|
||||
pub use codex_core::lookup_message_history_entry;
|
||||
pub use codex_core::message_history_metadata;
|
||||
pub use codex_core::web_search_detail;
|
||||
|
||||
pub mod config {
|
||||
@@ -954,9 +951,13 @@ mod tests {
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_app_server_protocol::ToolRequestUserInputQuestion;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
@@ -975,19 +976,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_test_config_for_codex_home(codex_home: &Path) -> Config {
|
||||
match ConfigBuilder::default()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides_for_codex_home(
|
||||
codex_home.to_path_buf(),
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
struct TestClient {
|
||||
_codex_home: TempDir,
|
||||
client: InProcessAppServerClient,
|
||||
}
|
||||
|
||||
impl Deref for TestClient {
|
||||
type Target = InProcessAppServerClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl TestClient {
|
||||
async fn shutdown(self) -> IoResult<()> {
|
||||
self.client.shutdown().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_test_client_with_capacity(
|
||||
session_source: SessionSource,
|
||||
channel_capacity: usize,
|
||||
) -> InProcessAppServerClient {
|
||||
InProcessAppServerClient::start(InProcessClientStartArgs {
|
||||
) -> TestClient {
|
||||
let codex_home = TempDir::new().expect("temp dir");
|
||||
let config = Arc::new(build_test_config_for_codex_home(codex_home.path()).await);
|
||||
let state_db = init_state_db_from_config(config.as_ref())
|
||||
.await
|
||||
.expect("state db should initialize for in-process test");
|
||||
let client = InProcessAppServerClient::start(InProcessClientStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(build_test_config().await),
|
||||
config,
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
state_db: Some(state_db),
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source,
|
||||
@@ -999,10 +1040,15 @@ mod tests {
|
||||
channel_capacity,
|
||||
})
|
||||
.await
|
||||
.expect("in-process app-server client should start")
|
||||
.expect("in-process app-server client should start");
|
||||
|
||||
TestClient {
|
||||
_codex_home: codex_home,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_test_client(session_source: SessionSource) -> InProcessAppServerClient {
|
||||
async fn start_test_client(session_source: SessionSource) -> TestClient {
|
||||
start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
|
||||
}
|
||||
|
||||
|
||||
+106
-65
@@ -2197,11 +2197,37 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareListParams": {
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"properties": {
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
@@ -2210,6 +2236,15 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -2217,6 +2252,39 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTarget": {
|
||||
"properties": {
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareUpdateTargetsParams": {
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareTargets"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"properties": {
|
||||
"remoteMarketplaceName": {
|
||||
@@ -3256,13 +3324,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionMigration": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -3540,20 +3601,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -3962,20 +4012,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -4159,20 +4198,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
@@ -4339,22 +4367,11 @@
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
@@ -5177,6 +5194,30 @@
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/updateTargets"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareUpdateTargetsParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -3072,6 +3072,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3120,6 +3124,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
+163
-100
@@ -810,6 +810,30 @@
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/updateTargets"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PluginShareUpdateTargetsParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -7200,13 +7224,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -12479,6 +12499,14 @@
|
||||
"title": "PluginShareDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
@@ -12525,9 +12553,46 @@
|
||||
"title": "PluginShareListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
@@ -12536,6 +12601,15 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginShareTarget"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -12561,6 +12635,57 @@
|
||||
"title": "PluginShareSaveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTarget": {
|
||||
"properties": {
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareUpdateTargetsParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginShareTarget"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareTargets"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareUpdateTargetsResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"principals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginSharePrincipal"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principals"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -12960,13 +13085,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -14495,13 +14616,6 @@
|
||||
"title": "ServerRequestResolvedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionMigration": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -15216,6 +15330,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -15264,6 +15382,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
@@ -15439,20 +15558,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -15526,13 +15634,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -16957,20 +17061,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -17033,13 +17126,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -17265,20 +17354,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
@@ -17356,13 +17434,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -18029,22 +18103,11 @@
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
|
||||
+163
-100
@@ -1569,6 +1569,30 @@
|
||||
"title": "Plugin/share/saveRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/share/updateTargets"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginShareUpdateTargetsParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/share/updateTargetsRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3656,13 +3680,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -9090,6 +9110,14 @@
|
||||
"title": "PluginShareDeleteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareListItem": {
|
||||
"properties": {
|
||||
"localPluginPath": {
|
||||
@@ -9136,9 +9164,46 @@
|
||||
"title": "PluginShareListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareSaveParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
@@ -9147,6 +9212,15 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -9172,6 +9246,57 @@
|
||||
"title": "PluginShareSaveResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareTarget": {
|
||||
"properties": {
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareUpdateTargetsParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareTargets"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginShareUpdateTargetsResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"principals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principals"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSkillReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -9571,13 +9696,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -12381,13 +12502,6 @@
|
||||
"title": "ServerRequestResolvedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionMigration": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -13102,6 +13216,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -13150,6 +13268,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
@@ -13325,20 +13444,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -13412,13 +13520,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -14843,20 +14947,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
@@ -14919,13 +15012,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -15151,20 +15240,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
@@ -15242,13 +15320,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
@@ -15915,22 +15989,11 @@
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -352,13 +352,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -658,13 +654,9 @@
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
@@ -754,13 +746,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ToolsV2": {
|
||||
"properties": {
|
||||
"view_image": {
|
||||
|
||||
@@ -4,9 +4,50 @@
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareDiscoverability": {
|
||||
"enum": [
|
||||
"LISTED",
|
||||
"UNLISTED",
|
||||
"PRIVATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareTarget": {
|
||||
"properties": {
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"discoverability": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PluginShareDiscoverability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginPath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
@@ -15,6 +56,15 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginShareTarget": {
|
||||
"properties": {
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"remotePluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareTargets": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginShareTarget"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"remotePluginId",
|
||||
"shareTargets"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsParams",
|
||||
"type": "object"
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PluginSharePrincipal": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"principalType": {
|
||||
"$ref": "#/definitions/PluginSharePrincipalType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"principalId",
|
||||
"principalType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSharePrincipalType": {
|
||||
"enum": [
|
||||
"user",
|
||||
"group",
|
||||
"workspace"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"principals": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSharePrincipal"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"principals"
|
||||
],
|
||||
"title": "PluginShareUpdateTargetsResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -131,13 +131,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
@@ -222,20 +215,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
|
||||
@@ -1177,13 +1177,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1403,6 +1396,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -1451,6 +1448,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
@@ -2610,13 +2608,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
|
||||
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
+5
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
@@ -1010,13 +1010,6 @@
|
||||
"danger-full-access"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -1101,20 +1094,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
|
||||
@@ -1177,13 +1177,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1403,6 +1396,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -1451,6 +1448,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
@@ -2610,13 +2608,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
|
||||
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
@@ -165,13 +165,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadSource": {
|
||||
"enum": [
|
||||
"user",
|
||||
@@ -295,20 +288,9 @@
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sessionStartSource": {
|
||||
|
||||
@@ -1177,13 +1177,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1403,6 +1396,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -1451,6 +1448,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
@@ -2610,13 +2608,9 @@
|
||||
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
|
||||
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
@@ -853,6 +853,10 @@
|
||||
"description": "Usually the first user message in the thread, if available.",
|
||||
"type": "string"
|
||||
},
|
||||
"sessionId": {
|
||||
"description": "Session id shared by threads that belong to the same session tree.",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -901,6 +905,7 @@
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
"sessionId",
|
||||
"source",
|
||||
"status",
|
||||
"turns",
|
||||
|
||||
@@ -312,13 +312,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Settings for a collaboration mode.",
|
||||
"properties": {
|
||||
@@ -586,22 +579,11 @@
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -64,7 +64,6 @@ export type { ResponseItem } from "./ResponseItem";
|
||||
export type { ReviewDecision } from "./ReviewDecision";
|
||||
export type { ServerNotification } from "./ServerNotification";
|
||||
export type { ServerRequest } from "./ServerRequest";
|
||||
export type { ServiceTier } from "./ServiceTier";
|
||||
export type { SessionSource } from "./SessionSource";
|
||||
export type { Settings } from "./Settings";
|
||||
export type { SubAgentSource } from "./SubAgentSource";
|
||||
|
||||
+1
-2
@@ -4,7 +4,6 @@
|
||||
import type { ForcedLoginMethod } from "../ForcedLoginMethod";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { Verbosity } from "../Verbosity";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
@@ -20,4 +19,4 @@ export type Config = {model: string | null, review_model: string | null, model_c
|
||||
* [UNSTABLE] Optional default for where approval requests are routed for
|
||||
* review.
|
||||
*/
|
||||
approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: ServiceTier | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginShareDiscoverability = "LISTED" | "UNLISTED" | "PRIVATE";
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
|
||||
export type PluginSharePrincipal = { principalType: PluginSharePrincipalType, principalId: string, name: string, };
|
||||
+1
-1
@@ -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 ServiceTier = "fast" | "flex";
|
||||
export type PluginSharePrincipalType = "user" | "group" | "workspace";
|
||||
+3
-1
@@ -2,5 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { PluginShareDiscoverability } from "./PluginShareDiscoverability";
|
||||
import type { PluginShareTarget } from "./PluginShareTarget";
|
||||
|
||||
export type PluginShareSaveParams = { pluginPath: AbsolutePathBuf, remotePluginId?: string | null, };
|
||||
export type PluginShareSaveParams = { pluginPath: AbsolutePathBuf, remotePluginId?: string | null, discoverability?: PluginShareDiscoverability | null, shareTargets?: Array<PluginShareTarget> | null, };
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
|
||||
export type PluginShareTarget = { principalType: PluginSharePrincipalType, principalId: string, };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginShareTarget } from "./PluginShareTarget";
|
||||
|
||||
export type PluginShareUpdateTargetsParams = { remotePluginId: string, shareTargets: Array<PluginShareTarget>, };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSharePrincipal } from "./PluginSharePrincipal";
|
||||
|
||||
export type PluginShareUpdateTargetsResponse = { principals: Array<PluginSharePrincipal>, };
|
||||
@@ -3,7 +3,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { Verbosity } from "../Verbosity";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
@@ -16,4 +15,4 @@ export type ProfileV2 = {model: string | null, model_provider: string | null, ap
|
||||
* are routed for review. If omitted, the enclosing config default is
|
||||
* used.
|
||||
*/
|
||||
approvals_reviewer: ApprovalsReviewer | null, service_tier: ServiceTier | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, chatgpt_base_url: string | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
approvals_reviewer: ApprovalsReviewer | null, service_tier: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, chatgpt_base_url: string | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
|
||||
@@ -9,6 +9,10 @@ import type { ThreadStatus } from "./ThreadStatus";
|
||||
import type { Turn } from "./Turn";
|
||||
|
||||
export type Thread = { id: string,
|
||||
/**
|
||||
* Session id shared by threads that belong to the same session tree.
|
||||
*/
|
||||
sessionId: string,
|
||||
/**
|
||||
* Source thread id when this thread was created by forking another thread.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -20,7 +19,7 @@ import type { ThreadSource } from "./ThreadSource";
|
||||
export type ThreadForkParams = {threadId: string, /**
|
||||
* Configuration overrides for the forked thread, if any.
|
||||
*/
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -22,7 +21,7 @@ import type { SandboxMode } from "./SandboxMode";
|
||||
export type ThreadResumeParams = {threadId: string, /**
|
||||
* Configuration overrides for the resumed thread, if any.
|
||||
*/
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -10,7 +9,7 @@ import type { SandboxMode } from "./SandboxMode";
|
||||
import type { ThreadSource } from "./ThreadSource";
|
||||
import type { ThreadStartSource } from "./ThreadStartSource";
|
||||
|
||||
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /**
|
||||
* Override where approval requests are routed for review on this thread
|
||||
* and subsequent turns.
|
||||
*/
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
@@ -30,7 +29,7 @@ sandboxPolicy?: SandboxPolicy | null, /**
|
||||
model?: string | null, /**
|
||||
* Override the service tier for this turn and subsequent turns.
|
||||
*/
|
||||
serviceTier?: ServiceTier | null | null, /**
|
||||
serviceTier?: string | null | null, /**
|
||||
* Override the reasoning effort for this turn and subsequent turns.
|
||||
*/
|
||||
effort?: ReasoningEffort | null, /**
|
||||
|
||||
@@ -285,11 +285,17 @@ export type { PluginReadParams } from "./PluginReadParams";
|
||||
export type { PluginReadResponse } from "./PluginReadResponse";
|
||||
export type { PluginShareDeleteParams } from "./PluginShareDeleteParams";
|
||||
export type { PluginShareDeleteResponse } from "./PluginShareDeleteResponse";
|
||||
export type { PluginShareDiscoverability } from "./PluginShareDiscoverability";
|
||||
export type { PluginShareListItem } from "./PluginShareListItem";
|
||||
export type { PluginShareListParams } from "./PluginShareListParams";
|
||||
export type { PluginShareListResponse } from "./PluginShareListResponse";
|
||||
export type { PluginSharePrincipal } from "./PluginSharePrincipal";
|
||||
export type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
|
||||
export type { PluginShareSaveParams } from "./PluginShareSaveParams";
|
||||
export type { PluginShareSaveResponse } from "./PluginShareSaveResponse";
|
||||
export type { PluginShareTarget } from "./PluginShareTarget";
|
||||
export type { PluginShareUpdateTargetsParams } from "./PluginShareUpdateTargetsParams";
|
||||
export type { PluginShareUpdateTargetsResponse } from "./PluginShareUpdateTargetsResponse";
|
||||
export type { PluginSkillReadParams } from "./PluginSkillReadParams";
|
||||
export type { PluginSkillReadResponse } from "./PluginSkillReadResponse";
|
||||
export type { PluginSource } from "./PluginSource";
|
||||
|
||||
@@ -628,6 +628,11 @@ client_request_definitions! {
|
||||
serialization: global("config"),
|
||||
response: v2::PluginShareSaveResponse,
|
||||
},
|
||||
PluginShareUpdateTargets => "plugin/share/updateTargets" {
|
||||
params: v2::PluginShareUpdateTargetsParams,
|
||||
serialization: global("config"),
|
||||
response: v2::PluginShareUpdateTargetsResponse,
|
||||
},
|
||||
PluginShareList => "plugin/share/list" {
|
||||
params: v2::PluginShareListParams,
|
||||
serialization: global("config"),
|
||||
@@ -2173,6 +2178,7 @@ mod tests {
|
||||
response: v2::ThreadStartResponse {
|
||||
thread: v2::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
preview: "first prompt".to_string(),
|
||||
ephemeral: true,
|
||||
@@ -2214,6 +2220,7 @@ mod tests {
|
||||
"response": {
|
||||
"thread": {
|
||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
||||
"forkedFromId": null,
|
||||
"preview": "first prompt",
|
||||
"ephemeral": true,
|
||||
|
||||
@@ -5,7 +5,6 @@ use super::shared::default_enabled;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
@@ -137,7 +136,7 @@ pub struct ProfileV2 {
|
||||
/// used.
|
||||
#[experimental("config/read.approvalsReviewer")]
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub service_tier: Option<String>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
@@ -248,7 +247,7 @@ pub struct Config {
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub service_tier: Option<String>,
|
||||
pub analytics: Option<AnalyticsConfig>,
|
||||
#[experimental("config/read.apps")]
|
||||
#[serde(default)]
|
||||
|
||||
@@ -194,6 +194,10 @@ pub struct PluginShareSaveParams {
|
||||
pub plugin_path: AbsolutePathBuf,
|
||||
#[ts(optional = nullable)]
|
||||
pub remote_plugin_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub discoverability: Option<PluginShareDiscoverability>,
|
||||
#[ts(optional = nullable)]
|
||||
pub share_targets: Option<Vec<PluginShareTarget>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -204,6 +208,21 @@ pub struct PluginShareSaveResponse {
|
||||
pub share_url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareUpdateTargetsParams {
|
||||
pub remote_plugin_id: String,
|
||||
pub share_targets: Vec<PluginShareTarget>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareUpdateTargetsResponse {
|
||||
pub principals: Vec<PluginSharePrincipal>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -237,6 +256,51 @@ pub struct PluginShareListItem {
|
||||
pub local_plugin_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginShareDiscoverability {
|
||||
#[serde(rename = "LISTED")]
|
||||
#[ts(rename = "LISTED")]
|
||||
Listed,
|
||||
#[serde(rename = "UNLISTED")]
|
||||
#[ts(rename = "UNLISTED")]
|
||||
Unlisted,
|
||||
#[serde(rename = "PRIVATE")]
|
||||
#[ts(rename = "PRIVATE")]
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginSharePrincipalType {
|
||||
#[serde(rename = "user")]
|
||||
#[ts(rename = "user")]
|
||||
User,
|
||||
#[serde(rename = "group")]
|
||||
#[ts(rename = "group")]
|
||||
Group,
|
||||
#[serde(rename = "workspace")]
|
||||
#[ts(rename = "workspace")]
|
||||
Workspace,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginShareTarget {
|
||||
pub principal_type: PluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSharePrincipal {
|
||||
pub principal_type: PluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
|
||||
@@ -2981,11 +2981,15 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
serde_json::to_value(PluginShareSaveParams {
|
||||
plugin_path: plugin_path.clone(),
|
||||
remote_plugin_id: None,
|
||||
discoverability: None,
|
||||
share_targets: None,
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"pluginPath": plugin_path_json,
|
||||
"remotePluginId": null,
|
||||
"discoverability": null,
|
||||
"shareTargets": null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2993,11 +2997,33 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
serde_json::to_value(PluginShareSaveParams {
|
||||
plugin_path,
|
||||
remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string(),),
|
||||
discoverability: Some(PluginShareDiscoverability::Private),
|
||||
share_targets: Some(vec![
|
||||
PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
},
|
||||
PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::Workspace,
|
||||
principal_id: "workspace-1".to_string(),
|
||||
},
|
||||
]),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"pluginPath": plugin_path_json,
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"discoverability": "PRIVATE",
|
||||
"shareTargets": [
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
},
|
||||
{
|
||||
"principalType": "workspace",
|
||||
"principalId": "workspace-1",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3013,6 +3039,42 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareUpdateTargetsParams {
|
||||
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
|
||||
share_targets: vec![PluginShareTarget {
|
||||
principal_type: PluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
}],
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"shareTargets": [{
|
||||
"principalType": "group",
|
||||
"principalId": "group-1",
|
||||
}],
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginShareUpdateTargetsResponse {
|
||||
principals: vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"principals": [{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
"name": "Gavin",
|
||||
}],
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PluginShareListParams>(json!({})).unwrap(),
|
||||
PluginShareListParams {},
|
||||
@@ -3375,10 +3437,11 @@ fn thread_start_params_preserve_explicit_null_service_tier() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_lifecycle_responses_default_missing_compat_fields() {
|
||||
fn thread_lifecycle_responses_default_missing_optional_fields() {
|
||||
let response = json!({
|
||||
"thread": {
|
||||
"id": "thread-id",
|
||||
"sessionId": "thread-id",
|
||||
"forkedFromId": null,
|
||||
"preview": "",
|
||||
"ephemeral": false,
|
||||
|
||||
@@ -12,7 +12,6 @@ use super::TurnEnvironmentParams;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
|
||||
@@ -103,7 +102,7 @@ pub struct ThreadStartParams {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
pub service_tier: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
@@ -192,7 +191,7 @@ pub struct ThreadStartResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub service_tier: Option<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
@@ -260,7 +259,7 @@ pub struct ThreadResumeParams {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
pub service_tier: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
@@ -307,7 +306,7 @@ pub struct ThreadResumeResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub service_tier: Option<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
@@ -366,7 +365,7 @@ pub struct ThreadForkParams {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
pub service_tier: Option<Option<String>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
@@ -416,7 +415,7 @@ pub struct ThreadForkResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub service_tier: Option<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -104,6 +104,8 @@ pub struct GitInfo {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Thread {
|
||||
pub id: String,
|
||||
/// Session id shared by threads that belong to the same session tree.
|
||||
pub session_id: String,
|
||||
/// Source thread id when this thread was created by forking another thread.
|
||||
pub forked_from_id: Option<String>,
|
||||
/// Usually the first user message in the thread, if available.
|
||||
|
||||
@@ -7,7 +7,6 @@ use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
|
||||
@@ -94,7 +93,7 @@ pub struct TurnStartParams {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
pub service_tier: Option<Option<String>>,
|
||||
/// Override the reasoning effort for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub effort: Option<ReasoningEffort>,
|
||||
|
||||
@@ -303,11 +303,11 @@ Example:
|
||||
{ "id": 12, "result": { "thread": { "id": "thr_123", "turns": [], … } } }
|
||||
```
|
||||
|
||||
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only:
|
||||
To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. The returned `thread.sessionId` identifies the current live session tree root. Root threads use their own `thread.id` as `thread.sessionId`; stored threads that are not loaded also report their own `thread.id`, because resuming one makes it the root of a new live session tree. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only:
|
||||
|
||||
```json
|
||||
{ "method": "thread/fork", "id": 12, "params": { "threadId": "thr_123", "ephemeral": true } }
|
||||
{ "id": 12, "result": { "thread": { "id": "thr_456", … } } }
|
||||
{ "id": 12, "result": { "thread": { "id": "thr_456", "sessionId": "thr_456", … } } }
|
||||
{ "method": "thread/started", "params": { "thread": { … } } }
|
||||
```
|
||||
|
||||
@@ -1393,6 +1393,12 @@ If the session approval policy uses `Granular` with `request_permissions: false`
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
Dynamic tool identifiers follow the same constraints as Responses function tools:
|
||||
|
||||
- `name` must match `^[a-zA-Z0-9_-]+$` and be between 1 and 128 characters.
|
||||
- `namespace`, when present, must match `^[a-zA-Z0-9_-]+$` and be between 1 and 64 characters.
|
||||
- `namespace` must not collide with reserved Responses runtime namespaces such as `functions`, `multi_tool_use`, `file_search`, `web`, `browser`, `image_gen`, `computer`, `container`, `terminal`, `python`, `python_user_visible`, `api_tool`, `tool_search`, or `submodel_delegator`.
|
||||
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
|
||||
When a dynamic tool is invoked during a turn, the server sends an `item/tool/call` JSON-RPC request to the client:
|
||||
|
||||
@@ -1190,6 +1190,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
let response = match thread_rollback_response_from_stored_thread(
|
||||
stored_thread,
|
||||
conversation.session_configured().session_id.to_string(),
|
||||
fallback_model_provider.as_str(),
|
||||
&fallback_cwd,
|
||||
loaded_status,
|
||||
@@ -1543,6 +1544,7 @@ async fn handle_thread_rollback_failed(
|
||||
|
||||
fn thread_rollback_response_from_stored_thread(
|
||||
stored_thread: codex_thread_store::StoredThread,
|
||||
session_id: String,
|
||||
fallback_model_provider: &str,
|
||||
fallback_cwd: &AbsolutePathBuf,
|
||||
loaded_status: ThreadStatus,
|
||||
@@ -1550,6 +1552,7 @@ fn thread_rollback_response_from_stored_thread(
|
||||
let thread_id = stored_thread.thread_id;
|
||||
let (mut thread, history) =
|
||||
thread_from_stored_thread(stored_thread, fallback_model_provider, fallback_cwd);
|
||||
thread.session_id = session_id;
|
||||
let Some(history) = history else {
|
||||
return Err(format!(
|
||||
"thread {thread_id} did not include persisted history after rollback"
|
||||
@@ -2200,6 +2203,7 @@ mod tests {
|
||||
|
||||
let response = thread_rollback_response_from_stored_thread(
|
||||
stored_thread,
|
||||
thread_id.to_string(),
|
||||
"fallback-provider",
|
||||
&fallback_cwd,
|
||||
ThreadStatus::NotLoaded,
|
||||
|
||||
@@ -140,6 +140,21 @@ impl ConfigManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_latest_config_for_thread(
|
||||
&self,
|
||||
thread_config: &Config,
|
||||
) -> std::io::Result<Config> {
|
||||
let refreshed_config = self
|
||||
.load_latest_config(Some(thread_config.cwd.to_path_buf()))
|
||||
.await?;
|
||||
let mut config = thread_config
|
||||
.rebuild_preserving_session_layers(&refreshed_config)
|
||||
.await?;
|
||||
self.apply_runtime_feature_enablement(&mut config);
|
||||
self.apply_arg0_paths(&mut config);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(crate) async fn load_default_config(&self) -> std::io::Result<Config> {
|
||||
let mut config = Config::load_default_with_cli_overrides_for_codex_home(
|
||||
self.codex_home.clone(),
|
||||
|
||||
@@ -83,6 +83,7 @@ use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use codex_core::resolve_installation_id;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
@@ -345,7 +346,7 @@ impl InProcessClientHandle {
|
||||
/// the runtime is shut down and an `InvalidData` error is returned.
|
||||
pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
|
||||
let initialize = args.initialize.clone();
|
||||
let client = start_uninitialized(args).await;
|
||||
let client = start_uninitialized(args).await?;
|
||||
|
||||
let initialize_response = client
|
||||
.request(ClientRequest::Initialize {
|
||||
@@ -365,12 +366,13 @@ pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle>
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
|
||||
let channel_capacity = args.channel_capacity.max(1);
|
||||
let state_db = match args.state_db.clone() {
|
||||
Some(state_db) => Some(state_db),
|
||||
None => init_state_db_from_config(args.config.as_ref()).await,
|
||||
};
|
||||
let installation_id = resolve_installation_id(&args.config.codex_home).await?;
|
||||
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
|
||||
let (event_tx, event_rx) = mpsc::channel::<InProcessServerEvent>(channel_capacity);
|
||||
|
||||
@@ -438,6 +440,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle
|
||||
config_warnings: args.config_warnings,
|
||||
session_source: args.session_source,
|
||||
auth_manager,
|
||||
installation_id,
|
||||
rpc_transport: AppServerRpcTransport::InProcess,
|
||||
remote_control_handle: None,
|
||||
plugin_startup_tasks: crate::PluginStartupTasks::Start,
|
||||
@@ -718,13 +721,13 @@ async fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle
|
||||
}
|
||||
});
|
||||
|
||||
InProcessClientHandle {
|
||||
Ok(InProcessClientHandle {
|
||||
client: InProcessClientSender { client_tx },
|
||||
event_rx,
|
||||
runtime_handle,
|
||||
#[cfg(test)]
|
||||
_test_codex_home: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::resolve_installation_id;
|
||||
use codex_exec_server::EnvironmentManagerArgs;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
@@ -85,6 +86,7 @@ mod filters;
|
||||
mod fs_watch;
|
||||
mod fuzzy_file_search;
|
||||
pub mod in_process;
|
||||
mod mcp_refresh;
|
||||
mod message_processor;
|
||||
mod models;
|
||||
mod outgoing_message;
|
||||
@@ -620,6 +622,7 @@ pub async fn run_main_with_transport_options(
|
||||
None => error!("{}", warning.summary),
|
||||
}
|
||||
}
|
||||
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
||||
let transport_shutdown_token = CancellationToken::new();
|
||||
let mut transport_accept_handles = Vec::<JoinHandle<()>>::new();
|
||||
|
||||
@@ -763,6 +766,7 @@ pub async fn run_main_with_transport_options(
|
||||
config_warnings,
|
||||
session_source,
|
||||
auth_manager,
|
||||
installation_id,
|
||||
rpc_transport: analytics_rpc_transport(&transport),
|
||||
remote_control_handle: Some(remote_control_handle.clone()),
|
||||
plugin_startup_tasks: runtime_options.plugin_startup_tasks,
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
use crate::config_manager::ConfigManager;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::McpServerRefreshConfig;
|
||||
use codex_protocol::protocol::Op;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) async fn queue_strict_refresh(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
config_manager: &ConfigManager,
|
||||
) -> io::Result<()> {
|
||||
config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await?;
|
||||
let mut refreshes = Vec::new();
|
||||
for thread_id in thread_manager.list_thread_ids().await {
|
||||
let thread = thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|err| io::Error::other(format!("failed to load thread {thread_id}: {err}")))?;
|
||||
let config =
|
||||
build_refresh_config(thread_manager, config_manager, thread.config().await).await?;
|
||||
refreshes.push((thread_id, thread, config));
|
||||
}
|
||||
for (thread_id, thread, config) in refreshes {
|
||||
queue_refresh(thread_id, thread, config).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn queue_best_effort_refresh(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
config_manager: &ConfigManager,
|
||||
) {
|
||||
for thread_id in thread_manager.list_thread_ids().await {
|
||||
let thread = match thread_manager.get_thread(thread_id).await {
|
||||
Ok(thread) => thread,
|
||||
Err(err) => {
|
||||
warn!("failed to load thread {thread_id} for MCP refresh: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let config =
|
||||
match build_refresh_config(thread_manager, config_manager, thread.config().await).await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
warn!("failed to build MCP refresh config for thread {thread_id}: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(err) = queue_refresh(thread_id, thread, config).await {
|
||||
warn!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_refresh_config(
|
||||
thread_manager: &ThreadManager,
|
||||
config_manager: &ConfigManager,
|
||||
thread_config: Arc<Config>,
|
||||
) -> io::Result<McpServerRefreshConfig> {
|
||||
let config = config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await?;
|
||||
let mcp_servers = thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(&config)
|
||||
.await;
|
||||
Ok(McpServerRefreshConfig {
|
||||
mcp_servers: serde_json::to_value(mcp_servers).map_err(io::Error::other)?,
|
||||
mcp_oauth_credentials_store_mode: serde_json::to_value(
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
)
|
||||
.map_err(io::Error::other)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn queue_refresh(
|
||||
thread_id: ThreadId,
|
||||
thread: Arc<CodexThread>,
|
||||
config: McpServerRefreshConfig,
|
||||
) -> io::Result<()> {
|
||||
thread
|
||||
.submit(Op::RefreshMcpServers { config })
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to queue MCP refresh for thread {thread_id}: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigContext;
|
||||
use codex_config::ThreadConfigLoadError;
|
||||
use codex_config::ThreadConfigLoadErrorCode;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::ThreadConfigSource;
|
||||
use codex_core::agent_graph_store_from_state_db;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use codex_core::thread_store_from_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn strict_refresh_reports_thread_planning_failures() -> anyhow::Result<()> {
|
||||
let (_temp_dir, thread_manager, config_manager, _loader) = refresh_test_state().await?;
|
||||
|
||||
let err = queue_strict_refresh(&thread_manager, &config_manager)
|
||||
.await
|
||||
.expect_err("strict refresh should fail");
|
||||
|
||||
assert_eq!(err.to_string(), "failed to load refresh config");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn best_effort_refresh_attempts_every_loaded_thread() -> anyhow::Result<()> {
|
||||
let (_temp_dir, thread_manager, config_manager, loader) = refresh_test_state().await?;
|
||||
|
||||
queue_best_effort_refresh(&thread_manager, &config_manager).await;
|
||||
|
||||
assert_eq!(loader.good_loads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(loader.bad_loads.load(Ordering::Relaxed), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_test_state() -> anyhow::Result<(
|
||||
TempDir,
|
||||
Arc<ThreadManager>,
|
||||
ConfigManager,
|
||||
Arc<CountingThreadConfigLoader>,
|
||||
)> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let good_cwd = temp_dir.path().join("good");
|
||||
let bad_cwd = temp_dir.path().join("bad");
|
||||
std::fs::create_dir_all(&good_cwd)?;
|
||||
std::fs::create_dir_all(&bad_cwd)?;
|
||||
|
||||
let initial_config_manager =
|
||||
ConfigManager::without_managed_config_for_tests(temp_dir.path().to_path_buf());
|
||||
let good_config = initial_config_manager
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
ConfigOverrides::default(),
|
||||
Some(good_cwd.clone()),
|
||||
)
|
||||
.await?;
|
||||
let bad_config = initial_config_manager
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
ConfigOverrides::default(),
|
||||
Some(bad_cwd.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
|
||||
let state_db = init_state_db_from_config(&good_config)
|
||||
.await
|
||||
.expect("refresh tests require state db");
|
||||
let thread_store = thread_store_from_config(&good_config, state_db.clone());
|
||||
let agent_graph_store = agent_graph_store_from_state_db(state_db.clone());
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
&good_config,
|
||||
auth_manager,
|
||||
SessionSource::Exec,
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
state_db,
|
||||
thread_store,
|
||||
agent_graph_store,
|
||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||
));
|
||||
thread_manager.start_thread(good_config).await?;
|
||||
thread_manager.start_thread(bad_config).await?;
|
||||
|
||||
let loader = Arc::new(CountingThreadConfigLoader {
|
||||
good_cwd: AbsolutePathBuf::try_from(good_cwd)?,
|
||||
bad_cwd: AbsolutePathBuf::try_from(bad_cwd)?,
|
||||
good_loads: AtomicUsize::new(0),
|
||||
bad_loads: AtomicUsize::new(0),
|
||||
});
|
||||
let config_manager = ConfigManager::new(
|
||||
temp_dir.path().to_path_buf(),
|
||||
Vec::new(),
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
Arg0DispatchPaths::default(),
|
||||
loader.clone(),
|
||||
);
|
||||
|
||||
Ok((temp_dir, thread_manager, config_manager, loader))
|
||||
}
|
||||
|
||||
struct CountingThreadConfigLoader {
|
||||
good_cwd: AbsolutePathBuf,
|
||||
bad_cwd: AbsolutePathBuf,
|
||||
good_loads: AtomicUsize,
|
||||
bad_loads: AtomicUsize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ThreadConfigLoader for CountingThreadConfigLoader {
|
||||
async fn load(
|
||||
&self,
|
||||
context: ThreadConfigContext,
|
||||
) -> Result<Vec<ThreadConfigSource>, ThreadConfigLoadError> {
|
||||
if context.cwd.as_ref() == Some(&self.good_cwd) {
|
||||
self.good_loads.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if context.cwd.as_ref() == Some(&self.bad_cwd) {
|
||||
self.bad_loads.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ThreadConfigLoadError::new(
|
||||
ThreadConfigLoadErrorCode::Internal,
|
||||
/*status_code*/ None,
|
||||
"failed to load refresh config",
|
||||
));
|
||||
}
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,6 +259,7 @@ pub(crate) struct MessageProcessorArgs {
|
||||
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
pub(crate) installation_id: String,
|
||||
pub(crate) rpc_transport: AppServerRpcTransport,
|
||||
pub(crate) remote_control_handle: Option<RemoteControlHandle>,
|
||||
pub(crate) plugin_startup_tasks: crate::PluginStartupTasks,
|
||||
@@ -281,6 +282,7 @@ impl MessageProcessor {
|
||||
config_warnings,
|
||||
session_source,
|
||||
auth_manager,
|
||||
installation_id,
|
||||
rpc_transport,
|
||||
remote_control_handle,
|
||||
plugin_startup_tasks,
|
||||
@@ -302,6 +304,7 @@ impl MessageProcessor {
|
||||
state_db.clone(),
|
||||
Arc::clone(&thread_store),
|
||||
agent_graph_store.clone(),
|
||||
installation_id,
|
||||
));
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
@@ -416,7 +419,7 @@ impl MessageProcessor {
|
||||
if matches!(plugin_startup_tasks, crate::PluginStartupTasks::Start) {
|
||||
// Keep plugin startup warmups aligned at app-server startup.
|
||||
let on_effective_plugins_changed =
|
||||
plugin_processor.effective_plugins_changed_callback((*config).clone());
|
||||
plugin_processor.effective_plugins_changed_callback();
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_plugin_startup_tasks_for_config(
|
||||
@@ -1087,6 +1090,11 @@ impl MessageProcessor {
|
||||
ClientRequest::PluginShareSave { params, .. } => {
|
||||
self.plugin_processor.plugin_share_save(params).await
|
||||
}
|
||||
ClientRequest::PluginShareUpdateTargets { params, .. } => {
|
||||
self.plugin_processor
|
||||
.plugin_share_update_targets(params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::PluginShareList { params, .. } => {
|
||||
self.plugin_processor.plugin_share_list(params).await
|
||||
}
|
||||
|
||||
@@ -298,6 +298,7 @@ async fn build_test_processor(
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::VSCode,
|
||||
auth_manager,
|
||||
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
remote_control_handle: None,
|
||||
plugin_startup_tasks: crate::PluginStartupTasks::Start,
|
||||
|
||||
@@ -115,11 +115,17 @@ use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginShareDeleteParams;
|
||||
use codex_app_server_protocol::PluginShareDeleteResponse;
|
||||
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||
use codex_app_server_protocol::PluginShareListItem;
|
||||
use codex_app_server_protocol::PluginShareListParams;
|
||||
use codex_app_server_protocol::PluginShareListResponse;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginShareSaveParams;
|
||||
use codex_app_server_protocol::PluginShareSaveResponse;
|
||||
use codex_app_server_protocol::PluginShareTarget;
|
||||
use codex_app_server_protocol::PluginShareUpdateTargetsParams;
|
||||
use codex_app_server_protocol::PluginShareUpdateTargetsResponse;
|
||||
use codex_app_server_protocol::PluginSkillReadParams;
|
||||
use codex_app_server_protocol::PluginSkillReadResponse;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
@@ -355,7 +361,6 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GitInfo as CoreGitInfo;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus;
|
||||
use codex_protocol::protocol::McpServerRefreshConfig;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||
use codex_protocol::protocol::RealtimeVoicesList;
|
||||
|
||||
@@ -168,7 +168,7 @@ impl AccountRequestProcessor {
|
||||
{
|
||||
Ok(config) => {
|
||||
let refresh_thread_manager = Arc::clone(thread_manager);
|
||||
let refresh_config = config.clone();
|
||||
let refresh_config_manager = config_manager.clone();
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
.maybe_start_remote_installed_plugins_cache_refresh(
|
||||
@@ -177,7 +177,7 @@ impl AccountRequestProcessor {
|
||||
Some(Arc::new(move || {
|
||||
Self::spawn_effective_plugins_changed_task(
|
||||
Arc::clone(&refresh_thread_manager),
|
||||
refresh_config.clone(),
|
||||
refresh_config_manager.clone(),
|
||||
);
|
||||
})),
|
||||
);
|
||||
@@ -190,19 +190,17 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_effective_plugins_changed_task(thread_manager: Arc<ThreadManager>, config: Config) {
|
||||
fn spawn_effective_plugins_changed_task(
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
config_manager: ConfigManager,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_manager().clear_cache();
|
||||
if thread_manager.list_thread_ids().await.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(err) =
|
||||
McpRequestProcessor::queue_mcp_server_refresh_for_config(&thread_manager, &config)
|
||||
.await
|
||||
{
|
||||
warn!("failed to queue MCP refresh after effective plugins changed: {err:?}");
|
||||
}
|
||||
crate::mcp_refresh::queue_best_effort_refresh(&thread_manager, &config_manager).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
_params: Option<()>,
|
||||
) -> Result<McpServerRefreshResponse, JSONRPCErrorError> {
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
Self::queue_mcp_server_refresh_for_config(&self.thread_manager, &config).await?;
|
||||
crate::mcp_refresh::queue_strict_refresh(&self.thread_manager, &self.config_manager)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to refresh MCP servers: {err}")))?;
|
||||
Ok(McpServerRefreshResponse {})
|
||||
}
|
||||
|
||||
@@ -108,44 +109,6 @@ impl McpRequestProcessor {
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
|
||||
pub(super) async fn queue_mcp_server_refresh_for_config(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
config: &Config,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let configured_servers = thread_manager
|
||||
.mcp_manager()
|
||||
.configured_servers(config)
|
||||
.await;
|
||||
let mcp_servers = match serde_json::to_value(configured_servers) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to serialize MCP servers: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let mcp_oauth_credentials_store_mode =
|
||||
match serde_json::to_value(config.mcp_oauth_credentials_store_mode) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to serialize MCP OAuth credentials store mode: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_config = McpServerRefreshConfig {
|
||||
mcp_servers,
|
||||
mcp_oauth_credentials_store_mode,
|
||||
};
|
||||
|
||||
// Refresh requests are queued per thread; each thread rebuilds MCP connections on its next
|
||||
// active turn to avoid work for threads that never resume.
|
||||
thread_manager.refresh_mcp_servers(refresh_config).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mcp_server_oauth_login_response(
|
||||
&self,
|
||||
params: McpServerOauthLoginParams,
|
||||
|
||||
@@ -86,6 +86,66 @@ fn marketplace_plugin_source_to_info(source: MarketplacePluginSource) -> PluginS
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_plugin_share_discoverability(
|
||||
discoverability: PluginShareDiscoverability,
|
||||
) -> codex_core_plugins::remote::RemotePluginShareDiscoverability {
|
||||
match discoverability {
|
||||
PluginShareDiscoverability::Listed => {
|
||||
codex_core_plugins::remote::RemotePluginShareDiscoverability::Listed
|
||||
}
|
||||
PluginShareDiscoverability::Unlisted => {
|
||||
codex_core_plugins::remote::RemotePluginShareDiscoverability::Unlisted
|
||||
}
|
||||
PluginShareDiscoverability::Private => {
|
||||
codex_core_plugins::remote::RemotePluginShareDiscoverability::Private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_plugin_share_targets(
|
||||
targets: Vec<PluginShareTarget>,
|
||||
) -> Vec<codex_core_plugins::remote::RemotePluginShareTarget> {
|
||||
targets
|
||||
.into_iter()
|
||||
.map(
|
||||
|target| codex_core_plugins::remote::RemotePluginShareTarget {
|
||||
principal_type: match target.principal_type {
|
||||
PluginSharePrincipalType::User => {
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::User
|
||||
}
|
||||
PluginSharePrincipalType::Group => {
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::Group
|
||||
}
|
||||
PluginSharePrincipalType::Workspace => {
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::Workspace
|
||||
}
|
||||
},
|
||||
principal_id: target.principal_id,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn plugin_share_principal_from_remote(
|
||||
principal: codex_core_plugins::remote::RemotePluginSharePrincipal,
|
||||
) -> PluginSharePrincipal {
|
||||
PluginSharePrincipal {
|
||||
principal_type: match principal.principal_type {
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::User => {
|
||||
PluginSharePrincipalType::User
|
||||
}
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::Group => {
|
||||
PluginSharePrincipalType::Group
|
||||
}
|
||||
codex_core_plugins::remote::RemotePluginSharePrincipalType::Workspace => {
|
||||
PluginSharePrincipalType::Workspace
|
||||
}
|
||||
},
|
||||
principal_id: principal.principal_id,
|
||||
name: principal.name,
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
@@ -141,6 +201,15 @@ impl PluginRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn plugin_share_update_targets(
|
||||
&self,
|
||||
params: PluginShareUpdateTargetsParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.plugin_share_update_targets_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn plugin_share_list(
|
||||
&self,
|
||||
params: PluginShareListParams,
|
||||
@@ -177,33 +246,35 @@ impl PluginRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) fn effective_plugins_changed_callback(
|
||||
&self,
|
||||
config: Config,
|
||||
) -> Arc<dyn Fn() + Send + Sync> {
|
||||
pub(crate) fn effective_plugins_changed_callback(&self) -> Arc<dyn Fn() + Send + Sync> {
|
||||
let thread_manager = Arc::clone(&self.thread_manager);
|
||||
let config_manager = self.config_manager.clone();
|
||||
Arc::new(move || {
|
||||
Self::spawn_effective_plugins_changed_task(Arc::clone(&thread_manager), config.clone());
|
||||
Self::spawn_effective_plugins_changed_task(
|
||||
Arc::clone(&thread_manager),
|
||||
config_manager.clone(),
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
fn on_effective_plugins_changed(&self, config: Config) {
|
||||
Self::spawn_effective_plugins_changed_task(Arc::clone(&self.thread_manager), config);
|
||||
fn on_effective_plugins_changed(&self) {
|
||||
Self::spawn_effective_plugins_changed_task(
|
||||
Arc::clone(&self.thread_manager),
|
||||
self.config_manager.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_effective_plugins_changed_task(thread_manager: Arc<ThreadManager>, config: Config) {
|
||||
fn spawn_effective_plugins_changed_task(
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
config_manager: ConfigManager,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_manager().clear_cache();
|
||||
if thread_manager.list_thread_ids().await.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(err) =
|
||||
McpRequestProcessor::queue_mcp_server_refresh_for_config(&thread_manager, &config)
|
||||
.await
|
||||
{
|
||||
warn!("failed to queue MCP refresh after effective plugins changed: {err:?}");
|
||||
}
|
||||
crate::mcp_refresh::queue_best_effort_refresh(&thread_manager, &config_manager).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,7 +344,7 @@ impl PluginRequestProcessor {
|
||||
&plugins_input,
|
||||
auth.clone(),
|
||||
&roots,
|
||||
Some(self.effective_plugins_changed_callback(config.clone())),
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
);
|
||||
|
||||
let config_for_marketplace_listing = plugins_input.clone();
|
||||
@@ -570,22 +641,34 @@ impl PluginRequestProcessor {
|
||||
let PluginShareSaveParams {
|
||||
plugin_path,
|
||||
remote_plugin_id,
|
||||
discoverability,
|
||||
share_targets,
|
||||
} = params;
|
||||
if let Some(remote_plugin_id) = remote_plugin_id.as_ref()
|
||||
&& (remote_plugin_id.is_empty() || !is_valid_remote_plugin_id(remote_plugin_id))
|
||||
{
|
||||
return Err(invalid_request("invalid remote plugin id"));
|
||||
}
|
||||
if remote_plugin_id.is_some() && (discoverability.is_some() || share_targets.is_some()) {
|
||||
return Err(invalid_request(
|
||||
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share targets",
|
||||
));
|
||||
}
|
||||
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let access_policy = codex_core_plugins::remote::RemotePluginShareAccessPolicy {
|
||||
discoverability: discoverability.map(remote_plugin_share_discoverability),
|
||||
share_targets: share_targets.map(remote_plugin_share_targets),
|
||||
};
|
||||
let result = codex_core_plugins::remote::save_remote_plugin_share(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
config.codex_home.as_path(),
|
||||
&plugin_path,
|
||||
remote_plugin_id.as_deref(),
|
||||
access_policy,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| remote_plugin_catalog_error_to_jsonrpc(err, "save remote plugin share"))?;
|
||||
@@ -597,6 +680,42 @@ impl PluginRequestProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
async fn plugin_share_update_targets_response(
|
||||
&self,
|
||||
params: PluginShareUpdateTargetsParams,
|
||||
) -> Result<PluginShareUpdateTargetsResponse, JSONRPCErrorError> {
|
||||
let (config, auth) = self.load_plugin_share_config_and_auth().await?;
|
||||
let PluginShareUpdateTargetsParams {
|
||||
remote_plugin_id,
|
||||
share_targets,
|
||||
} = params;
|
||||
if remote_plugin_id.is_empty() || !is_valid_remote_plugin_id(&remote_plugin_id) {
|
||||
return Err(invalid_request("invalid remote plugin id"));
|
||||
}
|
||||
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let result = codex_core_plugins::remote::update_remote_plugin_share_targets(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&remote_plugin_id,
|
||||
remote_plugin_share_targets(share_targets),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "update remote plugin share targets")
|
||||
})?;
|
||||
self.clear_plugin_related_caches();
|
||||
Ok(PluginShareUpdateTargetsResponse {
|
||||
principals: result
|
||||
.principals
|
||||
.into_iter()
|
||||
.map(plugin_share_principal_from_remote)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn plugin_share_list_response(
|
||||
&self,
|
||||
_params: PluginShareListParams,
|
||||
@@ -723,7 +842,7 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
self.on_effective_plugins_changed(config.clone());
|
||||
self.on_effective_plugins_changed();
|
||||
|
||||
let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path()).await;
|
||||
if !plugin_mcp_servers.is_empty() {
|
||||
@@ -834,7 +953,7 @@ impl PluginRequestProcessor {
|
||||
.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
|
||||
&config.plugins_config_input(),
|
||||
auth.clone(),
|
||||
Some(self.effective_plugins_changed_callback(config.clone())),
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
);
|
||||
|
||||
let mut plugin_metadata =
|
||||
@@ -1027,7 +1146,7 @@ impl PluginRequestProcessor {
|
||||
.await
|
||||
.map_err(Self::plugin_uninstall_error)?;
|
||||
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => self.on_effective_plugins_changed(config),
|
||||
Ok(_) => self.on_effective_plugins_changed(),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to reload config after plugin uninstall, clearing plugin-related caches only: {err:?}"
|
||||
@@ -1128,12 +1247,12 @@ impl PluginRequestProcessor {
|
||||
) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
if plugins_manager.clear_remote_installed_plugins_cache() {
|
||||
self.on_effective_plugins_changed(config.clone());
|
||||
self.on_effective_plugins_changed();
|
||||
}
|
||||
plugins_manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
|
||||
&config.plugins_config_input(),
|
||||
auth.clone(),
|
||||
Some(self.effective_plugins_changed_callback(config.clone())),
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -598,6 +598,8 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
||||
let active_permission_profile =
|
||||
thread_response_active_permission_profile(active_permission_profile);
|
||||
let session_id = conversation.session_configured().session_id.to_string();
|
||||
thread.session_id = session_id;
|
||||
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
|
||||
@@ -184,6 +184,53 @@ fn has_model_resume_override(
|
||||
}
|
||||
|
||||
fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
const DYNAMIC_TOOL_NAME_MAX_LEN: usize = 128;
|
||||
const DYNAMIC_TOOL_NAMESPACE_MAX_LEN: usize = 64;
|
||||
const DYNAMIC_TOOL_IDENTIFIER_PATTERN: &str = "^[a-zA-Z0-9_-]+$";
|
||||
const RESERVED_RESPONSES_NAMESPACES: &[&str] = &[
|
||||
"api_tool",
|
||||
"browser",
|
||||
"computer",
|
||||
"container",
|
||||
"file_search",
|
||||
"functions",
|
||||
"image_gen",
|
||||
"multi_tool_use",
|
||||
"python",
|
||||
"python_user_visible",
|
||||
"submodel_delegator",
|
||||
"terminal",
|
||||
"tool_search",
|
||||
"web",
|
||||
];
|
||||
|
||||
fn escape_identifier_for_error(value: &str) -> String {
|
||||
value.escape_default().to_string()
|
||||
}
|
||||
|
||||
fn validate_dynamic_tool_identifier(
|
||||
value: &str,
|
||||
label: &str,
|
||||
max_len: usize,
|
||||
) -> Result<(), String> {
|
||||
if !value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
||||
{
|
||||
return Err(format!(
|
||||
"{label} must match {DYNAMIC_TOOL_IDENTIFIER_PATTERN} to match Responses API: {}",
|
||||
escape_identifier_for_error(value),
|
||||
));
|
||||
}
|
||||
if value.chars().count() > max_len {
|
||||
return Err(format!(
|
||||
"{label} must be at most {max_len} characters to match Responses API: {}",
|
||||
escape_identifier_for_error(value),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for tool in tools {
|
||||
let name = tool.name.trim();
|
||||
@@ -193,9 +240,10 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
if name != tool.name {
|
||||
return Err(format!(
|
||||
"dynamic tool name has leading/trailing whitespace: {}",
|
||||
tool.name
|
||||
escape_identifier_for_error(&tool.name),
|
||||
));
|
||||
}
|
||||
validate_dynamic_tool_identifier(name, "dynamic tool name", DYNAMIC_TOOL_NAME_MAX_LEN)?;
|
||||
if name == "mcp" || name.starts_with("mcp__") {
|
||||
return Err(format!("dynamic tool name is reserved: {name}"));
|
||||
}
|
||||
@@ -209,13 +257,25 @@ fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
|
||||
if Some(namespace) != tool.namespace.as_deref() {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace has leading/trailing whitespace for {name}: {namespace}",
|
||||
name = escape_identifier_for_error(name),
|
||||
namespace = escape_identifier_for_error(namespace),
|
||||
));
|
||||
}
|
||||
validate_dynamic_tool_identifier(
|
||||
namespace,
|
||||
"dynamic tool namespace",
|
||||
DYNAMIC_TOOL_NAMESPACE_MAX_LEN,
|
||||
)?;
|
||||
if namespace == "mcp" || namespace.starts_with("mcp__") {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace is reserved for {name}: {namespace}"
|
||||
));
|
||||
}
|
||||
if RESERVED_RESPONSES_NAMESPACES.contains(&namespace) {
|
||||
return Err(format!(
|
||||
"dynamic tool namespace collides with a reserved Responses API namespace for {name}: {namespace}",
|
||||
));
|
||||
}
|
||||
}
|
||||
if !seen.insert((namespace, name)) {
|
||||
if let Some(namespace) = namespace {
|
||||
@@ -1036,6 +1096,7 @@ impl ThreadRequestProcessor {
|
||||
.await;
|
||||
let mut thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
session_configured.session_id.to_string(),
|
||||
&config_snapshot,
|
||||
session_configured.rollout_path.clone(),
|
||||
);
|
||||
@@ -1127,7 +1188,7 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
model: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
service_tier: Option<Option<codex_protocol::config_types::ServiceTier>>,
|
||||
service_tier: Option<Option<String>>,
|
||||
cwd: Option<String>,
|
||||
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
|
||||
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
@@ -1468,9 +1529,9 @@ impl ThreadRequestProcessor {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
|
||||
let updated_thread = {
|
||||
let _thread_list_state_permit = self.acquire_thread_list_state_permit().await?;
|
||||
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
|
||||
if let Some(loaded_thread) = loaded_thread.as_ref() {
|
||||
if loaded_thread.config_snapshot().await.ephemeral {
|
||||
return Err(invalid_request(format!(
|
||||
@@ -1491,12 +1552,14 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
.map_err(|err| thread_store_write_error("update thread metadata", err))?
|
||||
};
|
||||
|
||||
let (mut thread, _) = thread_from_stored_thread(
|
||||
updated_thread,
|
||||
self.config.model_provider_id.as_str(),
|
||||
&self.config.cwd,
|
||||
);
|
||||
if let Some(loaded_thread) = loaded_thread.as_ref() {
|
||||
thread.session_id = loaded_thread.session_configured().session_id.to_string();
|
||||
}
|
||||
self.attach_thread_name(thread_uuid, &mut thread).await;
|
||||
thread.status = resolve_thread_status(
|
||||
self.thread_watch_manager
|
||||
@@ -1542,18 +1605,16 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let fallback_provider = self.config.model_provider_id.clone();
|
||||
let mut thread = self
|
||||
let stored_thread = self
|
||||
.thread_store
|
||||
.unarchive_thread(StoreArchiveThreadParams { thread_id })
|
||||
.await
|
||||
.map_err(|err| thread_store_archive_error("unarchive", err))
|
||||
.and_then(|stored_thread| {
|
||||
summary_from_stored_thread(stored_thread, fallback_provider.as_str())
|
||||
.map(|summary| summary_to_thread(summary, &self.config.cwd))
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!("failed to read unarchived thread {thread_id}"))
|
||||
})
|
||||
.map_err(|err| thread_store_archive_error("unarchive", err))?;
|
||||
let summary = summary_from_stored_thread(stored_thread, fallback_provider.as_str())
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!("failed to read unarchived thread {thread_id}"))
|
||||
})?;
|
||||
let mut thread = summary_to_thread(summary, &self.config.cwd);
|
||||
|
||||
thread.status = resolve_thread_status(
|
||||
self.thread_watch_manager
|
||||
@@ -1979,6 +2040,7 @@ impl ThreadRequestProcessor {
|
||||
if thread.path.is_none() {
|
||||
thread.path = fallback_thread.path.clone();
|
||||
}
|
||||
thread.session_id.clone_from(&fallback_thread.session_id);
|
||||
thread.ephemeral = fallback_thread.ephemeral;
|
||||
thread
|
||||
} else {
|
||||
@@ -2162,8 +2224,12 @@ impl ThreadRequestProcessor {
|
||||
) {
|
||||
if let Ok(thread) = self.thread_manager.get_thread(thread_id).await {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let loaded_thread =
|
||||
build_thread_from_snapshot(thread_id, &config_snapshot, thread.rollout_path());
|
||||
let loaded_thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
thread.session_configured().session_id.to_string(),
|
||||
&config_snapshot,
|
||||
thread.rollout_path(),
|
||||
);
|
||||
self.thread_watch_manager.upsert_thread(loaded_thread).await;
|
||||
}
|
||||
|
||||
@@ -2589,11 +2655,12 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
let mut summary_source_thread = source_thread;
|
||||
summary_source_thread.history = None;
|
||||
let thread_summary = self.stored_thread_to_api_thread(
|
||||
let mut thread_summary = self.stored_thread_to_api_thread(
|
||||
summary_source_thread,
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
/*include_turns*/ false,
|
||||
);
|
||||
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
|
||||
let mut config_for_instruction_sources = self.config.as_ref().clone();
|
||||
config_for_instruction_sources.cwd = config_snapshot.cwd.clone();
|
||||
let instruction_sources =
|
||||
@@ -2762,6 +2829,7 @@ impl ThreadRequestProcessor {
|
||||
include_turns: bool,
|
||||
) -> std::result::Result<Thread, String> {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let session_id = thread.session_configured().session_id.to_string();
|
||||
let thread = match thread_history {
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
let fallback_provider = config_snapshot.model_provider_id.as_str();
|
||||
@@ -2823,6 +2891,7 @@ impl ThreadRequestProcessor {
|
||||
InitialHistory::Forked(items) => {
|
||||
let mut thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
session_id.clone(),
|
||||
&config_snapshot,
|
||||
Some(rollout_path.into()),
|
||||
);
|
||||
@@ -2835,6 +2904,7 @@ impl ThreadRequestProcessor {
|
||||
};
|
||||
let mut thread = thread?;
|
||||
thread.id = thread_id.to_string();
|
||||
thread.session_id = session_id;
|
||||
thread.path = Some(rollout_path.to_path_buf());
|
||||
if include_turns {
|
||||
let history_items = thread_history.get_rollout_items();
|
||||
@@ -3016,8 +3086,12 @@ impl ThreadRequestProcessor {
|
||||
} else {
|
||||
let config_snapshot = forked_thread.config_snapshot().await;
|
||||
// forked thread names do not inherit the source thread name
|
||||
let mut thread =
|
||||
build_thread_from_snapshot(thread_id, &config_snapshot, /*path*/ None);
|
||||
let mut thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
session_configured.session_id.to_string(),
|
||||
&config_snapshot,
|
||||
/*path*/ None,
|
||||
);
|
||||
thread.preview = preview_from_rollout_items(&history_items);
|
||||
thread.forked_from_id = Some(source_thread_id.to_string());
|
||||
if include_turns {
|
||||
@@ -3029,6 +3103,7 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
thread
|
||||
};
|
||||
thread.session_id = session_configured.session_id.to_string();
|
||||
thread.thread_source = forked_thread
|
||||
.config_snapshot()
|
||||
.await
|
||||
@@ -3617,8 +3692,10 @@ pub(crate) fn thread_from_stored_thread(
|
||||
thread.agent_role.clone(),
|
||||
);
|
||||
let history = thread.history;
|
||||
let thread_id = thread.thread_id.to_string();
|
||||
let thread = Thread {
|
||||
id: thread.thread_id.to_string(),
|
||||
id: thread_id.clone(),
|
||||
session_id: thread_id,
|
||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||
preview: thread.first_user_message.unwrap_or(thread.preview),
|
||||
ephemeral: false,
|
||||
@@ -3815,12 +3892,14 @@ fn permission_profile_trusts_project(
|
||||
|
||||
fn build_thread_from_snapshot(
|
||||
thread_id: ThreadId,
|
||||
session_id: String,
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
path: Option<PathBuf>,
|
||||
) -> Thread {
|
||||
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id,
|
||||
forked_from_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: config_snapshot.ephemeral,
|
||||
@@ -3846,7 +3925,12 @@ fn build_thread_from_loaded_snapshot(
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
loaded_thread: &CodexThread,
|
||||
) -> Thread {
|
||||
build_thread_from_snapshot(thread_id, config_snapshot, loaded_thread.rollout_path())
|
||||
build_thread_from_snapshot(
|
||||
thread_id,
|
||||
loaded_thread.session_configured().session_id.to_string(),
|
||||
config_snapshot,
|
||||
loaded_thread.rollout_path(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -156,6 +156,22 @@ mod thread_processor_behavior_tests {
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_accepts_responses_compatible_identifiers() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("Codex-App_2".to_string()),
|
||||
name: "lookup-ticket_2".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_duplicate_name_in_same_namespace() {
|
||||
let tools = vec![
|
||||
@@ -260,6 +276,104 @@ mod thread_processor_behavior_tests {
|
||||
assert!(err.contains("reserved"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_name_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid name");
|
||||
assert!(err.contains("lookup.ticket"), "unexpected error: {err}");
|
||||
assert!(
|
||||
err.contains("Responses API") && err.contains("^[a-zA-Z0-9_-]+$"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_namespace_not_supported_by_responses() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("invalid namespace");
|
||||
assert!(err.contains("codex.app"), "unexpected error: {err}");
|
||||
assert!(
|
||||
err.contains("Responses API") && err.contains("^[a-zA-Z0-9_-]+$"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_name_longer_than_responses_limit() {
|
||||
let long_name = "a".repeat(129);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: None,
|
||||
name: long_name.clone(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: false,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("name too long");
|
||||
assert!(err.contains("at most 128"), "unexpected error: {err}");
|
||||
assert!(err.contains(&long_name), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_namespace_longer_than_responses_limit() {
|
||||
let long_namespace = "a".repeat(65);
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some(long_namespace.clone()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("namespace too long");
|
||||
assert!(err.contains("at most 64"), "unexpected error: {err}");
|
||||
assert!(err.contains(&long_namespace), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_dynamic_tools_rejects_reserved_responses_namespace() {
|
||||
let tools = vec![ApiDynamicToolSpec {
|
||||
namespace: Some("functions".to_string()),
|
||||
name: "lookup_ticket".to_string(),
|
||||
description: "test".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
defer_loading: true,
|
||||
}];
|
||||
let err = validate_dynamic_tools(&tools).expect_err("reserved Responses namespace");
|
||||
assert!(err.contains("functions"), "unexpected error: {err}");
|
||||
assert!(err.contains("Responses API"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_from_stored_thread_preserves_millisecond_precision() {
|
||||
let created_at =
|
||||
@@ -515,7 +629,7 @@ mod thread_processor_behavior_tests {
|
||||
path: None,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
service_tier: Some(Some(codex_protocol::config_types::ServiceTier::Fast)),
|
||||
service_tier: Some(Some("priority".to_string())),
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
@@ -531,7 +645,7 @@ mod thread_processor_behavior_tests {
|
||||
let config_snapshot = ThreadConfigSnapshot {
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider_id: "openai".to_string(),
|
||||
service_tier: Some(codex_protocol::config_types::ServiceTier::Flex),
|
||||
service_tier: Some("flex".to_string()),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
|
||||
@@ -546,7 +660,7 @@ mod thread_processor_behavior_tests {
|
||||
|
||||
assert_eq!(
|
||||
collect_resume_override_mismatches(&request, &config_snapshot),
|
||||
vec!["service_tier requested=Some(Fast) active=Some(Flex)".to_string()]
|
||||
vec!["service_tier requested=Some(\"priority\") active=Some(\"flex\")".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,8 +263,10 @@ pub(crate) fn summary_to_thread(
|
||||
fallback_cwd.clone()
|
||||
});
|
||||
|
||||
let thread_id = conversation_id.to_string();
|
||||
Thread {
|
||||
id: conversation_id.to_string(),
|
||||
id: thread_id.clone(),
|
||||
session_id: thread_id,
|
||||
forked_from_id: None,
|
||||
preview,
|
||||
ephemeral: false,
|
||||
|
||||
@@ -438,7 +438,7 @@ impl TurnRequestProcessor {
|
||||
model: model.clone(),
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality,
|
||||
})
|
||||
@@ -935,6 +935,7 @@ impl TurnRequestProcessor {
|
||||
Ok(stored_thread) => {
|
||||
let (mut thread, _) =
|
||||
thread_from_stored_thread(stored_thread, fallback_provider, &self.config.cwd);
|
||||
thread.session_id = review_thread.session_configured().session_id.to_string();
|
||||
self.thread_watch_manager
|
||||
.upsert_thread_silently(thread.clone())
|
||||
.await;
|
||||
|
||||
@@ -889,6 +889,7 @@ mod tests {
|
||||
fn test_thread(thread_id: &str, source: codex_app_server_protocol::SessionSource) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
|
||||
@@ -239,6 +239,46 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_dynamic_tools_not_supported_by_responses() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec {
|
||||
namespace: Some("codex.app".to_string()),
|
||||
name: "lookup.ticket".to_string(),
|
||||
description: "Invalid dynamic tool".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false,
|
||||
}),
|
||||
defer_loading: false,
|
||||
};
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
dynamic_tools: Some(vec![dynamic_tool]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(error.error.message.contains("Responses API"));
|
||||
assert!(error.error.message.contains("lookup.ticket"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exercises the full dynamic tool call path (server request, client response, model output).
|
||||
#[tokio::test]
|
||||
async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Result<()> {
|
||||
|
||||
@@ -65,6 +65,9 @@ const TEST_TOOL_NAME: &str = "echo_tool";
|
||||
const LARGE_RESPONSE_MESSAGE: &str = "large";
|
||||
const ELICITATION_TRIGGER_MESSAGE: &str = "confirm";
|
||||
const ELICITATION_MESSAGE: &str = "Allow this request?";
|
||||
const URL_ELICITATION_TRIGGER_MESSAGE: &str = "auth";
|
||||
const URL_ELICITATION_MESSAGE: &str = "Sign in to GitHub to continue.";
|
||||
const URL_ELICITATION_URL: &str = "https://github.example/login/device";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mcp_server_tool_call_returns_tool_result() -> Result<()> {
|
||||
@@ -294,6 +297,109 @@ url = "{mcp_server_url}/mcp"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mcp_server_tool_call_forwards_url_elicitation() -> Result<()> {
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let (mcp_server_url, mcp_server_handle) = start_mcp_server().await?;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_mock_responses_config_toml(
|
||||
codex_home.path(),
|
||||
&responses_server.uri(),
|
||||
&BTreeMap::new(),
|
||||
/*auto_compact_limit*/ 1024,
|
||||
/*requires_openai_auth*/ None,
|
||||
"mock_provider",
|
||||
"compact",
|
||||
)?;
|
||||
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let mut config_toml = std::fs::read_to_string(&config_path)?;
|
||||
config_toml.push_str(&format!(
|
||||
r#"
|
||||
[mcp_servers.{TEST_SERVER_NAME}]
|
||||
url = "{mcp_server_url}/mcp"
|
||||
"#
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;
|
||||
|
||||
let tool_call_request_id = mcp
|
||||
.send_mcp_server_tool_call_request(McpServerToolCallParams {
|
||||
thread_id: thread.id.clone(),
|
||||
server: TEST_SERVER_NAME.to_string(),
|
||||
tool: TEST_TOOL_NAME.to_string(),
|
||||
arguments: Some(json!({
|
||||
"message": URL_ELICITATION_TRIGGER_MESSAGE,
|
||||
})),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::McpServerElicitationRequest { request_id, params } = server_req else {
|
||||
panic!("expected McpServerElicitationRequest request, got: {server_req:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
params,
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: thread.id,
|
||||
turn_id: None,
|
||||
server_name: TEST_SERVER_NAME.to_string(),
|
||||
request: McpServerElicitationRequest::Url {
|
||||
meta: None,
|
||||
message: URL_ELICITATION_MESSAGE.to_string(),
|
||||
url: URL_ELICITATION_URL.to_string(),
|
||||
elicitation_id: "github-auth-123".to_string(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Accept,
|
||||
content: None,
|
||||
meta: None,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tool_call_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(tool_call_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: McpServerToolCallResponse = to_response(tool_call_response)?;
|
||||
assert_eq!(response.content.len(), 1);
|
||||
assert_eq!(response.content[0].get("type"), Some(&json!("text")));
|
||||
assert_eq!(response.content[0].get("text"), Some(&json!("accepted")));
|
||||
|
||||
mcp_server_handle.abort();
|
||||
let _ = mcp_server_handle.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mcp_tool_call_completion_notification_contains_truncated_large_result() -> Result<()> {
|
||||
let call_id = "call-large-mcp";
|
||||
@@ -528,6 +634,28 @@ impl ServerHandler for ToolAppsMcpServer {
|
||||
return Ok(CallToolResult::success(vec![Content::text(output)]));
|
||||
}
|
||||
|
||||
if message == URL_ELICITATION_TRIGGER_MESSAGE {
|
||||
let result = context
|
||||
.peer
|
||||
.create_elicitation(CreateElicitationRequestParams::UrlElicitationParams {
|
||||
meta: None,
|
||||
message: URL_ELICITATION_MESSAGE.to_string(),
|
||||
url: URL_ELICITATION_URL.to_string(),
|
||||
elicitation_id: "github-auth-123".to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?;
|
||||
let output = match result.action {
|
||||
ElicitationAction::Accept => {
|
||||
assert_eq!(result.content, Some(json!({})));
|
||||
"accepted"
|
||||
}
|
||||
ElicitationAction::Decline => "declined",
|
||||
ElicitationAction::Cancel => "cancelled",
|
||||
};
|
||||
return Ok(CallToolResult::success(vec![Content::text(output)]));
|
||||
}
|
||||
|
||||
let mut result = CallToolResult::structured(json!({
|
||||
"echoed": message,
|
||||
"threadId": thread_id,
|
||||
|
||||
@@ -7,6 +7,7 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginAuthPolicy;
|
||||
use codex_app_server_protocol::PluginInstallPolicy;
|
||||
@@ -14,7 +15,10 @@ use codex_app_server_protocol::PluginInterface;
|
||||
use codex_app_server_protocol::PluginShareDeleteResponse;
|
||||
use codex_app_server_protocol::PluginShareListItem;
|
||||
use codex_app_server_protocol::PluginShareListResponse;
|
||||
use codex_app_server_protocol::PluginSharePrincipal;
|
||||
use codex_app_server_protocol::PluginSharePrincipalType;
|
||||
use codex_app_server_protocol::PluginShareSaveResponse;
|
||||
use codex_app_server_protocol::PluginShareUpdateTargetsResponse;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -174,6 +178,154 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_save_forwards_access_policy() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let plugin_root = TempDir::new()?;
|
||||
let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/public/plugins/workspace/upload-url"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
||||
"file_id": "file_123",
|
||||
"upload_url": format!("{}/upload/file_123", server.uri()),
|
||||
"etag": "\"upload_etag_123\"",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/upload/file_123"))
|
||||
.respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_123\""))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/public/plugins/workspace"))
|
||||
.and(body_json(json!({
|
||||
"file_id": "file_123",
|
||||
"etag": "\"upload_etag_123\"",
|
||||
"discoverability": "PRIVATE",
|
||||
"share_targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "workspace-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
||||
"plugin_id": "plugins_123",
|
||||
"share_url": "https://chatgpt.example/plugins/share/share-key-1",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let expected_plugin_path = AbsolutePathBuf::try_from(plugin_path)?;
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/share/save",
|
||||
Some(json!({
|
||||
"pluginPath": expected_plugin_path,
|
||||
"discoverability": "PRIVATE",
|
||||
"shareTargets": [
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
},
|
||||
{
|
||||
"principalType": "workspace",
|
||||
"principalId": "workspace-1",
|
||||
},
|
||||
],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginShareSaveResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginShareSaveResponse {
|
||||
remote_plugin_id: "plugins_123".to_string(),
|
||||
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let plugin_root = TempDir::new()?;
|
||||
let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/share/save",
|
||||
Some(json!({
|
||||
"pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
|
||||
"remotePluginId": "plugins_123",
|
||||
"discoverability": "PRIVATE",
|
||||
"shareTargets": [
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
},
|
||||
],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share targets"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -250,6 +402,82 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/backend-api/public/plugins/plugins_123/shares"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.and(body_json(json!({
|
||||
"targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"name": "Gavin",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/share/updateTargets",
|
||||
Some(json!({
|
||||
"remotePluginId": "plugins_123",
|
||||
"shareTargets": [
|
||||
{
|
||||
"principalType": "user",
|
||||
"principalId": "user-1",
|
||||
},
|
||||
],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginShareUpdateTargetsResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginShareUpdateTargetsResponse {
|
||||
principals: vec![PluginSharePrincipal {
|
||||
principal_type: PluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -363,6 +363,7 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<(
|
||||
let started: ThreadStartedNotification =
|
||||
serde_json::from_value(notification.params.expect("params must be present"))?;
|
||||
assert_eq!(started.thread.id, review_thread_id);
|
||||
assert_eq!(started.thread.session_id, review_thread_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -108,11 +108,21 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
.get("thread")
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/fork result.thread must be an object");
|
||||
assert_eq!(
|
||||
thread_json.get("sessionId").and_then(Value::as_str),
|
||||
Some(thread.session_id.as_str()),
|
||||
"forked threads should serialize `sessionId` on the thread object"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("name"),
|
||||
Some(&Value::Null),
|
||||
"forked threads do not inherit a name; expected `name: null`"
|
||||
);
|
||||
assert_eq!(
|
||||
fork_result.get("sessionId"),
|
||||
None,
|
||||
"thread/fork should not serialize a top-level `sessionId`"
|
||||
);
|
||||
|
||||
let after_contents = std::fs::read_to_string(&original_path)?;
|
||||
assert_eq!(
|
||||
@@ -121,6 +131,7 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
);
|
||||
|
||||
assert_ne!(thread.id, conversation_id);
|
||||
assert_eq!(thread.session_id, thread.id);
|
||||
assert_eq!(thread.forked_from_id, Some(conversation_id.clone()));
|
||||
assert_eq!(thread.preview, preview);
|
||||
assert_eq!(thread.model_provider, "mock_provider");
|
||||
|
||||
@@ -879,6 +879,7 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
|
||||
assert_eq!(ids, vec![subagent_id.as_str()]);
|
||||
assert_ne!(cli_id, subagent_id);
|
||||
assert!(matches!(data[0].source, SessionSource::SubAgent(_)));
|
||||
assert_eq!(data[0].session_id, subagent_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread()
|
||||
to_response::<ThreadMetadataUpdateResponse>(update_resp)?;
|
||||
|
||||
assert_eq!(updated.id, thread.id);
|
||||
assert_eq!(updated.session_id, thread.session_id);
|
||||
assert_eq!(
|
||||
updated.git_info,
|
||||
Some(GitInfo {
|
||||
@@ -90,6 +91,10 @@ async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread()
|
||||
.get("thread")
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/metadata/update result.thread must be an object");
|
||||
assert_eq!(
|
||||
updated_thread_json.get("sessionId").and_then(Value::as_str),
|
||||
Some(thread.session_id.as_str())
|
||||
);
|
||||
let updated_git_info_json = updated_thread_json
|
||||
.get("gitInfo")
|
||||
.and_then(Value::as_object)
|
||||
|
||||
@@ -271,6 +271,10 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> {
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert!(
|
||||
!thread.session_id.is_empty(),
|
||||
"session id should not be empty"
|
||||
);
|
||||
assert_eq!(thread.thread_source, Some(ThreadSource::User));
|
||||
|
||||
let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?;
|
||||
|
||||
@@ -119,11 +119,16 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/rollback result.thread must be an object");
|
||||
assert_eq!(rolled_back_thread.name, None);
|
||||
assert_eq!(rolled_back_thread.session_id, thread.session_id);
|
||||
assert_eq!(
|
||||
thread_json.get("name"),
|
||||
Some(&Value::Null),
|
||||
"thread/rollback must serialize `name: null` when unset"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("sessionId").and_then(Value::as_str),
|
||||
Some(thread.session_id.as_str())
|
||||
);
|
||||
|
||||
assert_eq!(rolled_back_thread.turns.len(), 1);
|
||||
assert_eq!(rolled_back_thread.status, ThreadStatus::Idle);
|
||||
|
||||
@@ -28,7 +28,6 @@ use codex_core::config::set_project_trust_level;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -125,6 +124,10 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
model_provider,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(resp)?;
|
||||
assert!(
|
||||
!thread.session_id.is_empty(),
|
||||
"session id should not be empty"
|
||||
);
|
||||
assert!(!thread.id.is_empty(), "thread id should not be empty");
|
||||
assert!(
|
||||
thread.preview.is_empty(),
|
||||
@@ -153,11 +156,21 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
.get("thread")
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/start result.thread must be an object");
|
||||
assert_eq!(
|
||||
thread_json.get("sessionId").and_then(Value::as_str),
|
||||
Some(thread.session_id.as_str()),
|
||||
"new threads should serialize `sessionId` on the thread object"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("name"),
|
||||
Some(&Value::Null),
|
||||
"new threads should serialize `name: null`"
|
||||
);
|
||||
assert_eq!(
|
||||
resp_result.get("sessionId"),
|
||||
None,
|
||||
"thread/start should not serialize a top-level `sessionId`"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
@@ -388,7 +401,7 @@ model_reasoning_effort = "high"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_accepts_flex_service_tier() -> Result<()> {
|
||||
async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -397,9 +410,10 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> {
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let service_tier_id = "experimental-tier-id".to_string();
|
||||
let req_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
service_tier: Some(Some(ServiceTier::Flex)),
|
||||
service_tier: Some(Some(service_tier_id.clone())),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -411,7 +425,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> {
|
||||
.await??;
|
||||
let ThreadStartResponse { service_tier, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(service_tier, Some(ServiceTier::Flex));
|
||||
assert_eq!(service_tier, Some(service_tier_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,71 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let body = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let response_mock = responses::mount_sse_once(&server, body).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let service_tier_id = "experimental-tier-id".to_string();
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
service_tier: Some(Some(service_tier_id.clone())),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(
|
||||
response_mock.single_request().body_json()["service_tier"],
|
||||
json!(service_tier_id)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "builtin-mcps",
|
||||
crate_name = "codex_builtin_mcps",
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-builtin-mcps"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_builtin_mcps"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-memories-mcp = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Built-in MCP servers shipped with Codex.
|
||||
//!
|
||||
//! Built-ins use the same stdio MCP path as user-configured servers, but are
|
||||
//! declared here so product-owned MCPs do not need to live in `codex-core`.
|
||||
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub const MEMORIES_MCP_SERVER_NAME: &str = "memories";
|
||||
const BUILTIN_MCP_SUBCOMMAND: &str = "builtin-mcp";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BuiltinMcpServerOptions<'a> {
|
||||
pub codex_self_exe: Option<&'a Path>,
|
||||
pub codex_home: &'a Path,
|
||||
pub memories_enabled: bool,
|
||||
}
|
||||
|
||||
pub fn configured_builtin_mcp_servers(
|
||||
options: BuiltinMcpServerOptions<'_>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
let Some(codex_self_exe) = options.codex_self_exe else {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
let mut servers = HashMap::new();
|
||||
if options.memories_enabled {
|
||||
servers.insert(
|
||||
MEMORIES_MCP_SERVER_NAME.to_string(),
|
||||
builtin_stdio_server_config(
|
||||
codex_self_exe,
|
||||
options.codex_home,
|
||||
MEMORIES_MCP_SERVER_NAME,
|
||||
),
|
||||
);
|
||||
}
|
||||
servers
|
||||
}
|
||||
|
||||
pub async fn run_builtin_mcp_server(
|
||||
name: &str,
|
||||
codex_home: &AbsolutePathBuf,
|
||||
) -> anyhow::Result<()> {
|
||||
match name {
|
||||
MEMORIES_MCP_SERVER_NAME => codex_memories_mcp::run_stdio_server(codex_home).await,
|
||||
_ => anyhow::bail!("unknown built-in MCP server: {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn builtin_stdio_server_config(
|
||||
codex_self_exe: &Path,
|
||||
codex_home: &Path,
|
||||
name: &str,
|
||||
) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: codex_self_exe.to_string_lossy().into_owned(),
|
||||
args: vec![
|
||||
BUILTIN_MCP_SUBCOMMAND.to_string(),
|
||||
name.to_string(),
|
||||
"--codex-home".to_string(),
|
||||
codex_home.to_string_lossy().into_owned(),
|
||||
],
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn configured_builtin_mcp_servers_adds_memories_when_enabled() {
|
||||
let codex_home = AbsolutePathBuf::try_from("/tmp/codex-home").expect("absolute codex home");
|
||||
let servers = configured_builtin_mcp_servers(BuiltinMcpServerOptions {
|
||||
codex_self_exe: Some(Path::new("/tmp/codex")),
|
||||
codex_home: codex_home.as_path(),
|
||||
memories_enabled: true,
|
||||
});
|
||||
|
||||
let server = servers
|
||||
.get(MEMORIES_MCP_SERVER_NAME)
|
||||
.expect("memories server should exist");
|
||||
assert_eq!(
|
||||
server.transport,
|
||||
McpServerTransportConfig::Stdio {
|
||||
command: "/tmp/codex".to_string(),
|
||||
args: vec![
|
||||
"builtin-mcp".to_string(),
|
||||
"memories".to_string(),
|
||||
"--codex-home".to_string(),
|
||||
codex_home.as_path().to_string_lossy().into_owned(),
|
||||
],
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_builtin_mcp_servers_requires_reexec_path() {
|
||||
let codex_home = AbsolutePathBuf::try_from("/tmp/codex-home").expect("absolute codex home");
|
||||
let servers = configured_builtin_mcp_servers(BuiltinMcpServerOptions {
|
||||
codex_self_exe: None,
|
||||
codex_home: codex_home.as_path(),
|
||||
memories_enabled: true,
|
||||
});
|
||||
|
||||
assert!(servers.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ fn try_build_bwrap() -> Result<(), String> {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").map_err(|err| err.to_string())?);
|
||||
let src_dir = resolve_bwrap_source_dir(&manifest_dir)?;
|
||||
let libcap = pkg_config::Config::new()
|
||||
.cargo_metadata(false)
|
||||
.probe("libcap")
|
||||
.map_err(|err| format!("libcap not available via pkg-config: {err}"))?;
|
||||
|
||||
@@ -65,6 +66,12 @@ fn try_build_bwrap() -> Result<(), String> {
|
||||
}
|
||||
|
||||
build.compile("standalone_bwrap");
|
||||
for link_path in libcap.link_paths {
|
||||
println!("cargo:rustc-link-search=native={}", link_path.display());
|
||||
}
|
||||
for lib in libcap.libs {
|
||||
println!("cargo:rustc-link-lib={lib}");
|
||||
}
|
||||
println!("cargo:rustc-cfg=bwrap_available");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ codex-app-server = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-app-server-test-client = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-builtin-mcps = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-cloud-tasks = { path = "../cloud-tasks" }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -123,6 +123,10 @@ enum Subcommand {
|
||||
/// Start Codex as an MCP server (stdio).
|
||||
McpServer,
|
||||
|
||||
/// Internal: start a Codex-shipped MCP server (stdio).
|
||||
#[clap(hide = true, name = "builtin-mcp")]
|
||||
BuiltinMcp(BuiltinMcpCommand),
|
||||
|
||||
/// [experimental] Run the app server or related tooling.
|
||||
AppServer(AppServerCommand),
|
||||
|
||||
@@ -175,6 +179,13 @@ enum Subcommand {
|
||||
Features(FeaturesCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct BuiltinMcpCommand {
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
codex_home: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin")]
|
||||
struct PluginCli {
|
||||
@@ -809,6 +820,15 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
)?;
|
||||
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
|
||||
}
|
||||
Some(Subcommand::BuiltinMcp(command)) => {
|
||||
reject_remote_mode_for_subcommand(
|
||||
root_remote.as_deref(),
|
||||
root_remote_auth_token_env.as_deref(),
|
||||
"builtin-mcp",
|
||||
)?;
|
||||
let codex_home = AbsolutePathBuf::try_from(command.codex_home)?;
|
||||
codex_builtin_mcps::run_builtin_mcp_server(&command.name, &codex_home).await?;
|
||||
}
|
||||
Some(Subcommand::Mcp(mut mcp_cli)) => {
|
||||
reject_remote_mode_for_subcommand(
|
||||
root_remote.as_deref(),
|
||||
|
||||
@@ -32,6 +32,10 @@ pub struct CompactionInput<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<Reasoning>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_tier: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_cache_key: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<TextControls>,
|
||||
}
|
||||
|
||||
@@ -235,6 +239,11 @@ pub struct ResponseCreateWsRequest {
|
||||
pub client_metadata: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseProcessedWsRequest {
|
||||
pub response_id: String,
|
||||
}
|
||||
|
||||
pub fn response_create_client_metadata(
|
||||
client_metadata: Option<HashMap<String, String>>,
|
||||
trace: Option<&W3cTraceContext>,
|
||||
@@ -263,6 +272,8 @@ pub fn response_create_client_metadata(
|
||||
pub enum ResponsesWsRequest {
|
||||
#[serde(rename = "response.create")]
|
||||
ResponseCreate(ResponseCreateWsRequest),
|
||||
#[serde(rename = "response.processed")]
|
||||
ResponseProcessed(ResponseProcessedWsRequest),
|
||||
}
|
||||
|
||||
pub fn create_text_param_for_request(
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::error::ApiError;
|
||||
use crate::provider::Provider;
|
||||
use crate::requests::Compression;
|
||||
use crate::requests::attach_item_ids;
|
||||
use crate::requests::headers::build_conversation_headers;
|
||||
use crate::requests::headers::build_session_headers;
|
||||
use crate::requests::headers::insert_header;
|
||||
use crate::requests::headers::subagent_header;
|
||||
use crate::sse::spawn_response_stream;
|
||||
@@ -30,7 +30,8 @@ pub struct ResponsesClient<T: HttpTransport> {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ResponsesOptions {
|
||||
pub conversation_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub thread_id: Option<String>,
|
||||
pub session_source: Option<SessionSource>,
|
||||
pub extra_headers: HeaderMap,
|
||||
pub compression: Compression,
|
||||
@@ -72,7 +73,8 @@ impl<T: HttpTransport> ResponsesClient<T> {
|
||||
options: ResponsesOptions,
|
||||
) -> Result<ResponseStream, ApiError> {
|
||||
let ResponsesOptions {
|
||||
conversation_id,
|
||||
session_id,
|
||||
thread_id,
|
||||
session_source,
|
||||
extra_headers,
|
||||
compression,
|
||||
@@ -86,10 +88,10 @@ impl<T: HttpTransport> ResponsesClient<T> {
|
||||
}
|
||||
|
||||
let mut headers = extra_headers;
|
||||
if let Some(ref conv_id) = conversation_id {
|
||||
insert_header(&mut headers, "x-client-request-id", conv_id);
|
||||
if let Some(ref thread_id) = thread_id {
|
||||
insert_header(&mut headers, "x-client-request-id", thread_id);
|
||||
}
|
||||
headers.extend(build_conversation_headers(conversation_id));
|
||||
headers.extend(build_session_headers(session_id, thread_id));
|
||||
if let Some(subagent) = subagent_header(&session_source) {
|
||||
insert_header(&mut headers, "x-openai-subagent", &subagent);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::auth::SharedAuthProvider;
|
||||
use crate::common::ResponseEvent;
|
||||
use crate::common::ResponseProcessedWsRequest;
|
||||
use crate::common::ResponseStream;
|
||||
use crate::common::ResponsesWsRequest;
|
||||
use crate::error::ApiError;
|
||||
@@ -204,6 +205,40 @@ impl ResponsesWebsocketConnection {
|
||||
self.stream.lock().await.is_none()
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "responses_websocket.send_response_processed",
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(transport = "responses_websocket", api.path = "responses")
|
||||
)]
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "the guard serializes exclusive use of the websocket while sending a request frame"
|
||||
)]
|
||||
pub async fn send_response_processed(&self, response_id: String) -> Result<(), ApiError> {
|
||||
let request =
|
||||
ResponsesWsRequest::ResponseProcessed(ResponseProcessedWsRequest { response_id });
|
||||
let request_body = serde_json::to_value(&request).map_err(|err| {
|
||||
ApiError::Stream(format!("failed to encode websocket request: {err}"))
|
||||
})?;
|
||||
|
||||
let mut guard = self.stream.lock().await;
|
||||
let Some(ws_stream) = guard.as_mut() else {
|
||||
return Err(ApiError::Stream(
|
||||
"websocket connection is closed".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
send_websocket_request(
|
||||
ws_stream,
|
||||
request_body,
|
||||
self.idle_timeout,
|
||||
self.telemetry.as_ref(),
|
||||
/*connection_reused*/ true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
name = "responses_websocket.stream_request",
|
||||
level = "info",
|
||||
@@ -545,36 +580,14 @@ async fn run_websocket_response_stream(
|
||||
connection_reused: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let mut last_server_model: Option<String> = None;
|
||||
let request_text = match serde_json::to_string(&request_body) {
|
||||
Ok(text) => text,
|
||||
Err(err) => {
|
||||
return Err(ApiError::Stream(format!(
|
||||
"failed to encode websocket request: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
trace!("websocket request: {request_text}");
|
||||
|
||||
let request_start = Instant::now();
|
||||
let result = tokio::time::timeout(
|
||||
send_websocket_request(
|
||||
ws_stream,
|
||||
request_body,
|
||||
idle_timeout,
|
||||
ws_stream.send(Message::Text(request_text.into())),
|
||||
telemetry.as_ref(),
|
||||
connection_reused,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ApiError::Stream("idle timeout sending websocket request".into()))
|
||||
.and_then(|result| {
|
||||
result.map_err(|err| ApiError::Stream(format!("failed to send websocket request: {err}")))
|
||||
});
|
||||
|
||||
if let Some(t) = telemetry.as_ref() {
|
||||
t.on_ws_request(
|
||||
request_start.elapsed(),
|
||||
result.as_ref().err(),
|
||||
connection_reused,
|
||||
);
|
||||
}
|
||||
|
||||
result?;
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
let poll_start = Instant::now();
|
||||
@@ -671,6 +684,47 @@ async fn run_websocket_response_stream(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_websocket_request(
|
||||
ws_stream: &WsStream,
|
||||
request_body: Value,
|
||||
idle_timeout: Duration,
|
||||
telemetry: Option<&Arc<dyn WebsocketTelemetry>>,
|
||||
connection_reused: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let request_text = match serde_json::to_string(&request_body) {
|
||||
Ok(text) => text,
|
||||
Err(err) => {
|
||||
return Err(ApiError::Stream(format!(
|
||||
"failed to encode websocket request: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
trace!("websocket request: {request_text}");
|
||||
|
||||
let request_start = Instant::now();
|
||||
let result = tokio::time::timeout(
|
||||
idle_timeout,
|
||||
ws_stream.send(Message::Text(request_text.into())),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ApiError::Stream("idle timeout sending websocket request".into()))
|
||||
.and_then(|result| {
|
||||
result.map_err(|err| ApiError::Stream(format!("failed to send websocket request: {err}")))
|
||||
});
|
||||
|
||||
if let Some(t) = telemetry.as_ref() {
|
||||
t.on_ws_request(
|
||||
request_start.elapsed(),
|
||||
result.as_ref().err(),
|
||||
connection_reused,
|
||||
);
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub(crate) mod requests;
|
||||
pub(crate) mod sse;
|
||||
pub(crate) mod telemetry;
|
||||
|
||||
pub use crate::requests::headers::build_conversation_headers;
|
||||
pub use crate::requests::headers::build_session_headers;
|
||||
pub use codex_client::RequestTelemetry;
|
||||
pub use codex_client::ReqwestTransport;
|
||||
pub use codex_client::TransportError;
|
||||
@@ -30,6 +30,7 @@ pub use crate::common::RawMemoryMetadata;
|
||||
pub use crate::common::Reasoning;
|
||||
pub use crate::common::ResponseCreateWsRequest;
|
||||
pub use crate::common::ResponseEvent;
|
||||
pub use crate::common::ResponseProcessedWsRequest;
|
||||
pub use crate::common::ResponseStream;
|
||||
pub use crate::common::ResponsesApiRequest;
|
||||
pub use crate::common::ResponsesWsRequest;
|
||||
|
||||
@@ -2,11 +2,14 @@ use codex_protocol::protocol::SessionSource;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
|
||||
pub fn build_conversation_headers(conversation_id: Option<String>) -> HeaderMap {
|
||||
pub fn build_session_headers(session_id: Option<String>, thread_id: Option<String>) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(id) = conversation_id {
|
||||
if let Some(id) = session_id {
|
||||
insert_header(&mut headers, "session_id", &id);
|
||||
}
|
||||
if let Some(id) = thread_id {
|
||||
insert_header(&mut headers, "thread_id", &id);
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
|
||||
@@ -444,7 +444,8 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
|
||||
.stream_request(
|
||||
request,
|
||||
ResponsesOptions {
|
||||
conversation_id: Some("sess_123".into()),
|
||||
session_id: Some("sess_123".into()),
|
||||
thread_id: Some("thread_123".into()),
|
||||
session_source: Some(SessionSource::SubAgent(SubAgentSource::Review)),
|
||||
extra_headers,
|
||||
compression: Compression::None,
|
||||
@@ -461,6 +462,16 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
|
||||
req.headers.get("session_id").and_then(|v| v.to_str().ok()),
|
||||
Some("sess_123")
|
||||
);
|
||||
assert_eq!(
|
||||
req.headers.get("thread_id").and_then(|v| v.to_str().ok()),
|
||||
Some("thread_123")
|
||||
);
|
||||
assert_eq!(
|
||||
req.headers
|
||||
.get("x-client-request-id")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("thread_123")
|
||||
);
|
||||
assert_eq!(
|
||||
req.headers
|
||||
.get("x-openai-subagent")
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
codex-async-utils = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-builtin-mcps = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
//! Auth elicitation helpers.
|
||||
//!
|
||||
//! This module owns protocol-neutral auth elicitation parsing and payload shaping.
|
||||
//! Session orchestration stays in `codex-core`.
|
||||
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
|
||||
pub const CONNECTOR_AUTH_FAILURE_META_KEY: &str = "connector_auth_failure";
|
||||
pub const CONNECTOR_AUTH_FAILURE_IS_AUTH_FAILURE_KEY: &str = "is_auth_failure";
|
||||
pub const CONNECTOR_AUTH_FAILURE_AUTH_REASON_KEY: &str = "auth_reason";
|
||||
pub const CONNECTOR_AUTH_FAILURE_CONNECTOR_ID_KEY: &str = "connector_id";
|
||||
pub const CONNECTOR_AUTH_FAILURE_LINK_ID_KEY: &str = "link_id";
|
||||
pub const CONNECTOR_AUTH_FAILURE_ERROR_CODE_KEY: &str = "error_code";
|
||||
pub const CONNECTOR_AUTH_FAILURE_ERROR_HTTP_STATUS_CODE_KEY: &str = "error_http_status_code";
|
||||
pub const CONNECTOR_AUTH_FAILURE_ERROR_ACTION_KEY: &str = "error_action";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CodexAppsConnectorAuthFailure {
|
||||
pub connector_id: String,
|
||||
pub connector_name: String,
|
||||
pub install_url: String,
|
||||
pub auth_reason: Option<String>,
|
||||
pub link_id: Option<String>,
|
||||
pub error_code: Option<String>,
|
||||
pub error_http_status_code: Option<i64>,
|
||||
pub error_action: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CodexAppsAuthElicitation {
|
||||
pub meta: serde_json::Value,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
pub elicitation_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CodexAppsAuthElicitationPlan {
|
||||
pub auth_failure: CodexAppsConnectorAuthFailure,
|
||||
pub elicitation: CodexAppsAuthElicitation,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodexAppsConnectorAuthFailureMeta<'a> {
|
||||
is_auth_failure: bool,
|
||||
connector_id: &'a str,
|
||||
connector_name: &'a str,
|
||||
install_url: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
auth_reason: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
link_id: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error_code: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error_http_status_code: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error_action: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn connector_auth_failure_from_tool_result(
|
||||
result: &CallToolResult,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
install_url: Option<String>,
|
||||
) -> Option<CodexAppsConnectorAuthFailure> {
|
||||
if result.is_error != Some(true) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let auth_failure = result
|
||||
.meta
|
||||
.as_ref()?
|
||||
.as_object()?
|
||||
.get(MCP_TOOL_CODEX_APPS_META_KEY)?
|
||||
.as_object()?
|
||||
.get(CONNECTOR_AUTH_FAILURE_META_KEY)?
|
||||
.as_object()?;
|
||||
if auth_failure
|
||||
.get(CONNECTOR_AUTH_FAILURE_IS_AUTH_FAILURE_KEY)
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
!= Some(true)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let connector_id = connector_id
|
||||
.map(str::trim)
|
||||
.filter(|connector_id| !connector_id.is_empty())?;
|
||||
if let Some(auth_failure_connector_id) =
|
||||
string_auth_failure_field(auth_failure, CONNECTOR_AUTH_FAILURE_CONNECTOR_ID_KEY)
|
||||
&& auth_failure_connector_id != connector_id
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let connector_name = connector_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or(connector_id)
|
||||
.to_string();
|
||||
|
||||
Some(CodexAppsConnectorAuthFailure {
|
||||
connector_id: connector_id.to_string(),
|
||||
connector_name,
|
||||
install_url: install_url?,
|
||||
auth_reason: string_auth_failure_field(
|
||||
auth_failure,
|
||||
CONNECTOR_AUTH_FAILURE_AUTH_REASON_KEY,
|
||||
),
|
||||
link_id: string_auth_failure_field(auth_failure, CONNECTOR_AUTH_FAILURE_LINK_ID_KEY),
|
||||
error_code: string_auth_failure_field(auth_failure, CONNECTOR_AUTH_FAILURE_ERROR_CODE_KEY),
|
||||
error_http_status_code: auth_failure
|
||||
.get(CONNECTOR_AUTH_FAILURE_ERROR_HTTP_STATUS_CODE_KEY)
|
||||
.and_then(serde_json::Value::as_i64),
|
||||
error_action: string_auth_failure_field(
|
||||
auth_failure,
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_ACTION_KEY,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_auth_elicitation_plan(
|
||||
call_id: &str,
|
||||
result: &CallToolResult,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
install_url: Option<String>,
|
||||
) -> Option<CodexAppsAuthElicitationPlan> {
|
||||
let auth_failure =
|
||||
connector_auth_failure_from_tool_result(result, connector_id, connector_name, install_url)?;
|
||||
let elicitation = build_auth_elicitation(call_id, &auth_failure);
|
||||
Some(CodexAppsAuthElicitationPlan {
|
||||
auth_failure,
|
||||
elicitation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_auth_elicitation(
|
||||
call_id: &str,
|
||||
auth_failure: &CodexAppsConnectorAuthFailure,
|
||||
) -> CodexAppsAuthElicitation {
|
||||
CodexAppsAuthElicitation {
|
||||
meta: serde_json::json!({
|
||||
MCP_TOOL_CODEX_APPS_META_KEY: {
|
||||
CONNECTOR_AUTH_FAILURE_META_KEY: CodexAppsConnectorAuthFailureMeta {
|
||||
is_auth_failure: true,
|
||||
connector_id: &auth_failure.connector_id,
|
||||
connector_name: &auth_failure.connector_name,
|
||||
install_url: &auth_failure.install_url,
|
||||
auth_reason: auth_failure.auth_reason.as_deref(),
|
||||
link_id: auth_failure.link_id.as_deref(),
|
||||
error_code: auth_failure.error_code.as_deref(),
|
||||
error_http_status_code: auth_failure.error_http_status_code,
|
||||
error_action: auth_failure.error_action.as_deref(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
message: auth_elicitation_message(auth_failure),
|
||||
url: auth_failure.install_url.clone(),
|
||||
elicitation_id: auth_elicitation_id(call_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auth_elicitation_completed_result(
|
||||
auth_failure: &CodexAppsConnectorAuthFailure,
|
||||
meta: Option<serde_json::Value>,
|
||||
) -> CallToolResult {
|
||||
CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": format!(
|
||||
"Authentication for {} was requested and accepted. Retry this tool call now.",
|
||||
auth_failure.connector_name
|
||||
),
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auth_elicitation_id(call_id: &str) -> String {
|
||||
format!("codex_apps_auth_{call_id}")
|
||||
}
|
||||
|
||||
fn string_auth_failure_field(
|
||||
auth_failure: &serde_json::Map<String, serde_json::Value>,
|
||||
key: &str,
|
||||
) -> Option<String> {
|
||||
auth_failure
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn auth_elicitation_message(auth_failure: &CodexAppsConnectorAuthFailure) -> String {
|
||||
match auth_failure.auth_reason.as_deref() {
|
||||
Some("oauth_upgrade_required") => format!(
|
||||
"Reconnect {} on ChatGPT to grant the permissions needed for this request.",
|
||||
auth_failure.connector_name
|
||||
),
|
||||
Some("reauthentication_required") => format!(
|
||||
"Reconnect {} on ChatGPT to restore access for this request.",
|
||||
auth_failure.connector_name
|
||||
),
|
||||
Some("missing_link") => format!(
|
||||
"Sign in to {} on ChatGPT to use it in Codex.",
|
||||
auth_failure.connector_name
|
||||
),
|
||||
_ => format!(
|
||||
"Sign in to {} on ChatGPT to continue.",
|
||||
auth_failure.connector_name
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn auth_failure_result() -> CallToolResult {
|
||||
CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "Connector reauthentication required",
|
||||
})],
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
meta: Some(serde_json::json!({
|
||||
MCP_TOOL_CODEX_APPS_META_KEY: {
|
||||
CONNECTOR_AUTH_FAILURE_META_KEY: {
|
||||
CONNECTOR_AUTH_FAILURE_IS_AUTH_FAILURE_KEY: true,
|
||||
CONNECTOR_AUTH_FAILURE_AUTH_REASON_KEY: "reauthentication_required",
|
||||
CONNECTOR_AUTH_FAILURE_CONNECTOR_ID_KEY: "connector_calendar",
|
||||
"connector_name": "Untrusted Calendar",
|
||||
CONNECTOR_AUTH_FAILURE_LINK_ID_KEY: "link_123",
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_CODE_KEY: "UNAUTHORIZED",
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_HTTP_STATUS_CODE_KEY: 401,
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_ACTION_KEY: "TRIGGER_REAUTHENTICATION",
|
||||
},
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_auth_failure_from_trusted_connector_metadata() {
|
||||
assert_eq!(
|
||||
connector_auth_failure_from_tool_result(
|
||||
&auth_failure_result(),
|
||||
Some("connector_calendar"),
|
||||
Some("Google Calendar"),
|
||||
Some("https://chatgpt.com/apps/google-calendar/connector_calendar".to_string()),
|
||||
),
|
||||
Some(CodexAppsConnectorAuthFailure {
|
||||
connector_id: "connector_calendar".to_string(),
|
||||
connector_name: "Google Calendar".to_string(),
|
||||
install_url: "https://chatgpt.com/apps/google-calendar/connector_calendar"
|
||||
.to_string(),
|
||||
auth_reason: Some("reauthentication_required".to_string()),
|
||||
link_id: Some("link_123".to_string()),
|
||||
error_code: Some("UNAUTHORIZED".to_string()),
|
||||
error_http_status_code: Some(401),
|
||||
error_action: Some("TRIGGER_REAUTHENTICATION".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_or_mismatched_connector_ids() {
|
||||
assert_eq!(
|
||||
connector_auth_failure_from_tool_result(
|
||||
&auth_failure_result(),
|
||||
/*connector_id*/ None,
|
||||
Some("Google Calendar"),
|
||||
Some("https://chatgpt.com/apps/google-calendar/connector_calendar".to_string()),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
connector_auth_failure_from_tool_result(
|
||||
&auth_failure_result(),
|
||||
Some("connector_drive"),
|
||||
Some("Google Drive"),
|
||||
Some("https://chatgpt.com/apps/google-drive/connector_drive".to_string()),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_url_elicitation_payload() {
|
||||
let auth_failure = connector_auth_failure_from_tool_result(
|
||||
&auth_failure_result(),
|
||||
Some("connector_calendar"),
|
||||
Some("Google Calendar"),
|
||||
Some("https://chatgpt.com/apps/google-calendar/connector_calendar".to_string()),
|
||||
)
|
||||
.expect("auth failure");
|
||||
|
||||
assert_eq!(
|
||||
build_auth_elicitation("call_123", &auth_failure),
|
||||
CodexAppsAuthElicitation {
|
||||
meta: serde_json::json!({
|
||||
MCP_TOOL_CODEX_APPS_META_KEY: {
|
||||
CONNECTOR_AUTH_FAILURE_META_KEY: {
|
||||
CONNECTOR_AUTH_FAILURE_IS_AUTH_FAILURE_KEY: true,
|
||||
CONNECTOR_AUTH_FAILURE_CONNECTOR_ID_KEY: "connector_calendar",
|
||||
"connector_name": "Google Calendar",
|
||||
"install_url":
|
||||
"https://chatgpt.com/apps/google-calendar/connector_calendar",
|
||||
CONNECTOR_AUTH_FAILURE_AUTH_REASON_KEY: "reauthentication_required",
|
||||
CONNECTOR_AUTH_FAILURE_LINK_ID_KEY: "link_123",
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_CODE_KEY: "UNAUTHORIZED",
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_HTTP_STATUS_CODE_KEY: 401,
|
||||
CONNECTOR_AUTH_FAILURE_ERROR_ACTION_KEY: "TRIGGER_REAUTHENTICATION",
|
||||
},
|
||||
},
|
||||
}),
|
||||
message: "Reconnect Google Calendar on ChatGPT to restore access for this request."
|
||||
.to_string(),
|
||||
url: "https://chatgpt.com/apps/google-calendar/connector_calendar".to_string(),
|
||||
elicitation_id: "codex_apps_auth_call_123".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_auth_elicitation_plan() {
|
||||
let plan = build_auth_elicitation_plan(
|
||||
"call_123",
|
||||
&auth_failure_result(),
|
||||
Some("connector_calendar"),
|
||||
Some("Google Calendar"),
|
||||
Some("https://chatgpt.com/apps/google-calendar/connector_calendar".to_string()),
|
||||
)
|
||||
.expect("auth elicitation plan");
|
||||
|
||||
assert_eq!(plan.auth_failure.connector_name, "Google Calendar");
|
||||
assert_eq!(plan.elicitation.elicitation_id, "codex_apps_auth_call_123");
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ use url::Url;
|
||||
pub struct McpConnectionManager {
|
||||
clients: HashMap<String, AsyncManagedClient>,
|
||||
server_origins: HashMap<String, String>,
|
||||
host_owned_codex_apps_enabled: bool,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
startup_cancellation_token: CancellationToken,
|
||||
}
|
||||
@@ -82,6 +83,7 @@ impl McpConnectionManager {
|
||||
Self {
|
||||
clients: HashMap::new(),
|
||||
server_origins: HashMap::new(),
|
||||
host_owned_codex_apps_enabled: false,
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
permission_profile.get().clone(),
|
||||
@@ -116,6 +118,10 @@ impl McpConnectionManager {
|
||||
self.server_origins.get(server_name).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
|
||||
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
|
||||
*policy = approval_policy.value();
|
||||
@@ -148,6 +154,7 @@ impl McpConnectionManager {
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
codex_home: PathBuf,
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
host_owned_codex_apps_enabled: bool,
|
||||
tool_plugin_provenance: ToolPluginProvenance,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Self, CancellationToken) {
|
||||
@@ -248,6 +255,7 @@ impl McpConnectionManager {
|
||||
let manager = Self {
|
||||
clients,
|
||||
server_origins,
|
||||
host_owned_codex_apps_enabled,
|
||||
elicitation_requests: elicitation_requests.clone(),
|
||||
startup_cancellation_token: cancel_token.clone(),
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user