mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge f9ac5af535 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -26,6 +26,9 @@ jobs:
|
||||
- name: Verify Bazel clippy flags match Cargo workspace lints
|
||||
run: python3 .github/scripts/verify_bazel_clippy_lints.py
|
||||
|
||||
- name: Test Codex package builder
|
||||
run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
||||
with:
|
||||
|
||||
Generated
-1
@@ -1799,7 +1799,6 @@
|
||||
"winnow_0.7.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"annotate-snippets\",\"req\":\"^0.11.4\"},{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.15\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.8\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.100\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"circular\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"name\":\"is_terminal_polyfill\",\"optional\":true,\"req\":\"^1.48.1\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.6.0\"},{\"kind\":\"dev\",\"name\":\"rustc-hash\",\"req\":\"^2.1.1\"},{\"features\":[\"examples\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"term-transcript\",\"req\":\"^0.2.0\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.3\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\",\"dep:is_terminal_polyfill\",\"dep:terminal_size\"],\"default\":[\"std\"],\"simd\":[\"dep:memchr\"],\"std\":[\"alloc\",\"memchr?/std\"],\"unstable-doc\":[\"alloc\",\"std\",\"simd\",\"unstable-recover\"],\"unstable-recover\":[]}}",
|
||||
"winreg_0.10.1": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"~3.0\"},{\"features\":[\"impl-default\",\"impl-debug\",\"minwindef\",\"minwinbase\",\"timezoneapi\",\"winerror\",\"winnt\",\"winreg\",\"handleapi\"],\"name\":\"winapi\",\"req\":\"^0.3.9\"}],\"features\":{\"serialization-serde\":[\"transactions\",\"serde\"],\"transactions\":[\"winapi/ktmw32\"]}}",
|
||||
"winreg_0.50.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_bytes\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"~3.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Time\",\"Win32_System_Registry\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_Diagnostics_Debug\"],\"name\":\"windows-sys\",\"req\":\"^0.48.0\"}],\"features\":{\"serialization-serde\":[\"transactions\",\"serde\"],\"transactions\":[]}}",
|
||||
"winres_0.1.12": "{\"dependencies\":[{\"name\":\"toml\",\"req\":\"^0.5\"},{\"features\":[\"winnt\"],\"kind\":\"dev\",\"name\":\"winapi\",\"req\":\"^0.3\"}],\"features\":{}}",
|
||||
"winsafe_0.0.19": "{\"dependencies\":[],\"features\":{\"comctl\":[\"ole\"],\"dshow\":[\"oleaut\"],\"dwm\":[\"uxtheme\"],\"dxgi\":[\"ole\"],\"gdi\":[\"user\"],\"gui\":[\"comctl\",\"shell\",\"uxtheme\"],\"kernel\":[],\"mf\":[\"oleaut\"],\"ole\":[\"user\"],\"oleaut\":[\"ole\"],\"shell\":[\"oleaut\"],\"taskschd\":[\"oleaut\"],\"user\":[\"kernel\"],\"uxtheme\":[\"gdi\",\"ole\"],\"version\":[\"kernel\"]}}",
|
||||
"winsplit_0.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"wiremock_0.6.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2.10.0\"},{\"name\":\"assert-json-diff\",\"req\":\"^2.0.2\"},{\"features\":[\"attributes\",\"tokio1\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.2\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"deadpool\",\"req\":\"^0.12.2\"},{\"name\":\"futures\",\"req\":\"^0.3.31\"},{\"name\":\"http\",\"req\":\"^1.3\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"full\"],\"name\":\"hyper\",\"req\":\"^1.7\"},{\"features\":[\"tokio\",\"server\",\"http1\",\"http2\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"regex\",\"req\":\"^1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12.23\"},{\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"rt\",\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.47.1\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.47.1\"},{\"name\":\"url\",\"req\":\"^2.5\"}],\"features\":{}}",
|
||||
|
||||
Generated
+1
-10
@@ -2434,6 +2434,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"indexmap 2.13.0",
|
||||
"libc",
|
||||
"multimap",
|
||||
"pretty_assertions",
|
||||
@@ -4142,7 +4143,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"windows 0.58.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14727,15 +14727,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
|
||||
@@ -3628,6 +3628,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: json!({}),
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: Some(2),
|
||||
|
||||
@@ -991,6 +991,7 @@ fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
|
||||
HookEventName::SessionStart => "SessionStart",
|
||||
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
||||
HookEventName::SubagentStart => "SubagentStart",
|
||||
HookEventName::SubagentStop => "SubagentStop",
|
||||
HookEventName::Stop => "Stop",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3156,6 +3156,62 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalGetParams": {
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalSetParams": {
|
||||
"properties": {
|
||||
"objective": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
@@ -4316,6 +4372,78 @@
|
||||
"title": "Thread/name/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/set"
|
||||
],
|
||||
"title": "Thread/goal/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalSetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/get"
|
||||
],
|
||||
"title": "Thread/goal/getRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalGetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/getRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/clear"
|
||||
],
|
||||
"title": "Thread/goal/clearRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalClearParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -1836,6 +1836,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -3860,6 +3861,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+199
@@ -372,6 +372,78 @@
|
||||
"title": "Thread/name/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/set"
|
||||
],
|
||||
"title": "Thread/goal/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalSetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/get"
|
||||
],
|
||||
"title": "Thread/goal/getRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalGetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/getRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/clear"
|
||||
],
|
||||
"title": "Thread/goal/clearRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadGoalClearParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -9710,6 +9782,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -10606,6 +10679,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStop": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookMatcherGroup"
|
||||
@@ -10634,6 +10713,7 @@
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -11266,6 +11346,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/v2/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -15746,6 +15834,32 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cleared": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cleared"
|
||||
],
|
||||
"title": "ThreadGoalClearResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -15759,6 +15873,85 @@
|
||||
"title": "ThreadGoalClearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalGetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalGetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalGetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadGoal"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadGoalGetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalSetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"objective": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ThreadGoalStatus"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalSetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalSetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/v2/ThreadGoal"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal"
|
||||
],
|
||||
"title": "ThreadGoalSetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
@@ -16126,6 +16319,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+199
@@ -1098,6 +1098,78 @@
|
||||
"title": "Thread/name/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/set"
|
||||
],
|
||||
"title": "Thread/goal/setRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalSetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/setRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/get"
|
||||
],
|
||||
"title": "Thread/goal/getRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalGetParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/getRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/goal/clear"
|
||||
],
|
||||
"title": "Thread/goal/clearRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadGoalClearParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/goal/clearRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6190,6 +6262,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -7135,6 +7208,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStop": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -7163,6 +7242,7 @@
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -7795,6 +7875,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -13570,6 +13658,32 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cleared": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cleared"
|
||||
],
|
||||
"title": "ThreadGoalClearResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalClearedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -13583,6 +13697,85 @@
|
||||
"title": "ThreadGoalClearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalGetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalGetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalGetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadGoalGetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalSetParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"objective": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalSetParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalSetResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal"
|
||||
],
|
||||
"title": "ThreadGoalSetResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
@@ -13950,6 +14143,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+7
@@ -288,6 +288,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStop": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -316,6 +322,7 @@
|
||||
"SessionStart",
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -800,6 +800,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -800,6 +800,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -944,6 +944,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1421,6 +1421,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalClearParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cleared": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cleared"
|
||||
],
|
||||
"title": "ThreadGoalClearResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalGetParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"goal": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadGoalGetResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"objective": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadGoalSetParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/ThreadGoalStatus"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeUsedSeconds": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"tokenBudget": {
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tokensUsed": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"objective",
|
||||
"status",
|
||||
"threadId",
|
||||
"timeUsedSeconds",
|
||||
"tokensUsed",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoalStatus": {
|
||||
"enum": [
|
||||
"active",
|
||||
"paused",
|
||||
"blocked",
|
||||
"usageLimited",
|
||||
"budgetLimited",
|
||||
"complete"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"goal": {
|
||||
"$ref": "#/definitions/ThreadGoal"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"goal"
|
||||
],
|
||||
"title": "ThreadGoalSetResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+6
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1421,6 +1421,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1421,6 +1421,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1236,6 +1236,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -944,6 +944,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -944,6 +944,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -944,6 +944,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "preCompact" | "postCompact" | "sessionStart" | "userPromptSubmit" | "subagentStart" | "stop";
|
||||
export type HookEventName = "preToolUse" | "permissionRequest" | "postToolUse" | "preCompact" | "postCompact" | "sessionStart" | "userPromptSubmit" | "subagentStart" | "subagentStop" | "stop";
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup";
|
||||
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, SubagentStart: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, SubagentStart: Array<ConfiguredHookMatcherGroup>, SubagentStop: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
|
||||
+5
-1
@@ -12,4 +12,8 @@ export type Model = { id: string, model: string, upgrade: string | null, upgrade
|
||||
/**
|
||||
* Deprecated: use `serviceTiers` instead.
|
||||
*/
|
||||
additionalSpeedTiers: Array<string>, serviceTiers: Array<ModelServiceTier>, isDefault: boolean, };
|
||||
additionalSpeedTiers: Array<string>, serviceTiers: Array<ModelServiceTier>,
|
||||
/**
|
||||
* Catalog default service tier id for this model, when one is configured.
|
||||
*/
|
||||
defaultServiceTier: string | null, isDefault: boolean, };
|
||||
|
||||
@@ -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 ThreadGoalClearParams = { threadId: string, };
|
||||
@@ -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 ThreadGoalClearResponse = { cleared: boolean, };
|
||||
@@ -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 ThreadGoalGetParams = { threadId: string, };
|
||||
@@ -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 { ThreadGoal } from "./ThreadGoal";
|
||||
|
||||
export type ThreadGoalGetResponse = { goal: ThreadGoal | 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 { ThreadGoalStatus } from "./ThreadGoalStatus";
|
||||
|
||||
export type ThreadGoalSetParams = { threadId: string, objective?: string | null, status?: ThreadGoalStatus | null, tokenBudget?: number | 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 { ThreadGoal } from "./ThreadGoal";
|
||||
|
||||
export type ThreadGoalSetResponse = { goal: ThreadGoal, };
|
||||
@@ -53,7 +53,7 @@ exitCode: number | null,
|
||||
/**
|
||||
* The duration of the command execution in milliseconds.
|
||||
*/
|
||||
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, mcpAppResourceUri?: string, result: McpToolCallResult | null, error: McpToolCallError | null,
|
||||
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, mcpAppResourceUri?: string, pluginId: string | null, result: McpToolCallResult | null, error: McpToolCallError | null,
|
||||
/**
|
||||
* The duration of the MCP tool call in milliseconds.
|
||||
*/
|
||||
|
||||
@@ -365,7 +365,13 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
|
||||
export type { ThreadForkParams } from "./ThreadForkParams";
|
||||
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
||||
export type { ThreadGoal } from "./ThreadGoal";
|
||||
export type { ThreadGoalClearParams } from "./ThreadGoalClearParams";
|
||||
export type { ThreadGoalClearResponse } from "./ThreadGoalClearResponse";
|
||||
export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification";
|
||||
export type { ThreadGoalGetParams } from "./ThreadGoalGetParams";
|
||||
export type { ThreadGoalGetResponse } from "./ThreadGoalGetResponse";
|
||||
export type { ThreadGoalSetParams } from "./ThreadGoalSetParams";
|
||||
export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse";
|
||||
export type { ThreadGoalStatus } from "./ThreadGoalStatus";
|
||||
export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification";
|
||||
export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams";
|
||||
|
||||
@@ -494,19 +494,16 @@ client_request_definitions! {
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadSetNameResponse,
|
||||
},
|
||||
#[experimental("thread/goal/set")]
|
||||
ThreadGoalSet => "thread/goal/set" {
|
||||
params: v2::ThreadGoalSetParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadGoalSetResponse,
|
||||
},
|
||||
#[experimental("thread/goal/get")]
|
||||
ThreadGoalGet => "thread/goal/get" {
|
||||
params: v2::ThreadGoalGetParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadGoalGetResponse,
|
||||
},
|
||||
#[experimental("thread/goal/clear")]
|
||||
ThreadGoalClear => "thread/goal/clear" {
|
||||
params: v2::ThreadGoalClearParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
@@ -1473,9 +1470,7 @@ server_notification_definitions! {
|
||||
ThreadClosed => "thread/closed" (v2::ThreadClosedNotification),
|
||||
SkillsChanged => "skills/changed" (v2::SkillsChangedNotification),
|
||||
ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification),
|
||||
#[experimental("thread/goal/updated")]
|
||||
ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification),
|
||||
#[experimental("thread/goal/cleared")]
|
||||
ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification),
|
||||
#[experimental("thread/settings/updated")]
|
||||
ThreadSettingsUpdated => "thread/settings/updated" (v2::ThreadSettingsUpdatedNotification),
|
||||
@@ -3035,7 +3030,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_goal_methods_are_marked_experimental() {
|
||||
fn thread_goal_methods_are_not_marked_experimental() {
|
||||
let set_request = ClientRequest::ThreadGoalSet {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadGoalSetParams {
|
||||
@@ -3060,20 +3055,20 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&set_request),
|
||||
Some("thread/goal/set")
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&get_request),
|
||||
Some("thread/goal/get")
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&clear_request),
|
||||
Some("thread/goal/clear")
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_goal_notifications_are_marked_experimental() {
|
||||
fn thread_goal_notifications_are_not_marked_experimental() {
|
||||
let goal = v2::ThreadGoal {
|
||||
thread_id: "thr_123".to_string(),
|
||||
objective: "ship goal mode".to_string(),
|
||||
@@ -3095,11 +3090,11 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&updated),
|
||||
Some("thread/goal/updated")
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&cleared),
|
||||
Some("thread/goal/cleared")
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -525,6 +525,7 @@ impl ThreadHistoryBuilder {
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -566,6 +567,7 @@ impl ThreadHistoryBuilder {
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
@@ -1911,6 +1913,7 @@ mod tests {
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
duration: Duration::from_millis(8),
|
||||
result: Err("boom".into()),
|
||||
}),
|
||||
@@ -1960,6 +1963,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "boom".into(),
|
||||
@@ -1986,6 +1990,7 @@ mod tests {
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
plugin_id: Some("sample@test".into()),
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
@@ -2016,6 +2021,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
plugin_id: Some("sample@test".into()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
|
||||
@@ -434,6 +434,9 @@ pub struct ManagedHooksRequirements {
|
||||
#[serde(rename = "SubagentStart")]
|
||||
#[ts(rename = "SubagentStart")]
|
||||
pub subagent_start: Vec<ConfiguredHookMatcherGroup>,
|
||||
#[serde(rename = "SubagentStop")]
|
||||
#[ts(rename = "SubagentStop")]
|
||||
pub subagent_stop: Vec<ConfiguredHookMatcherGroup>,
|
||||
#[serde(rename = "Stop")]
|
||||
#[ts(rename = "Stop")]
|
||||
pub stop: Vec<ConfiguredHookMatcherGroup>,
|
||||
|
||||
@@ -17,7 +17,7 @@ use ts_rs::TS;
|
||||
|
||||
v2_enum_from_core!(
|
||||
pub enum HookEventName from CoreHookEventName {
|
||||
PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit, SubagentStart, Stop
|
||||
PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, UserPromptSubmit, SubagentStart, SubagentStop, Stop
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ pub enum ThreadItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
plugin_id: Option<String>,
|
||||
result: Option<Box<McpToolCallResult>>,
|
||||
error: Option<McpToolCallError>,
|
||||
/// The duration of the MCP tool call in milliseconds.
|
||||
@@ -846,6 +847,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
status: McpToolCallStatus::from(mcp.status),
|
||||
arguments: mcp.arguments,
|
||||
mcp_app_resource_uri: mcp.mcp_app_resource_uri,
|
||||
plugin_id: mcp.plugin_id,
|
||||
result: mcp.result.map(McpToolCallResult::from).map(Box::new),
|
||||
error: mcp.error.map(McpToolCallError::from),
|
||||
duration_ms,
|
||||
|
||||
@@ -98,6 +98,9 @@ pub struct Model {
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
/// Catalog default service tier id for this model, when one is configured.
|
||||
#[serde(default)]
|
||||
pub default_service_tier: Option<String>,
|
||||
// Only one model should be marked as default.
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -2491,6 +2491,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({"arg": "value"}),
|
||||
mcp_app_resource_uri: Some("app://connector".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
status: CoreMcpToolCallStatus::InProgress,
|
||||
result: None,
|
||||
error: None,
|
||||
@@ -2506,6 +2507,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: json!({"arg": "value"}),
|
||||
mcp_app_resource_uri: Some("app://connector".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -2518,6 +2520,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
tool: "tool".to_string(),
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
status: CoreMcpToolCallStatus::Completed,
|
||||
result: Some(CallToolResult {
|
||||
content: vec![json!({"type": "text", "text": "ok"})],
|
||||
@@ -2538,6 +2541,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: vec![json!({"type": "text", "text": "ok"})],
|
||||
structured_content: Some(json!({"ok": true})),
|
||||
|
||||
@@ -188,7 +188,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
@@ -1231,7 +1231,7 @@ Today both notifications carry an empty `items` array even when item events were
|
||||
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
|
||||
- `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`.
|
||||
- `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
|
||||
- `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `mcpToolCall` — `{id, server, tool, status, arguments, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
|
||||
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
||||
|
||||
@@ -696,10 +696,25 @@ async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
"Codex guidance"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(codex_home.join("config.toml")).expect("read config"),
|
||||
"sandbox_mode = \"workspace-write\"\n\n[shell_environment_policy]\ninherit = \"core\"\n\n[shell_environment_policy.set]\nCI = \"false\"\nFOO = \"bar\"\nMAX_RETRIES = \"3\"\nMY_TEAM = \"codex\"\n"
|
||||
);
|
||||
let config: TomlValue =
|
||||
toml::from_str(&fs::read_to_string(codex_home.join("config.toml")).expect("read config"))
|
||||
.expect("parse config");
|
||||
let expected: TomlValue = toml::from_str(
|
||||
r#"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[shell_environment_policy]
|
||||
inherit = "core"
|
||||
|
||||
[shell_environment_policy.set]
|
||||
CI = "false"
|
||||
FOO = "bar"
|
||||
MAX_RETRIES = "3"
|
||||
MY_TEAM = "codex"
|
||||
"#,
|
||||
)
|
||||
.expect("parse expected config");
|
||||
assert_eq!(config, expected);
|
||||
assert_eq!(
|
||||
fs::read_to_string(agents_skills.join("skill-a").join("SKILL.md"))
|
||||
.expect("read copied skill"),
|
||||
@@ -732,10 +747,24 @@ async fn import_home_config_uses_local_settings_over_project_settings() {
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert_eq!(
|
||||
fs::read_to_string(codex_home.join("config.toml")).expect("read config"),
|
||||
"sandbox_mode = \"workspace-write\"\n\n[shell_environment_policy]\ninherit = \"core\"\n\n[shell_environment_policy.set]\nFOO = \"local\"\nLOCAL_ONLY = \"true\"\nPROJECT_ONLY = \"yes\"\n"
|
||||
);
|
||||
let config: TomlValue =
|
||||
toml::from_str(&fs::read_to_string(codex_home.join("config.toml")).expect("read config"))
|
||||
.expect("parse config");
|
||||
let expected: TomlValue = toml::from_str(
|
||||
r#"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[shell_environment_policy]
|
||||
inherit = "core"
|
||||
|
||||
[shell_environment_policy.set]
|
||||
FOO = "local"
|
||||
LOCAL_ONLY = "true"
|
||||
PROJECT_ONLY = "yes"
|
||||
"#,
|
||||
)
|
||||
.expect("parse expected config");
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -53,6 +53,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
|
||||
description: service_tier.description,
|
||||
})
|
||||
.collect(),
|
||||
default_service_tier: preset.default_service_tier,
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +467,7 @@ fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> Managed
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
} = hooks;
|
||||
|
||||
@@ -481,6 +482,7 @@ fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> Managed
|
||||
session_start: map_hook_matcher_groups_to_api(session_start),
|
||||
user_prompt_submit: map_hook_matcher_groups_to_api(user_prompt_submit),
|
||||
subagent_start: map_hook_matcher_groups_to_api(subagent_start),
|
||||
subagent_stop: map_hook_matcher_groups_to_api(subagent_stop),
|
||||
stop: map_hook_matcher_groups_to_api(stop),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"secret":"argument"}),
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
@@ -120,6 +121,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()),
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
result: Some(Box::new(redacted_mcp_tool_call_result())),
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
@@ -136,6 +138,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: serde_json::json!({"secret":"argument"}),
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "secret error".to_string(),
|
||||
@@ -154,6 +157,7 @@ mod tests {
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: JsonValue::String(REDACTED_PAYLOAD.to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: REDACTED_PAYLOAD.to_string(),
|
||||
|
||||
@@ -2,9 +2,8 @@ use super::*;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadGoal;
|
||||
use codex_app_server_protocol::ThreadGoalStatus;
|
||||
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
|
||||
use codex_protocol::protocol::RealtimeConversationVersion;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -15,20 +14,11 @@ fn absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
|
||||
}
|
||||
|
||||
fn thread_goal_updated_notification() -> ServerNotification {
|
||||
ServerNotification::ThreadGoalUpdated(ThreadGoalUpdatedNotification {
|
||||
fn thread_realtime_started_notification() -> ServerNotification {
|
||||
ServerNotification::ThreadRealtimeStarted(ThreadRealtimeStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: None,
|
||||
goal: ThreadGoal {
|
||||
thread_id: "thread-1".to_string(),
|
||||
objective: "ship goal mode".to_string(),
|
||||
status: ThreadGoalStatus::Active,
|
||||
token_budget: None,
|
||||
tokens_used: 0,
|
||||
time_used_seconds: 0,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
},
|
||||
realtime_session_id: None,
|
||||
version: RealtimeConversationVersion::V1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,7 +172,7 @@ async fn experimental_notifications_are_dropped_without_capability() {
|
||||
&mut connections,
|
||||
OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message: OutgoingMessage::AppServerNotification(thread_goal_updated_notification()),
|
||||
message: OutgoingMessage::AppServerNotification(thread_realtime_started_notification()),
|
||||
write_complete_tx: None,
|
||||
},
|
||||
)
|
||||
@@ -215,7 +205,7 @@ async fn experimental_notifications_are_preserved_with_capability() {
|
||||
&mut connections,
|
||||
OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message: OutgoingMessage::AppServerNotification(thread_goal_updated_notification()),
|
||||
message: OutgoingMessage::AppServerNotification(thread_realtime_started_notification()),
|
||||
write_complete_tx: None,
|
||||
},
|
||||
)
|
||||
@@ -227,7 +217,7 @@ async fn experimental_notifications_are_preserved_with_capability() {
|
||||
.expect("experimental notification should reach opted-in client");
|
||||
assert!(matches!(
|
||||
message.message,
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ThreadGoalUpdated(_))
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ThreadRealtimeStarted(_))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
priority,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
service_tiers: preset.service_tiers.clone(),
|
||||
default_service_tier: preset.default_service_tier.clone(),
|
||||
upgrade: preset.upgrade.as_ref().map(Into::into),
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -513,6 +513,7 @@ url = "{mcp_server_url}/mcp"
|
||||
status,
|
||||
arguments: json!({ "message": LARGE_RESPONSE_MESSAGE }),
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
|
||||
@@ -69,6 +69,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
description: service_tier.description.clone(),
|
||||
})
|
||||
.collect(),
|
||||
default_service_tier: preset.default_service_tier.clone(),
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,6 +679,7 @@ fn append_resume_redaction_history(
|
||||
arguments: Some(json!({"secret":"argument"})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".to_string()),
|
||||
plugin_id: None,
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
content: vec![json!({
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::test_support::all_model_presets;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
@@ -136,7 +137,7 @@ async fn thread_settings_update_while_turn_is_active_emits_notification() -> Res
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_settings_update_clears_service_tier() -> Result<()> {
|
||||
async fn thread_settings_update_null_service_tier_uses_default() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(vec![
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
])
|
||||
@@ -181,7 +182,10 @@ async fn thread_settings_update_clears_service_tier() -> Result<()> {
|
||||
let clear_updated = read_thread_settings_updated(&mut mcp).await?;
|
||||
assert_eq!(clear_updated.thread_id, thread.id);
|
||||
assert_eq!(clear_updated.thread_settings.model, model_id);
|
||||
assert_eq!(clear_updated.thread_settings.service_tier, None);
|
||||
assert_eq!(
|
||||
clear_updated.thread_settings.service_tier.as_deref(),
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE)
|
||||
);
|
||||
|
||||
start_text_turn(&mut mcp, thread.id).await?;
|
||||
timeout(
|
||||
|
||||
@@ -28,6 +28,7 @@ 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::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -484,7 +485,7 @@ model_reasoning_effort = "high"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> {
|
||||
async fn thread_start_drops_unsupported_service_tier_id() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -508,7 +509,39 @@ async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> {
|
||||
.await??;
|
||||
let ThreadStartResponse { service_tier, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(service_tier, Some(service_tier_id));
|
||||
// Unsupported catalog ids are dropped at session config time instead of echoed back.
|
||||
assert_eq!(service_tier, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_accepts_default_service_tier() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
service_tier: Some(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { service_tier, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(
|
||||
service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1004,10 +1004,14 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
|
||||
assert!(
|
||||
err.error
|
||||
.message
|
||||
.contains("invalid thread settings override")
|
||||
.contains("`approval_policy = \"never\"` cannot be used"),
|
||||
"unexpected error message: {}",
|
||||
err.error.message
|
||||
);
|
||||
assert!(
|
||||
err.error.message.contains("allowed set [ReadOnly]"),
|
||||
err.error
|
||||
.message
|
||||
.contains("requirements do not allow `sandbox_mode = \"danger-full-access\"`"),
|
||||
"unexpected error message: {}",
|
||||
err.error.message
|
||||
);
|
||||
|
||||
@@ -6,14 +6,15 @@ use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
|
||||
use codex_core_plugins::marketplace::marketplace_root_dir;
|
||||
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
|
||||
use codex_core_plugins::marketplace_add::add_marketplace;
|
||||
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
|
||||
use codex_core_plugins::marketplace_remove::remove_marketplace;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::plugin_cmd::configured_marketplace_snapshot_issues;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin marketplace")]
|
||||
@@ -30,7 +31,7 @@ enum MarketplaceSubcommand {
|
||||
/// Add a local or Git marketplace to the configured marketplace sources.
|
||||
Add(AddMarketplaceArgs),
|
||||
|
||||
/// List configured marketplace names and their local snapshot roots.
|
||||
/// List plugin marketplaces Codex is currently considering and their roots.
|
||||
List,
|
||||
|
||||
/// Refresh configured Git marketplace snapshots.
|
||||
@@ -150,39 +151,77 @@ async fn run_list(overrides: Vec<(String, toml::Value)>) -> Result<()> {
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let configured_marketplaces = config
|
||||
.config_layer_stack
|
||||
.get_active_user_layer()
|
||||
.and_then(|layer| layer.config.get("marketplaces"))
|
||||
.and_then(toml::Value::as_table);
|
||||
let Some(configured_marketplaces) = configured_marketplaces else {
|
||||
println!("No configured plugin marketplaces.");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if configured_marketplaces.is_empty() {
|
||||
println!("No configured plugin marketplaces.");
|
||||
let manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let marketplace_listing = manager
|
||||
.discover_marketplaces_for_config(&plugins_input, &[])
|
||||
.context("failed to list plugin marketplaces")?;
|
||||
let mut load_issues = configured_marketplace_snapshot_issues(
|
||||
config.codex_home.as_path(),
|
||||
&plugins_input,
|
||||
&marketplace_listing.errors,
|
||||
/*marketplace_name*/ None,
|
||||
);
|
||||
let mut issue_paths = load_issues
|
||||
.iter()
|
||||
.map(|issue| issue.path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
for error in &marketplace_listing.errors {
|
||||
if issue_paths.insert(error.path.to_path_buf()) {
|
||||
load_issues.push(crate::plugin_cmd::ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: error.path.display().to_string(),
|
||||
path: error.path.to_path_buf(),
|
||||
message: error.message.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if !load_issues.is_empty() {
|
||||
let issue_lines = load_issues
|
||||
.iter()
|
||||
.map(|issue| {
|
||||
format!(
|
||||
"- `{}` at {}: {}",
|
||||
issue.marketplace_name,
|
||||
issue.path.display(),
|
||||
issue.message
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
bail!("failed to load marketplace(s):\n{issue_lines}");
|
||||
}
|
||||
let marketplaces = marketplace_listing.marketplaces;
|
||||
if marketplaces.is_empty() {
|
||||
println!("No plugin marketplaces in scope.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let default_install_root = marketplace_install_root(config.codex_home.as_path());
|
||||
for (marketplace_name, marketplace) in configured_marketplaces {
|
||||
if !marketplace.is_table() {
|
||||
eprintln!("Ignoring invalid marketplace `{marketplace_name}`: expected table.");
|
||||
let mut seen_roots = HashSet::new();
|
||||
let mut rows = Vec::new();
|
||||
for marketplace in marketplaces {
|
||||
let Ok(root) = marketplace_root_dir(&marketplace.path) else {
|
||||
continue;
|
||||
};
|
||||
if !seen_roots.insert(root.clone()) {
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = validate_plugin_segment(marketplace_name, "marketplace name") {
|
||||
eprintln!("Ignoring invalid marketplace `{marketplace_name}`: {err}.");
|
||||
continue;
|
||||
}
|
||||
let root = resolve_configured_marketplace_root(
|
||||
rows.push((marketplace.name, root));
|
||||
}
|
||||
|
||||
let marketplace_width = rows
|
||||
.iter()
|
||||
.map(|(name, _)| name.len())
|
||||
.max()
|
||||
.unwrap_or("MARKETPLACE".len())
|
||||
.max("MARKETPLACE".len());
|
||||
|
||||
println!("{:<marketplace_width$} ROOT", "MARKETPLACE");
|
||||
for (marketplace_name, root) in rows {
|
||||
println!(
|
||||
"{:<marketplace_width$} {}",
|
||||
marketplace_name,
|
||||
marketplace,
|
||||
default_install_root.as_path(),
|
||||
)
|
||||
.map(|root| root.display().to_string())
|
||||
.unwrap_or_else(|| "<invalid source>".to_string());
|
||||
println!("{marketplace_name}\t{root}");
|
||||
root.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+138
-19
@@ -5,6 +5,7 @@ use clap::Parser;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core_plugins::ConfiguredMarketplace;
|
||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginInstallRequest;
|
||||
use codex_core_plugins::PluginsConfigInput;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
@@ -15,10 +16,14 @@ use codex_core_plugins::marketplace::find_marketplace_manifest_path;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::marketplace_cmd::MarketplaceCli;
|
||||
|
||||
const OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME: &str = "openai-bundled-alpha";
|
||||
const OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME: &str = "openai-primary-runtime";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(bin_name = "codex plugin")]
|
||||
pub struct PluginCli {
|
||||
@@ -168,9 +173,13 @@ pub async fn run_plugin_list(
|
||||
println!("No marketplace plugins found.");
|
||||
}
|
||||
} else {
|
||||
for marketplace in marketplaces {
|
||||
println!("Marketplace `{}`", marketplace.name);
|
||||
println!("Path: {}", marketplace.path.as_path().display());
|
||||
for (index, marketplace) in marketplaces.into_iter().enumerate() {
|
||||
let mut rows = Vec::new();
|
||||
let mut plugin_width = "PLUGIN".len();
|
||||
let mut status_width = "STATUS".len();
|
||||
let mut installed_version_width = "VERSION".len();
|
||||
let mut path_width = "PATH".len();
|
||||
|
||||
for plugin in &marketplace.plugins {
|
||||
let state = if plugin.installed && plugin.enabled {
|
||||
"installed, enabled"
|
||||
@@ -179,7 +188,51 @@ pub async fn run_plugin_list(
|
||||
} else {
|
||||
"not installed"
|
||||
};
|
||||
println!(" {} ({state})", plugin.id);
|
||||
let installed_version = plugin.installed_version.clone().unwrap_or_default();
|
||||
let path = match &plugin.source {
|
||||
codex_core_plugins::marketplace::MarketplacePluginSource::Local { path } => {
|
||||
path.as_path().display().to_string()
|
||||
}
|
||||
codex_core_plugins::marketplace::MarketplacePluginSource::Git {
|
||||
url,
|
||||
path,
|
||||
ref_name,
|
||||
sha,
|
||||
} => {
|
||||
let mut parts = vec![url.clone()];
|
||||
if let Some(path) = path {
|
||||
parts.push(format!("path `{path}`"));
|
||||
}
|
||||
if let Some(ref_name) = ref_name {
|
||||
parts.push(format!("ref `{ref_name}`"));
|
||||
}
|
||||
if let Some(sha) = sha {
|
||||
parts.push(format!("sha `{sha}`"));
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
};
|
||||
plugin_width = plugin_width.max(plugin.id.len());
|
||||
status_width = status_width.max(state.len());
|
||||
installed_version_width = installed_version_width.max(installed_version.len());
|
||||
path_width = path_width.max(path.len());
|
||||
rows.push((plugin.id.clone(), state, installed_version, path));
|
||||
}
|
||||
|
||||
if index > 0 {
|
||||
println!();
|
||||
}
|
||||
println!("Marketplace `{}`", marketplace.name);
|
||||
println!("{}", marketplace.path.as_path().display());
|
||||
println!();
|
||||
println!(
|
||||
"{:<plugin_width$} {:<status_width$} {:<installed_version_width$} {:<path_width$}",
|
||||
"PLUGIN", "STATUS", "VERSION", "PATH"
|
||||
);
|
||||
for (plugin, status, installed_version, path) in rows {
|
||||
println!(
|
||||
"{plugin:<plugin_width$} {status:<status_width$} {installed_version:<installed_version_width$} {path:<path_width$}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,10 +359,10 @@ fn find_marketplace_for_plugin(
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: String,
|
||||
path: PathBuf,
|
||||
message: String,
|
||||
pub(crate) struct ConfiguredMarketplaceSnapshotIssue {
|
||||
pub(crate) marketplace_name: String,
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
fn ensure_configured_marketplace_snapshots_loaded(
|
||||
@@ -343,17 +396,16 @@ fn ensure_configured_marketplace_snapshots_loaded(
|
||||
bail!("failed to load configured marketplace snapshot(s):\n{issue_lines}");
|
||||
}
|
||||
|
||||
fn configured_marketplace_snapshot_issues(
|
||||
pub(crate) fn configured_marketplace_snapshot_issues(
|
||||
codex_home: &std::path::Path,
|
||||
plugins_input: &PluginsConfigInput,
|
||||
load_errors: &[MarketplaceListError],
|
||||
marketplace_name: Option<&str>,
|
||||
) -> Vec<ConfiguredMarketplaceSnapshotIssue> {
|
||||
let Some(user_layer) = plugins_input.config_layer_stack.get_active_user_layer() else {
|
||||
let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(configured_marketplaces) = user_layer
|
||||
.config
|
||||
let Some(configured_marketplaces) = user_config
|
||||
.get("marketplaces")
|
||||
.and_then(toml::Value::as_table)
|
||||
else {
|
||||
@@ -367,9 +419,33 @@ fn configured_marketplace_snapshot_issues(
|
||||
if marketplace_name.is_some_and(|name| configured_name != name) {
|
||||
continue;
|
||||
}
|
||||
if !marketplace.is_table()
|
||||
|| validate_plugin_segment(configured_name, "marketplace name").is_err()
|
||||
if !marketplace.is_table() {
|
||||
issues.push(ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: configured_name.clone(),
|
||||
path: PathBuf::from("<invalid config>"),
|
||||
message: "configured marketplace entry must be a table".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = validate_plugin_segment(configured_name, "marketplace name") {
|
||||
issues.push(ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: configured_name.clone(),
|
||||
path: PathBuf::from("<invalid config>"),
|
||||
message: err.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if marketplace.get("source_type").and_then(toml::Value::as_str) == Some("local")
|
||||
&& marketplace
|
||||
.get("source")
|
||||
.and_then(toml::Value::as_str)
|
||||
.is_none_or(str::is_empty)
|
||||
{
|
||||
issues.push(ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: configured_name.clone(),
|
||||
path: PathBuf::from("<invalid source>"),
|
||||
message: "configured local marketplace source is missing or empty".to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let Some(root) = resolve_configured_marketplace_root(
|
||||
@@ -381,11 +457,16 @@ fn configured_marketplace_snapshot_issues(
|
||||
};
|
||||
match find_marketplace_manifest_path(&root) {
|
||||
Some(path) => manifest_paths.push((configured_name.clone(), path)),
|
||||
None => issues.push(ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: configured_name.clone(),
|
||||
path: root,
|
||||
message: "marketplace root does not contain a supported manifest".to_string(),
|
||||
}),
|
||||
None => {
|
||||
if is_implicit_system_marketplace_root(configured_name, codex_home, &root) {
|
||||
continue;
|
||||
}
|
||||
issues.push(ConfiguredMarketplaceSnapshotIssue {
|
||||
marketplace_name: configured_name.clone(),
|
||||
path: root,
|
||||
message: "marketplace root does not contain a supported manifest".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,3 +484,41 @@ fn configured_marketplace_snapshot_issues(
|
||||
}
|
||||
issues
|
||||
}
|
||||
|
||||
fn is_implicit_system_marketplace_root(
|
||||
marketplace_name: &str,
|
||||
_codex_home: &Path,
|
||||
root: &Path,
|
||||
) -> bool {
|
||||
if matches!(
|
||||
marketplace_name,
|
||||
OPENAI_BUNDLED_MARKETPLACE_NAME | OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME
|
||||
) && path_ends_with(root, &[".tmp", "bundled-marketplaces", marketplace_name])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
marketplace_name == OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME
|
||||
&& path_ends_with(
|
||||
root,
|
||||
&[
|
||||
"codex-runtimes",
|
||||
"codex-primary-runtime",
|
||||
"plugins",
|
||||
marketplace_name,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn path_ends_with(path: &Path, suffix: &[&str]) -> bool {
|
||||
let path_components = path
|
||||
.components()
|
||||
.map(|component| component.as_os_str().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>();
|
||||
path_components.as_slice().ends_with(
|
||||
&suffix
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,21 @@ use predicates::str::contains;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const MARKETPLACE_HEADER: &str = "MARKETPLACE";
|
||||
const MARKETPLACE_LIST_HEADER: &str = "MARKETPLACE ROOT";
|
||||
|
||||
fn marketplace_list_row(marketplace_name: &str, root: &Path) -> String {
|
||||
format!(
|
||||
"{marketplace_name:<width$} {}",
|
||||
root.display(),
|
||||
width = MARKETPLACE_HEADER.len()
|
||||
)
|
||||
}
|
||||
|
||||
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
|
||||
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||
cmd.env("CODEX_HOME", codex_home);
|
||||
cmd.env("HOME", codex_home);
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
@@ -40,11 +52,30 @@ plugins = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_marketplace_source(source: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(source.join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(source.join("plugins/sample/.codex-plugin"))?;
|
||||
fn write_marketplace_source_with_manifest(source: &Path, marketplace_manifest: &str) -> Result<()> {
|
||||
std::fs::create_dir_all(source.join(".agents").join("plugins"))?;
|
||||
std::fs::create_dir_all(source.join("plugins").join("sample").join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
source.join(".agents/plugins/marketplace.json"),
|
||||
source
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json"),
|
||||
marketplace_manifest,
|
||||
)?;
|
||||
std::fs::write(
|
||||
source
|
||||
.join("plugins")
|
||||
.join("sample")
|
||||
.join(".codex-plugin")
|
||||
.join("plugin.json"),
|
||||
r#"{"name":"sample","version":"1.2.3","description":"Sample plugin"}"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_marketplace_source(source: &Path) -> Result<()> {
|
||||
write_marketplace_source_with_manifest(
|
||||
source,
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
@@ -57,12 +88,28 @@ fn write_marketplace_source(source: &Path) -> Result<()> {
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
source.join("plugins/sample/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample","description":"Sample plugin"}"#,
|
||||
)?;
|
||||
Ok(())
|
||||
)
|
||||
}
|
||||
|
||||
fn write_marketplace_source_with_explicit_empty_products(source: &Path) -> Result<()> {
|
||||
write_marketplace_source_with_manifest(
|
||||
source,
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sample",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/sample"
|
||||
},
|
||||
"policy": {
|
||||
"products": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_local_marketplace() -> Result<(TempDir, TempDir)> {
|
||||
@@ -87,6 +134,20 @@ fn setup_unconfigured_local_marketplace() -> Result<(TempDir, TempDir)> {
|
||||
Ok((codex_home, source))
|
||||
}
|
||||
|
||||
fn setup_local_marketplace_with_explicit_empty_products() -> Result<(TempDir, TempDir)> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
write_marketplace_source_with_explicit_empty_products(source.path())?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
&configured_local_marketplace(&source_path),
|
||||
)?;
|
||||
Ok((codex_home, source))
|
||||
}
|
||||
|
||||
fn setup_configured_marketplace_without_manifest() -> Result<(TempDir, TempDir)> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
@@ -104,9 +165,13 @@ fn setup_configured_marketplace_with_malformed_manifest() -> Result<(TempDir, Te
|
||||
let codex_home = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::create_dir_all(source.path().join(".agents/plugins"))?;
|
||||
std::fs::create_dir_all(source.path().join(".agents").join("plugins"))?;
|
||||
std::fs::write(
|
||||
source.path().join(".agents/plugins/marketplace.json"),
|
||||
source
|
||||
.path()
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json"),
|
||||
"{not valid json",
|
||||
)?;
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
@@ -118,30 +183,328 @@ fn setup_configured_marketplace_with_malformed_manifest() -> Result<(TempDir, Te
|
||||
Ok((codex_home, source))
|
||||
}
|
||||
|
||||
fn setup_local_marketplace_with_implicit_system_roots() -> Result<(TempDir, TempDir, TempDir)> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
|
||||
let bundled_root = codex_home
|
||||
.path()
|
||||
.join(".tmp")
|
||||
.join("bundled-marketplaces")
|
||||
.join("openai-bundled");
|
||||
std::fs::create_dir_all(&bundled_root)?;
|
||||
let bundled_source = bundled_root.display().to_string();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"openai-bundled",
|
||||
&configured_local_marketplace(&bundled_source),
|
||||
)?;
|
||||
|
||||
let cache_home = TempDir::new()?;
|
||||
let runtime_root = cache_home
|
||||
.path()
|
||||
.join("codex-runtimes")
|
||||
.join("codex-primary-runtime")
|
||||
.join("plugins")
|
||||
.join("openai-primary-runtime");
|
||||
std::fs::create_dir_all(&runtime_root)?;
|
||||
let runtime_source = runtime_root.display().to_string();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"openai-primary-runtime",
|
||||
&configured_local_marketplace(&runtime_source),
|
||||
)?;
|
||||
|
||||
Ok((codex_home, source, cache_home))
|
||||
}
|
||||
|
||||
fn setup_custom_marketplace_under_implicit_system_root() -> Result<(TempDir, std::path::PathBuf)> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
|
||||
let custom_root = codex_home
|
||||
.path()
|
||||
.join(".tmp")
|
||||
.join("bundled-marketplaces")
|
||||
.join("custom-marketplace");
|
||||
std::fs::create_dir_all(&custom_root)?;
|
||||
let custom_source = custom_root.display().to_string();
|
||||
record_user_marketplace(
|
||||
codex_home.path(),
|
||||
"custom-marketplace",
|
||||
&configured_local_marketplace(&custom_source),
|
||||
)?;
|
||||
|
||||
Ok((codex_home, custom_root))
|
||||
}
|
||||
|
||||
fn remove_installed_plugin_config(codex_home: &Path, plugin_key: &str) -> Result<()> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let plugin_header = format!("[plugins.\"{plugin_key}\"]");
|
||||
let config = std::fs::read_to_string(&config_path)?;
|
||||
let mut rewritten = Vec::new();
|
||||
let mut skipping = false;
|
||||
|
||||
for line in config.lines() {
|
||||
if line == plugin_header {
|
||||
skipping = true;
|
||||
continue;
|
||||
}
|
||||
if skipping && line.starts_with('[') {
|
||||
skipping = false;
|
||||
}
|
||||
if !skipping {
|
||||
rewritten.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::write(config_path, format!("{}\n", rewritten.join("\n")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_configured_local_marketplace_with_missing_source() -> Result<TempDir> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[marketplaces.debug]
|
||||
source_type = "local"
|
||||
"#,
|
||||
)?;
|
||||
Ok(codex_home)
|
||||
}
|
||||
|
||||
fn setup_configured_local_marketplace_with_invalid_name() -> Result<TempDir> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[marketplaces."bad/name"]
|
||||
source_type = "local"
|
||||
source = "/tmp/debug"
|
||||
"#,
|
||||
)?;
|
||||
Ok(codex_home)
|
||||
}
|
||||
|
||||
fn assert_configured_marketplace_snapshot_failure(
|
||||
assert: assert_cmd::assert::Assert,
|
||||
source: &Path,
|
||||
detail: &str,
|
||||
) {
|
||||
assert
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"failed to load configured marketplace snapshot(s):",
|
||||
))
|
||||
.stderr(contains("`debug`"))
|
||||
.stderr(contains(source.display().to_string()))
|
||||
.stderr(contains(detail));
|
||||
}
|
||||
|
||||
fn assert_marketplace_failure(
|
||||
assert: assert_cmd::assert::Assert,
|
||||
marketplace_name: &str,
|
||||
source: &Path,
|
||||
detail: &str,
|
||||
) {
|
||||
assert
|
||||
.failure()
|
||||
.stderr(contains("failed to load marketplace(s):"))
|
||||
.stderr(contains(format!("`{marketplace_name}`")))
|
||||
.stderr(contains(source.display().to_string()))
|
||||
.stderr(contains(detail));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let expected_row = marketplace_list_row("debug", source.path());
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("debug"))
|
||||
.stdout(contains(source.path().display().to_string()));
|
||||
.stdout(contains(MARKETPLACE_LIST_HEADER))
|
||||
.stdout(contains(&expected_row))
|
||||
.stdout(contains("\t").not());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_shows_plugins_grouped_by_marketplace() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
async fn marketplace_list_includes_home_marketplace_when_present() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let home = TempDir::new()?;
|
||||
write_marketplace_source(home.path())?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
let expected_row = marketplace_list_row("debug", home.path());
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.env("HOME", home.path())
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains(MARKETPLACE_LIST_HEADER))
|
||||
.stdout(contains(&expected_row))
|
||||
.stdout(contains("\t").not());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_includes_root_when_plugins_are_filtered_out() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace_with_explicit_empty_products()?;
|
||||
let expected_row = marketplace_list_row("debug", source.path());
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains(MARKETPLACE_LIST_HEADER))
|
||||
.stdout(contains(&expected_row));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_fails_when_configured_marketplace_snapshot_is_missing() -> Result<()> {
|
||||
let (codex_home, source) = setup_configured_marketplace_without_manifest()?;
|
||||
|
||||
assert_marketplace_failure(
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert(),
|
||||
"debug",
|
||||
source.path(),
|
||||
"marketplace root does not contain a supported manifest",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_fails_when_configured_marketplace_name_is_invalid() -> Result<()> {
|
||||
let codex_home = setup_configured_local_marketplace_with_invalid_name()?;
|
||||
|
||||
assert_marketplace_failure(
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert(),
|
||||
"bad/name",
|
||||
Path::new("<invalid config>"),
|
||||
"marketplace name",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_fails_when_configured_local_marketplace_source_is_missing() -> Result<()>
|
||||
{
|
||||
let codex_home = setup_configured_local_marketplace_with_missing_source()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("failed to load marketplace(s):"))
|
||||
.stderr(contains("`debug`"))
|
||||
.stderr(contains("<invalid source>"))
|
||||
.stderr(contains(
|
||||
"configured local marketplace source is missing or empty",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_fails_when_home_marketplace_is_malformed() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let home = TempDir::new()?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
std::fs::create_dir_all(home.path().join(".agents/plugins"))?;
|
||||
let home_marketplace_path = home
|
||||
.path()
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json");
|
||||
std::fs::write(&home_marketplace_path, "{not valid json")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.env("HOME", home.path())
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("failed to load marketplace(s):"))
|
||||
.stderr(contains(home_marketplace_path.display().to_string()))
|
||||
.stderr(contains("key must be a string"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_list_fails_when_configured_marketplace_snapshot_is_malformed() -> Result<()> {
|
||||
let (codex_home, source) = setup_configured_marketplace_with_malformed_manifest()?;
|
||||
|
||||
assert_marketplace_failure(
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "marketplace", "list"])
|
||||
.assert(),
|
||||
"debug",
|
||||
source.path(),
|
||||
"key must be a string",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_prints_plugins_in_a_table() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let marketplace_manifest = source
|
||||
.path()
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json");
|
||||
let plugin_path = source.path().join("plugins").join("sample");
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Marketplace `debug`"))
|
||||
.stdout(contains("sample@debug (not installed)"));
|
||||
.stdout(contains("PLUGIN"))
|
||||
.stdout(contains("STATUS"))
|
||||
.stdout(contains("VERSION"))
|
||||
.stdout(contains("PATH"))
|
||||
.stdout(contains(marketplace_manifest.display().to_string()))
|
||||
.stdout(contains("sample@debug"))
|
||||
.stdout(contains("not installed"))
|
||||
.stdout(contains(plugin_path.display().to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_shows_installed_version_when_plugin_is_installed() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("sample@debug"))
|
||||
.stdout(contains("1.2.3"))
|
||||
.stdout(contains("installed, enabled"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -151,10 +514,10 @@ async fn plugin_list_excludes_unconfigured_repo_local_marketplaces() -> Result<(
|
||||
let (codex_home, source) = setup_unconfigured_local_marketplace()?;
|
||||
|
||||
codex_command_in(codex_home.path(), source.path())?
|
||||
.args(["plugin", "list"])
|
||||
.args(["plugin", "list", "--marketplace", "debug"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("No marketplace plugins found."))
|
||||
.stdout(contains("No plugins found in marketplace `debug`."))
|
||||
.stdout(predicates::str::is_match("sample@debug").unwrap().not());
|
||||
|
||||
Ok(())
|
||||
@@ -164,6 +527,47 @@ async fn plugin_list_excludes_unconfigured_repo_local_marketplaces() -> Result<(
|
||||
async fn plugin_list_fails_when_configured_marketplace_snapshot_is_missing() -> Result<()> {
|
||||
let (codex_home, source) = setup_configured_marketplace_without_manifest()?;
|
||||
|
||||
assert_configured_marketplace_snapshot_failure(
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert(),
|
||||
source.path(),
|
||||
"marketplace root does not contain a supported manifest",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_ignores_implicit_system_marketplace_roots_without_manifests() -> Result<()> {
|
||||
let (codex_home, source, cache_home) = setup_local_marketplace_with_implicit_system_roots()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.env("XDG_CACHE_HOME", cache_home.path())
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("Marketplace `debug`"))
|
||||
.stdout(contains(
|
||||
source
|
||||
.path()
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json")
|
||||
.display()
|
||||
.to_string(),
|
||||
))
|
||||
.stderr(
|
||||
predicates::str::contains("failed to load configured marketplace snapshot(s):").not(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_fails_for_custom_marketplace_under_system_root() -> Result<()> {
|
||||
let (codex_home, custom_root) = setup_custom_marketplace_under_implicit_system_root()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
@@ -171,8 +575,8 @@ async fn plugin_list_fails_when_configured_marketplace_snapshot_is_missing() ->
|
||||
.stderr(contains(
|
||||
"failed to load configured marketplace snapshot(s):",
|
||||
))
|
||||
.stderr(contains("`debug`"))
|
||||
.stderr(contains(source.path().display().to_string()))
|
||||
.stderr(contains("`custom-marketplace`"))
|
||||
.stderr(contains(custom_root.display().to_string()))
|
||||
.stderr(contains(
|
||||
"marketplace root does not contain a supported manifest",
|
||||
));
|
||||
@@ -180,6 +584,28 @@ async fn plugin_list_fails_when_configured_marketplace_snapshot_is_missing() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_hides_version_for_cached_but_unconfigured_plugin() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
remove_installed_plugin_config(codex_home.path(), "sample@debug")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("sample@debug"))
|
||||
.stdout(contains("not installed"))
|
||||
.stdout(predicates::str::contains("1.2.3").not());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_add_and_remove_updates_installed_plugin_config() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
@@ -224,18 +650,15 @@ async fn plugin_add_rejects_unconfigured_repo_local_marketplaces() -> Result<()>
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_add_fails_when_configured_marketplace_snapshot_is_malformed() -> Result<()> {
|
||||
let (codex_home, _source) = setup_configured_marketplace_with_malformed_manifest()?;
|
||||
let (codex_home, source) = setup_configured_marketplace_with_malformed_manifest()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"failed to load configured marketplace snapshot(s):",
|
||||
))
|
||||
.stderr(contains("`debug`"))
|
||||
.stderr(contains("invalid marketplace file"))
|
||||
.stderr(contains("key must be a string"));
|
||||
assert_configured_marketplace_snapshot_failure(
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert(),
|
||||
source.path(),
|
||||
"key must be a string",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -258,7 +681,7 @@ async fn plugin_add_reinstalls_from_configured_marketplace_snapshot() -> Result<
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
.join("plugins/cache/debug/sample/local/.codex-plugin/plugin.json")
|
||||
.join("plugins/cache/debug/sample/1.2.3/.codex-plugin/plugin.json")
|
||||
.is_file()
|
||||
);
|
||||
|
||||
@@ -311,7 +734,7 @@ async fn plugin_add_rejects_cached_plugins_without_authorizing_marketplace_snaps
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
.join("plugins/cache/debug/sample/local/.codex-plugin/plugin.json")
|
||||
.join("plugins/cache/debug/sample/1.2.3/.codex-plugin/plugin.json")
|
||||
.is_file()
|
||||
);
|
||||
|
||||
|
||||
@@ -993,6 +993,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v1_input_transcript_turn_marked_event() {
|
||||
let payload = json!({
|
||||
"type": "conversation.input_transcript.turn_marked",
|
||||
"transcript": "hello realtime"
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert_eq!(
|
||||
parse_realtime_event(payload.as_str(), RealtimeEventParser::V1),
|
||||
Some(RealtimeEvent::InputTranscriptDone(RealtimeTranscriptDone {
|
||||
text: "hello realtime".to_string(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_output_transcript_delta_event() {
|
||||
let payload = json!({
|
||||
@@ -1581,6 +1597,10 @@ mod tests {
|
||||
.expect("text");
|
||||
let third_json: Value = serde_json::from_str(&third).expect("json");
|
||||
assert_eq!(third_json["type"], "conversation.item.create");
|
||||
assert_eq!(
|
||||
third_json["item"]["content"][0]["type"],
|
||||
Value::String("input_text".to_string())
|
||||
);
|
||||
assert_eq!(third_json["item"]["content"][0]["text"], "hello agent");
|
||||
|
||||
let fourth = ws
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(super) fn conversation_item_create_message(text: String) -> RealtimeOutbound
|
||||
r#type: ConversationItemType::Message,
|
||||
role: ConversationRole::User,
|
||||
content: vec![ConversationItemContent {
|
||||
r#type: ConversationContentType::Text,
|
||||
r#type: ConversationContentType::InputText,
|
||||
text,
|
||||
}],
|
||||
}),
|
||||
|
||||
@@ -199,7 +199,6 @@ pub(super) struct ConversationItemContent {
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(super) enum ConversationContentType {
|
||||
Text,
|
||||
InputText,
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ pub(super) fn parse_realtime_event_v1(payload: &str) -> Option<RealtimeEvent> {
|
||||
| "conversation.item.input_audio_transcription.delta" => {
|
||||
parse_transcript_delta_event(&parsed, "delta").map(RealtimeEvent::InputTranscriptDelta)
|
||||
}
|
||||
"conversation.item.input_audio_transcription.completed" => {
|
||||
"conversation.input_transcript.turn_marked"
|
||||
| "conversation.item.input_audio_transcription.completed" => {
|
||||
parse_transcript_done_event(&parsed, "transcript")
|
||||
.map(RealtimeEvent::InputTranscriptDone)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn models_client_hits_models_endpoint() {
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -28,6 +28,7 @@ codex-utils-path = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
futures = { workspace = true, features = ["alloc", "std"] }
|
||||
gethostname = { workspace = true }
|
||||
indexmap = { workspace = true, features = ["serde"] }
|
||||
multimap = { workspace = true }
|
||||
prost = "0.14.3"
|
||||
schemars = { workspace = true }
|
||||
@@ -38,7 +39,7 @@ serde_path_to_error = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
toml = { workspace = true }
|
||||
toml = { workspace = true, features = ["preserve_order"] }
|
||||
toml_edit = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tonic-prost = { workspace = true }
|
||||
|
||||
@@ -359,7 +359,7 @@ pub struct ConfigToml {
|
||||
pub personality: Option<Personality>,
|
||||
|
||||
/// Optional explicit service tier request id for new turns (for example
|
||||
/// `priority` or `flex`; legacy `fast` also works).
|
||||
/// `default`, `priority`, or `flex`; legacy `fast` also works).
|
||||
pub service_tier: Option<String>,
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct HookEventsToml {
|
||||
pub user_prompt_submit: Vec<MatcherGroup>,
|
||||
#[serde(rename = "SubagentStart", default)]
|
||||
pub subagent_start: Vec<MatcherGroup>,
|
||||
#[serde(rename = "SubagentStop", default)]
|
||||
pub subagent_stop: Vec<MatcherGroup>,
|
||||
#[serde(rename = "Stop", default)]
|
||||
pub stop: Vec<MatcherGroup>,
|
||||
}
|
||||
@@ -62,6 +64,7 @@ impl HookEventsToml {
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
} = self;
|
||||
pre_tool_use.is_empty()
|
||||
@@ -72,6 +75,7 @@ impl HookEventsToml {
|
||||
&& session_start.is_empty()
|
||||
&& user_prompt_submit.is_empty()
|
||||
&& subagent_start.is_empty()
|
||||
&& subagent_stop.is_empty()
|
||||
&& stop.is_empty()
|
||||
}
|
||||
|
||||
@@ -85,6 +89,7 @@ impl HookEventsToml {
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
} = self;
|
||||
[
|
||||
@@ -96,6 +101,7 @@ impl HookEventsToml {
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
]
|
||||
.into_iter()
|
||||
@@ -104,7 +110,7 @@ impl HookEventsToml {
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 9] {
|
||||
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 10] {
|
||||
[
|
||||
(HookEventName::PreToolUse, self.pre_tool_use),
|
||||
(HookEventName::PermissionRequest, self.permission_request),
|
||||
@@ -114,6 +120,7 @@ impl HookEventsToml {
|
||||
(HookEventName::SessionStart, self.session_start),
|
||||
(HookEventName::UserPromptSubmit, self.user_prompt_submit),
|
||||
(HookEventName::SubagentStart, self.subagent_start),
|
||||
(HookEventName::SubagentStop, self.subagent_stop),
|
||||
(HookEventName::Stop, self.stop),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::merge::merge_toml_values;
|
||||
use codex_network_proxy::InjectedHeaderConfig;
|
||||
use codex_network_proxy::MitmHookActionsConfig;
|
||||
use codex_network_proxy::MitmHookBodyConfig;
|
||||
use codex_network_proxy::MitmHookConfig;
|
||||
use codex_network_proxy::MitmHookMatchConfig;
|
||||
use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use indexmap::IndexMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -350,6 +356,38 @@ pub struct NetworkToml {
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
pub mitm: Option<NetworkMitmToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct NetworkMitmToml {
|
||||
#[schemars(with = "Option<BTreeMap<String, NetworkMitmHookToml>>")]
|
||||
pub hooks: Option<IndexMap<String, NetworkMitmHookToml>>,
|
||||
#[schemars(with = "Option<BTreeMap<String, NetworkMitmActionToml>>")]
|
||||
pub actions: Option<IndexMap<String, NetworkMitmActionToml>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct NetworkMitmTomlUnchecked {
|
||||
pub hooks: Option<IndexMap<String, NetworkMitmHookToml>>,
|
||||
pub actions: Option<IndexMap<String, NetworkMitmActionToml>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct NetworkMitmHookToml {
|
||||
pub host: String,
|
||||
pub methods: Vec<String>,
|
||||
pub path_prefixes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub query: BTreeMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub headers: BTreeMap<String, Vec<String>>,
|
||||
#[schemars(with = "Option<MitmHookBodyConfigSchema>")]
|
||||
pub body: Option<MitmHookBodyConfig>,
|
||||
pub action: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -359,6 +397,114 @@ enum NetworkModeSchema {
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkMitmActionToml {
|
||||
pub strip_request_headers: Vec<String>,
|
||||
pub inject_request_headers: Vec<NetworkMitmInjectedHeaderToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkMitmInjectedHeaderToml {
|
||||
pub name: String,
|
||||
pub secret_env_var: Option<String>,
|
||||
pub secret_file: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
struct MitmHookBodyConfigSchema(pub serde_json::Value);
|
||||
|
||||
impl<'de> Deserialize<'de> for NetworkMitmToml {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let unchecked = NetworkMitmTomlUnchecked::deserialize(deserializer)?;
|
||||
let mitm = Self {
|
||||
hooks: unchecked.hooks,
|
||||
actions: unchecked.actions,
|
||||
};
|
||||
mitm.validate_action_definitions()
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(mitm)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmToml {
|
||||
pub fn validate_action_definitions(&self) -> Result<(), String> {
|
||||
if let Some(actions) = self.actions.as_ref() {
|
||||
for (action_name, action) in actions {
|
||||
if action.is_empty() {
|
||||
return Err(format!(
|
||||
"network.mitm.actions.{action_name} must define at least one operation"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(hooks) = self.hooks.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
if hook.action.is_empty() {
|
||||
return Err(format!(
|
||||
"network.mitm.hooks.{hook_name}.action must not be empty"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_action_references(
|
||||
&self,
|
||||
actions_by_name: &IndexMap<String, NetworkMitmActionToml>,
|
||||
) -> Result<(), String> {
|
||||
self.validate_action_definitions()?;
|
||||
|
||||
let Some(hooks) = self.hooks.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
for action_name in &hook.action {
|
||||
if !actions_by_name.contains_key(action_name) {
|
||||
return Err(format!(
|
||||
"network.mitm.hooks.{hook_name}.action references undefined action `{action_name}`"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_runtime_hooks(
|
||||
&self,
|
||||
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
|
||||
) -> Vec<MitmHookConfig> {
|
||||
self.hooks
|
||||
.as_ref()
|
||||
.map(|hooks| {
|
||||
hooks
|
||||
.values()
|
||||
.map(|hook| hook.to_runtime(actions_by_name))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmActionToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.strip_request_headers.is_empty() && self.inject_request_headers.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkToml {
|
||||
pub fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
@@ -411,6 +557,11 @@ impl NetworkToml {
|
||||
if let Some(allow_local_binding) = self.allow_local_binding {
|
||||
config.network.allow_local_binding = allow_local_binding;
|
||||
}
|
||||
if let Some(mitm) = self.mitm.as_ref() {
|
||||
config.network.mitm_hooks = mitm.to_runtime_hooks(mitm.actions.as_ref());
|
||||
}
|
||||
config.network.mitm =
|
||||
config.network.mode == NetworkMode::Limited || !config.network.mitm_hooks.is_empty();
|
||||
}
|
||||
|
||||
pub fn to_network_proxy_config(&self) -> NetworkProxyConfig {
|
||||
@@ -420,6 +571,61 @@ impl NetworkToml {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmHookToml {
|
||||
fn to_runtime(
|
||||
&self,
|
||||
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
|
||||
) -> MitmHookConfig {
|
||||
MitmHookConfig {
|
||||
host: self.host.clone(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: self.methods.clone(),
|
||||
path_prefixes: self.path_prefixes.clone(),
|
||||
query: self.query.clone(),
|
||||
headers: self.headers.clone(),
|
||||
body: self.body.clone(),
|
||||
},
|
||||
actions: self.selected_actions(actions_by_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_actions(
|
||||
&self,
|
||||
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
|
||||
) -> MitmHookActionsConfig {
|
||||
let Some(actions_by_name) = actions_by_name else {
|
||||
return MitmHookActionsConfig::default();
|
||||
};
|
||||
|
||||
let mut selected = MitmHookActionsConfig::default();
|
||||
for action_name in &self.action {
|
||||
if let Some(action) = actions_by_name.get(action_name) {
|
||||
selected
|
||||
.strip_request_headers
|
||||
.extend(action.strip_request_headers.clone());
|
||||
selected.inject_request_headers.extend(
|
||||
action
|
||||
.inject_request_headers
|
||||
.iter()
|
||||
.map(NetworkMitmInjectedHeaderToml::to_runtime),
|
||||
);
|
||||
}
|
||||
}
|
||||
selected
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmInjectedHeaderToml {
|
||||
fn to_runtime(&self) -> InjectedHeaderConfig {
|
||||
InjectedHeaderConfig {
|
||||
name: self.name.clone(),
|
||||
secret_env_var: self.secret_env_var.clone(),
|
||||
secret_file: self.secret_file.clone(),
|
||||
prefix: self.prefix.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlay_network_domain_permissions(
|
||||
config: &mut NetworkProxyConfig,
|
||||
domains: &NetworkDomainPermissionsToml,
|
||||
|
||||
@@ -24,7 +24,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
pub struct ConfigProfile {
|
||||
pub model: Option<String>,
|
||||
/// Optional explicit service tier request id for new turns (for example
|
||||
/// `priority` or `flex`; legacy `fast` also works).
|
||||
/// `default`, `priority`, or `flex`; legacy `fast` also works).
|
||||
pub service_tier: Option<String>,
|
||||
/// The key in the `model_providers` map identifying the
|
||||
/// [`ModelProviderInfo`] to use.
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::manifest::load_plugin_manifest;
|
||||
use crate::marketplace::MarketplaceError;
|
||||
use crate::marketplace::MarketplaceInterface;
|
||||
use crate::marketplace::MarketplaceListError;
|
||||
use crate::marketplace::MarketplaceListOutcome;
|
||||
use crate::marketplace::MarketplacePluginAuthPolicy;
|
||||
use crate::marketplace::MarketplacePluginPolicy;
|
||||
use crate::marketplace::MarketplacePluginSource;
|
||||
@@ -263,6 +264,7 @@ pub struct ConfiguredMarketplacePlugin {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub local_version: Option<String>,
|
||||
pub installed_version: Option<String>,
|
||||
pub source: MarketplacePluginSource,
|
||||
pub policy: MarketplacePluginPolicy,
|
||||
pub interface: Option<PluginManifestInterface>,
|
||||
@@ -1207,7 +1209,7 @@ impl PluginsManager {
|
||||
|
||||
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
|
||||
let marketplace_outcome =
|
||||
list_marketplaces(&self.marketplace_roots(config, additional_roots))?;
|
||||
self.discover_marketplaces_for_config(config, additional_roots)?;
|
||||
let mut seen_plugin_keys = HashSet::new();
|
||||
let marketplaces = marketplace_outcome
|
||||
.marketplaces
|
||||
@@ -1225,15 +1227,21 @@ impl PluginsManager {
|
||||
if !self.restriction_product_matches(plugin.policy.products.as_deref()) {
|
||||
return None;
|
||||
}
|
||||
let plugin_id =
|
||||
PluginId::new(plugin.name.clone(), marketplace_name.clone()).ok();
|
||||
let installed = installed_plugins.contains(&plugin_key);
|
||||
let installed_version = installed.then_some(()).and_then(|_| {
|
||||
plugin_id
|
||||
.as_ref()
|
||||
.and_then(|plugin_id| self.store.active_plugin_version(plugin_id))
|
||||
});
|
||||
let enabled = enabled_plugins.contains(&plugin_key);
|
||||
let mut interface = plugin.interface;
|
||||
let mut local_version = plugin.local_version;
|
||||
if installed
|
||||
&& matches!(&plugin.source, MarketplacePluginSource::Git { .. })
|
||||
&& let Ok(plugin_id) =
|
||||
PluginId::new(plugin.name.clone(), marketplace_name.clone())
|
||||
&& let Some(plugin_root) = self.store.active_plugin_root(&plugin_id)
|
||||
&& let Some(plugin_id) = plugin_id.as_ref()
|
||||
&& let Some(plugin_root) = self.store.active_plugin_root(plugin_id)
|
||||
&& let Some(manifest) = load_plugin_manifest(plugin_root.as_path())
|
||||
{
|
||||
local_version = manifest.version.clone();
|
||||
@@ -1251,6 +1259,7 @@ impl PluginsManager {
|
||||
// plugin entries from duplicate marketplace files intentionally
|
||||
// resolve to the first discovered source.
|
||||
id: plugin_key,
|
||||
installed_version,
|
||||
installed,
|
||||
enabled,
|
||||
name: plugin.name,
|
||||
@@ -1278,6 +1287,18 @@ impl PluginsManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discover_marketplaces_for_config(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
additional_roots: &[AbsolutePathBuf],
|
||||
) -> Result<MarketplaceListOutcome, MarketplaceError> {
|
||||
if !config.plugins_enabled {
|
||||
return Ok(MarketplaceListOutcome::default());
|
||||
}
|
||||
|
||||
list_marketplaces(&self.marketplace_roots(config, additional_roots))
|
||||
}
|
||||
|
||||
pub async fn read_plugin_for_config(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
@@ -1298,6 +1319,12 @@ impl PluginsManager {
|
||||
let marketplace_name = plugin.plugin_id.marketplace_name.clone();
|
||||
let plugin_key = plugin.plugin_id.as_key();
|
||||
let (installed_plugins, enabled_plugins) = self.configured_plugin_states(config);
|
||||
let installed = installed_plugins.contains(&plugin_key);
|
||||
let installed_version = if installed {
|
||||
self.store.active_plugin_version(&plugin.plugin_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let plugin = self
|
||||
.read_plugin_detail_for_marketplace_plugin(
|
||||
config,
|
||||
@@ -1309,6 +1336,7 @@ impl PluginsManager {
|
||||
.manifest
|
||||
.as_ref()
|
||||
.and_then(|manifest| manifest.version.clone()),
|
||||
installed_version,
|
||||
source: plugin.source,
|
||||
policy: plugin.policy,
|
||||
interface: plugin.interface,
|
||||
@@ -1317,7 +1345,7 @@ impl PluginsManager {
|
||||
.as_ref()
|
||||
.map(|manifest| manifest.keywords.clone())
|
||||
.unwrap_or_default(),
|
||||
installed: installed_plugins.contains(&plugin_key),
|
||||
installed,
|
||||
enabled: enabled_plugins.contains(&plugin_key),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1599,6 +1599,7 @@ enabled = false
|
||||
id: "enabled-plugin@debug".to_string(),
|
||||
name: "enabled-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: Some("local".to_string()),
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/enabled-plugin"))
|
||||
.unwrap(),
|
||||
@@ -1617,6 +1618,7 @@ enabled = false
|
||||
id: "disabled-plugin@debug".to_string(),
|
||||
name: "disabled-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: Some("local".to_string()),
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/disabled-plugin"),)
|
||||
.unwrap(),
|
||||
@@ -1738,6 +1740,7 @@ plugins = true
|
||||
id: "default-plugin@debug".to_string(),
|
||||
name: "default-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: None,
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/default-plugin")).unwrap(),
|
||||
},
|
||||
@@ -2170,6 +2173,7 @@ enabled = true
|
||||
id: "toolkit@debug".to_string(),
|
||||
name: "toolkit".to_string(),
|
||||
local_version: None,
|
||||
installed_version: Some("local".to_string()),
|
||||
source: MarketplacePluginSource::Git {
|
||||
url: missing_remote_repo_url,
|
||||
path: Some("plugins/toolkit".to_string()),
|
||||
@@ -2287,6 +2291,7 @@ plugins = true
|
||||
id: "linear@openai-curated".to_string(),
|
||||
name: "linear".to_string(),
|
||||
local_version: None,
|
||||
installed_version: None,
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(curated_root.join("plugins/linear")).unwrap(),
|
||||
},
|
||||
@@ -2582,6 +2587,7 @@ enabled = false
|
||||
id: "dup-plugin@debug".to_string(),
|
||||
name: "dup-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: None,
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo-a/from-a")).unwrap(),
|
||||
},
|
||||
@@ -2613,6 +2619,7 @@ enabled = false
|
||||
id: "b-only-plugin@debug".to_string(),
|
||||
name: "b-only-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: None,
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo-b/from-b-only")).unwrap(),
|
||||
},
|
||||
@@ -2698,6 +2705,7 @@ enabled = true
|
||||
id: "sample-plugin@debug".to_string(),
|
||||
name: "sample-plugin".to_string(),
|
||||
local_version: None,
|
||||
installed_version: None,
|
||||
source: MarketplacePluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(tmp.path().join("repo/sample-plugin")).unwrap(),
|
||||
},
|
||||
|
||||
@@ -698,7 +698,8 @@ pub fn plugin_interface_with_marketplace_category(
|
||||
interface
|
||||
}
|
||||
|
||||
fn marketplace_root_dir(
|
||||
#[doc(hidden)]
|
||||
pub fn marketplace_root_dir(
|
||||
marketplace_path: &AbsolutePathBuf,
|
||||
) -> Result<AbsolutePathBuf, MarketplaceError> {
|
||||
for relative_path in MARKETPLACE_MANIFEST_RELATIVE_PATHS {
|
||||
|
||||
@@ -686,7 +686,7 @@
|
||||
"$ref": "#/definitions/SandboxMode"
|
||||
},
|
||||
"service_tier": {
|
||||
"description": "Optional explicit service tier request id for new turns (for example `priority` or `flex`; legacy `fast` also works).",
|
||||
"description": "Optional explicit service tier request id for new turns (for example `default`, `priority`, or `flex`; legacy `fast` also works).",
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
@@ -1129,6 +1129,13 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"SubagentStop": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"default": [],
|
||||
"items": {
|
||||
@@ -1577,6 +1584,118 @@
|
||||
"NetworkDomainPermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmActionToml": {
|
||||
"properties": {
|
||||
"inject_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/NetworkMitmInjectedHeaderToml"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"strip_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmHookToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"action": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"body": true,
|
||||
"headers": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"methods": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"path_prefixes": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"query": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host",
|
||||
"methods",
|
||||
"path_prefixes"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmInjectedHeaderToml": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"prefix": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"secret_env_var": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"secret_file": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actions": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkMitmActionToml"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"hooks": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkMitmHookToml"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkModeSchema": {
|
||||
"enum": [
|
||||
"limited",
|
||||
@@ -1680,6 +1799,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mitm": {
|
||||
"$ref": "#/definitions/NetworkMitmToml"
|
||||
},
|
||||
"mode": {
|
||||
"$ref": "#/definitions/NetworkModeSchema"
|
||||
},
|
||||
@@ -4776,7 +4898,7 @@
|
||||
"description": "Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`."
|
||||
},
|
||||
"service_tier": {
|
||||
"description": "Optional explicit service tier request id for new turns (for example `priority` or `flex`; legacy `fast` also works).",
|
||||
"description": "Optional explicit service tier request id for new turns (for example `default`, `priority`, or `flex`; legacy `fast` also works).",
|
||||
"type": "string"
|
||||
},
|
||||
"shell_environment_policy": {
|
||||
@@ -4876,4 +4998,4 @@
|
||||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -748,8 +748,7 @@ impl ModelClient {
|
||||
prompt.output_schema_strict,
|
||||
);
|
||||
let prompt_cache_key = Some(self.state.thread_id.to_string());
|
||||
let service_tier =
|
||||
service_tier.filter(|service_tier| model_info.supports_service_tier(service_tier));
|
||||
let service_tier = model_info.service_tier_for_request(service_tier);
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions: instructions.clone(),
|
||||
|
||||
@@ -26,6 +26,9 @@ use codex_config::permissions_toml::FilesystemPermissionToml;
|
||||
use codex_config::permissions_toml::FilesystemPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkMitmActionToml;
|
||||
use codex_config::permissions_toml::NetworkMitmHookToml;
|
||||
use codex_config::permissions_toml::NetworkMitmToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
@@ -70,6 +73,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
@@ -95,6 +99,7 @@ use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use core_test_support::test_absolute_path;
|
||||
use indexmap::IndexMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::FormElicitationCapability;
|
||||
@@ -759,6 +764,15 @@ mode = "full"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"openai.com" = "allow"
|
||||
|
||||
[permissions.dev.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST", "PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.dev.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
@@ -816,6 +830,27 @@ mode = "full"
|
||||
}),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
mitm: Some(NetworkMitmToml {
|
||||
hooks: Some(IndexMap::from([(
|
||||
"github_write".to_string(),
|
||||
NetworkMitmHookToml {
|
||||
host: "api.github.com".to_string(),
|
||||
methods: vec!["POST".to_string(), "PUT".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
query: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
action: vec!["strip_auth".to_string()],
|
||||
},
|
||||
)])),
|
||||
actions: Some(IndexMap::from([(
|
||||
"strip_auth".to_string(),
|
||||
NetworkMitmActionToml {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
@@ -823,6 +858,140 @@ mode = "full"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_rejects_empty_mitm_action_reference_list() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = []
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#;
|
||||
|
||||
let err =
|
||||
toml::from_str::<ConfigToml>(toml).expect_err("empty MITM action refs should fail closed");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("network.mitm.hooks.github_write.action must not be empty"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_rejects_empty_mitm_action_definition() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
"#;
|
||||
|
||||
let err = toml::from_str::<ConfigToml>(toml)
|
||||
.expect_err("empty MITM action definitions should fail closed");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("network.mitm.actions.strip_auth must define at least one operation"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
|
||||
let network = NetworkToml {
|
||||
mode: Some(NetworkMode::Full),
|
||||
mitm: Some(NetworkMitmToml {
|
||||
hooks: Some(IndexMap::from([(
|
||||
"github_write".to_string(),
|
||||
NetworkMitmHookToml {
|
||||
host: "api.github.com".to_string(),
|
||||
methods: vec!["POST".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
action: vec!["strip_auth".to_string()],
|
||||
..NetworkMitmHookToml::default()
|
||||
},
|
||||
)])),
|
||||
actions: Some(IndexMap::from([(
|
||||
"strip_auth".to_string(),
|
||||
NetworkMitmActionToml {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
..NetworkToml::default()
|
||||
};
|
||||
|
||||
let config = network.to_network_proxy_config();
|
||||
|
||||
assert_eq!(config.network.mode, NetworkMode::Full);
|
||||
assert!(config.network.mitm);
|
||||
assert_eq!(config.network.mitm_hooks.len(), 1);
|
||||
assert_eq!(config.network.mitm_hooks[0].host, "api.github.com");
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].matcher.methods,
|
||||
vec!["POST".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].actions.strip_request_headers,
|
||||
vec!["authorization".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profile_network_to_proxy_config_preserves_mitm_hook_declaration_order() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.actions.noop]
|
||||
strip_request_headers = ["authorization"]
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.z_first]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["noop"]
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.a_second]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/"]
|
||||
action = ["noop"]
|
||||
"#;
|
||||
let cfg: ConfigToml = toml::from_str(toml).expect("permissions profile should deserialize");
|
||||
let permissions = cfg.permissions.expect("permissions should deserialize");
|
||||
let network = permissions
|
||||
.entries
|
||||
.get("workspace")
|
||||
.expect("workspace profile should exist")
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("network profile should exist");
|
||||
|
||||
let config = network.to_network_proxy_config();
|
||||
|
||||
assert_eq!(config.network.mitm_hooks.len(), 2);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].matcher.path_prefixes,
|
||||
vec!["/repos/openai/".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[1].matcher.path_prefixes,
|
||||
vec!["/repos/".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_without_feature()
|
||||
-> std::io::Result<()> {
|
||||
@@ -8277,7 +8446,7 @@ alpha = "one\ntwo"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std::io::Result<()> {
|
||||
async fn explicit_null_service_tier_override_maps_to_default_service_tier() -> std::io::Result<()> {
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
@@ -8291,8 +8460,33 @@ async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std:
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.service_tier, None);
|
||||
assert_eq!(config.notices.fast_default_opt_out, Some(true));
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
assert_eq!(config.notices.fast_default_opt_out, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_service_tier_override_uses_default_request_value() -> std::io::Result<()> {
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
fixture.cfg.clone(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(fixture.cwd_path()),
|
||||
service_tier: Some(Some("default".to_string())),
|
||||
..Default::default()
|
||||
},
|
||||
fixture.codex_home(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9712,6 +9906,73 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn danger_full_access_with_never_is_rejected_when_requirements_force_read_only()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let err = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("requirements-constrained yolo should require sandbox approval");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn named_full_access_profile_with_never_is_rejected_when_requirements_force_read_only()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"approval_policy = "never"
|
||||
default_permissions = "dev"
|
||||
|
||||
[permissions.dev.filesystem]
|
||||
":root" = "write"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let err = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("requirements-constrained full-access profile should require sandbox approval");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_falls_back_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -40,8 +40,6 @@ pub enum ConfigEdit {
|
||||
SetNoticeHideFullAccessWarning(bool),
|
||||
/// Toggle the Windows world-writable directories warning acknowledgement flag.
|
||||
SetNoticeHideWorldWritableWarning(bool),
|
||||
/// Toggle the opt-out marker for Codex-managed fast defaults.
|
||||
SetNoticeFastDefaultOptOut(bool),
|
||||
/// Toggle the rate limit model nudge acknowledgement flag.
|
||||
SetNoticeHideRateLimitModelNudge(bool),
|
||||
/// Toggle the model migration prompt acknowledgement flag.
|
||||
@@ -552,6 +550,8 @@ impl ConfigDocument {
|
||||
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
|
||||
&["service_tier"],
|
||||
service_tier.as_ref().map(|service_tier| {
|
||||
// Keep the legacy config spelling stable. Runtime values use
|
||||
// `priority`, but config.toml continues to store it as `fast`.
|
||||
let config_value = match ServiceTier::from_request_value(service_tier) {
|
||||
Some(ServiceTier::Fast) => "fast",
|
||||
Some(ServiceTier::Flex) => "flex",
|
||||
@@ -574,11 +574,6 @@ impl ConfigDocument {
|
||||
&[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::SetNoticeFastDefaultOptOut(opted_out) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "fast_default_opt_out"],
|
||||
value(*opted_out),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
|
||||
@@ -1182,12 +1177,6 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_fast_default_opt_out(mut self, opted_out: bool) -> Self {
|
||||
self.edits
|
||||
.push(ConfigEdit::SetNoticeFastDefaultOptOut(opted_out));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self {
|
||||
self.edits
|
||||
.push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged));
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_config::types::McpServerOAuthConfig;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -34,6 +35,20 @@ model_reasoning_effort = "high"
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_service_tier_saves_default_as_default() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.set_service_tier(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
assert_eq!(contents, "service_tier = \"default\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_service_tier_saves_priority_as_fast() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
@@ -519,6 +520,7 @@ pub struct Config {
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Effective service tier request id preference for new turns.
|
||||
/// `default` means the user explicitly selected standard routing.
|
||||
pub service_tier: Option<String>,
|
||||
|
||||
/// Model used specifically for review sessions.
|
||||
@@ -3136,15 +3138,10 @@ impl Config {
|
||||
let forced_login_method = cfg.forced_login_method;
|
||||
|
||||
let model = model.or(config_profile.model).or(cfg.model);
|
||||
let mut notices = cfg.notice.unwrap_or_default();
|
||||
let notices = cfg.notice.unwrap_or_default();
|
||||
let service_tier = match service_tier_override {
|
||||
Some(Some(service_tier)) => Some(service_tier),
|
||||
Some(None) => {
|
||||
// Preserve explicit standard/clear intent after the nested override
|
||||
// collapses into `Config.service_tier = None`.
|
||||
notices.fast_default_opt_out = Some(true);
|
||||
None
|
||||
}
|
||||
Some(None) => Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
None => config_profile.service_tier.or(cfg.service_tier),
|
||||
};
|
||||
let service_tier = service_tier.and_then(|service_tier| {
|
||||
@@ -3304,6 +3301,17 @@ impl Config {
|
||||
&mut constrained_permission_profile,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
if permission_profile_was_constrained
|
||||
&& sandbox_mode_requirement_for_permission_profile(&original_permission_profile)
|
||||
== SandboxModeRequirement::DangerFullAccess
|
||||
&& constrained_permission_profile.get() == &PermissionProfile::read_only()
|
||||
&& constrained_approval_policy.value() == AskForApproval::Never
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"`approval_policy = \"never\"` cannot be used because requirements do not allow `sandbox_mode = \"danger-full-access\"`; Codex would fall back to read-only permissions with approvals disabled. Choose an `approval_policy` based on what you need, such as `on-request`, or choose an allowed sandbox mode.",
|
||||
));
|
||||
}
|
||||
if permission_profile_was_constrained {
|
||||
// The selected profile no longer describes the effective
|
||||
// permissions after requirements forced a fallback.
|
||||
|
||||
@@ -186,6 +186,7 @@ pub(crate) fn apply_network_proxy_feature_config(
|
||||
}
|
||||
}),
|
||||
allow_local_binding: feature_config.allow_local_binding,
|
||||
mitm: None,
|
||||
}
|
||||
.apply_to_network_proxy_config(config);
|
||||
}
|
||||
|
||||
+24
-10
@@ -433,10 +433,10 @@ pub(crate) async fn execute_exec_request(
|
||||
expiration,
|
||||
capture_policy,
|
||||
sandbox,
|
||||
windows_sandbox_policy_cwd: _,
|
||||
windows_sandbox_policy_cwd,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile: _,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy: _,
|
||||
network_sandbox_policy,
|
||||
windows_sandbox_filesystem_overrides,
|
||||
@@ -465,6 +465,8 @@ pub(crate) async fn execute_exec_request(
|
||||
after_spawn,
|
||||
sandbox,
|
||||
&sandbox_policy,
|
||||
&permission_profile,
|
||||
&windows_sandbox_policy_cwd,
|
||||
windows_sandbox_filesystem_overrides.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -472,6 +474,7 @@ pub(crate) async fn execute_exec_request(
|
||||
finalize_exec_result(raw_output_result, sandbox, duration)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn get_raw_output_result(
|
||||
params: ExecParams,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
@@ -479,14 +482,22 @@ async fn get_raw_output_result(
|
||||
after_spawn: Option<Box<dyn FnOnce() + Send>>,
|
||||
#[cfg_attr(not(windows), allow(unused_variables))] sandbox: SandboxType,
|
||||
#[cfg_attr(not(windows), allow(unused_variables))] sandbox_policy: &SandboxPolicy,
|
||||
#[cfg_attr(not(windows), allow(unused_variables))] permission_profile: &PermissionProfile,
|
||||
#[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
#[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_filesystem_overrides: Option<
|
||||
&WindowsSandboxFilesystemOverrides,
|
||||
>,
|
||||
) -> Result<RawExecToolCallOutput> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if sandbox == SandboxType::WindowsRestrictedToken {
|
||||
return exec_windows_sandbox(params, sandbox_policy, windows_sandbox_filesystem_overrides)
|
||||
.await;
|
||||
return exec_windows_sandbox(
|
||||
params,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
windows_sandbox_policy_cwd,
|
||||
windows_sandbox_filesystem_overrides,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
exec(params, network_sandbox_policy, stdout_stream, after_spawn).await
|
||||
@@ -562,10 +573,12 @@ fn record_windows_sandbox_spawn_failure(
|
||||
async fn exec_windows_sandbox(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
windows_sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>,
|
||||
) -> Result<RawExecToolCallOutput> {
|
||||
use crate::config::find_codex_home;
|
||||
use codex_windows_sandbox::run_windows_sandbox_capture_elevated;
|
||||
use codex_windows_sandbox::run_windows_sandbox_capture_for_permission_profile_elevated;
|
||||
use codex_windows_sandbox::run_windows_sandbox_capture_with_filesystem_overrides;
|
||||
|
||||
let ExecParams {
|
||||
@@ -596,7 +609,8 @@ async fn exec_windows_sandbox(
|
||||
"failed to serialize Windows sandbox policy: {err}"
|
||||
)))
|
||||
})?;
|
||||
let sandbox_cwd = cwd.clone();
|
||||
let sandbox_cwd = windows_sandbox_policy_cwd.clone();
|
||||
let permission_profile = permission_profile.clone();
|
||||
let codex_home = find_codex_home().map_err(|err| {
|
||||
CodexErr::Io(io::Error::other(format!(
|
||||
"windows sandbox: failed to resolve codex_home: {err}"
|
||||
@@ -620,10 +634,10 @@ async fn exec_windows_sandbox(
|
||||
.and_then(|overrides| overrides.write_roots_override.clone());
|
||||
let spawn_res = tokio::task::spawn_blocking(move || {
|
||||
if use_elevated {
|
||||
run_windows_sandbox_capture_elevated(
|
||||
codex_windows_sandbox::ElevatedSandboxCaptureRequest {
|
||||
policy_json_or_preset: policy_str.as_str(),
|
||||
sandbox_policy_cwd: &sandbox_cwd,
|
||||
run_windows_sandbox_capture_for_permission_profile_elevated(
|
||||
codex_windows_sandbox::ElevatedSandboxProfileCaptureRequest {
|
||||
permission_profile: &permission_profile,
|
||||
permission_profile_cwd: &sandbox_cwd,
|
||||
codex_home: codex_home.as_ref(),
|
||||
command,
|
||||
cwd: &cwd,
|
||||
|
||||
@@ -14,8 +14,8 @@ use codex_hooks::PreToolUseOutcome;
|
||||
use codex_hooks::PreToolUseRequest;
|
||||
use codex_hooks::SessionStartOutcome;
|
||||
use codex_hooks::StartHookTarget;
|
||||
use codex_hooks::StopHookTarget;
|
||||
use codex_hooks::StopOutcome;
|
||||
use codex_hooks::StopRequest;
|
||||
use codex_hooks::UserPromptSubmitOutcome;
|
||||
use codex_hooks::UserPromptSubmitRequest;
|
||||
use codex_otel::HOOK_RUN_DURATION_METRIC;
|
||||
@@ -33,6 +33,7 @@ use codex_protocol::protocol::HookSource;
|
||||
use codex_protocol::protocol::HookStartedEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
@@ -288,6 +289,78 @@ pub(crate) async fn run_post_tool_use_hooks(
|
||||
outcome
|
||||
}
|
||||
|
||||
pub(crate) async fn run_turn_stop_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
stop_hook_active: bool,
|
||||
last_assistant_message: Option<String>,
|
||||
) -> StopOutcome {
|
||||
// Resolve the stop hook kind from the session source before building the
|
||||
// request. Root turns run Stop; thread-spawned child turns run SubagentStop.
|
||||
let (target, transcript_path) = match &turn_context.session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
agent_role,
|
||||
parent_thread_id,
|
||||
..
|
||||
}) => {
|
||||
let agent_type = agent_role
|
||||
.clone()
|
||||
.unwrap_or_else(|| crate::agent::role::DEFAULT_ROLE_NAME.to_string());
|
||||
let agent_transcript_path = sess.hook_transcript_path().await;
|
||||
let parent_transcript_path = match sess
|
||||
.services
|
||||
.thread_store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id: *parent_thread_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(thread) => thread.rollout_path,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
parent_thread_id = %parent_thread_id,
|
||||
error = %error,
|
||||
"failed to resolve parent transcript path for subagent hook"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
(
|
||||
StopHookTarget::SubagentStop {
|
||||
agent_id: sess.thread_id().to_string(),
|
||||
agent_type,
|
||||
agent_transcript_path,
|
||||
},
|
||||
parent_transcript_path,
|
||||
)
|
||||
}
|
||||
// Internal/synthetic subagents do not expose user-configured lifecycle
|
||||
// hooks, so there is no Stop or SubagentStop request to dispatch.
|
||||
SessionSource::SubAgent(_) => return StopOutcome::default(),
|
||||
_ => (StopHookTarget::Stop, sess.hook_transcript_path().await),
|
||||
};
|
||||
let request = codex_hooks::StopRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
stop_hook_active,
|
||||
last_assistant_message,
|
||||
target,
|
||||
};
|
||||
let hooks = sess.hooks();
|
||||
emit_hook_started_events(sess, turn_context, hooks.preview_stop(&request)).await;
|
||||
|
||||
let mut outcome = hooks.run_stop(request).await;
|
||||
emit_hook_completed_events(sess, turn_context, std::mem::take(&mut outcome.hook_events)).await;
|
||||
outcome
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pre_compact_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -352,30 +425,6 @@ pub(crate) async fn run_post_compact_hooks(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_stop_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
stop_hook_active: bool,
|
||||
last_assistant_message: Option<String>,
|
||||
) -> StopOutcome {
|
||||
let request = StopRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
stop_hook_active,
|
||||
last_assistant_message,
|
||||
};
|
||||
let hooks = sess.hooks();
|
||||
emit_hook_started_events(sess, turn_context, hooks.preview_stop(&request)).await;
|
||||
let mut outcome = hooks.run_stop(request).await;
|
||||
emit_hook_completed_events(sess, turn_context, std::mem::take(&mut outcome.hook_events)).await;
|
||||
outcome
|
||||
}
|
||||
|
||||
pub(crate) async fn run_legacy_after_agent_hook(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -638,6 +687,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
|
||||
HookEventName::SessionStart => "SessionStart",
|
||||
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
||||
HookEventName::SubagentStart => "SubagentStart",
|
||||
HookEventName::SubagentStop => "SubagentStop",
|
||||
HookEventName::Stop => "Stop",
|
||||
};
|
||||
let hook_source = match run.source {
|
||||
|
||||
@@ -138,9 +138,14 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
|
||||
let metadata =
|
||||
lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;
|
||||
let mcp_app_resource_uri = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.mcp_app_resource_uri.clone());
|
||||
let item_metadata = McpToolCallItemMetadata {
|
||||
mcp_app_resource_uri: metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
|
||||
plugin_id: metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.plugin_id.clone()),
|
||||
};
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connectors::app_tool_policy(
|
||||
&turn_context.config,
|
||||
@@ -171,7 +176,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
item_metadata.clone(),
|
||||
"MCP tool call blocked by app configuration".to_string(),
|
||||
/*already_started*/ false,
|
||||
)
|
||||
@@ -200,7 +205,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation.clone(),
|
||||
mcp_app_resource_uri.clone(),
|
||||
item_metadata.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -225,7 +230,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
&call_id,
|
||||
invocation,
|
||||
metadata.as_ref(),
|
||||
mcp_app_resource_uri,
|
||||
item_metadata,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -236,7 +241,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
item_metadata.clone(),
|
||||
message,
|
||||
/*already_started*/ true,
|
||||
)
|
||||
@@ -249,7 +254,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
item_metadata.clone(),
|
||||
message,
|
||||
/*already_started*/ true,
|
||||
)
|
||||
@@ -280,7 +285,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
&call_id,
|
||||
invocation,
|
||||
metadata.as_ref(),
|
||||
mcp_app_resource_uri,
|
||||
item_metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -290,13 +295,19 @@ pub(crate) struct HandledMcpToolCall {
|
||||
pub(crate) tool_input: JsonValue,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct McpToolCallItemMetadata {
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
plugin_id: Option<String>,
|
||||
}
|
||||
|
||||
async fn handle_approved_mcp_tool_call(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
item_metadata: McpToolCallItemMetadata,
|
||||
) -> HandledMcpToolCall {
|
||||
let server = invocation.server.clone();
|
||||
maybe_mark_thread_memory_mode_polluted(sess, turn_context, &server).await;
|
||||
@@ -365,7 +376,7 @@ async fn handle_approved_mcp_tool_call(
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
item_metadata,
|
||||
duration,
|
||||
truncate_mcp_tool_result_for_event(&result),
|
||||
)
|
||||
@@ -840,7 +851,7 @@ async fn notify_mcp_tool_call_started(
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
item_metadata: McpToolCallItemMetadata,
|
||||
) {
|
||||
let McpInvocation {
|
||||
server,
|
||||
@@ -852,7 +863,8 @@ async fn notify_mcp_tool_call_started(
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri,
|
||||
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
|
||||
plugin_id: item_metadata.plugin_id,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
result: None,
|
||||
error: None,
|
||||
@@ -866,7 +878,7 @@ async fn notify_mcp_tool_call_completed(
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
item_metadata: McpToolCallItemMetadata,
|
||||
duration: Duration,
|
||||
result: Result<CallToolResult, String>,
|
||||
) {
|
||||
@@ -891,7 +903,8 @@ async fn notify_mcp_tool_call_completed(
|
||||
server,
|
||||
tool,
|
||||
arguments: arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri,
|
||||
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
|
||||
plugin_id: item_metadata.plugin_id,
|
||||
status,
|
||||
result,
|
||||
error,
|
||||
@@ -2092,7 +2105,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
item_metadata: McpToolCallItemMetadata,
|
||||
message: String,
|
||||
already_started: bool,
|
||||
) -> Result<CallToolResult, String> {
|
||||
@@ -2102,7 +2115,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation.clone(),
|
||||
mcp_app_resource_uri.clone(),
|
||||
item_metadata.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2112,7 +2125,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
turn_context,
|
||||
call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
item_metadata,
|
||||
Duration::ZERO,
|
||||
truncate_mcp_tool_result_for_event(&Err(message.clone())),
|
||||
)
|
||||
|
||||
@@ -1112,6 +1112,40 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_call_item_includes_plugin_id() {
|
||||
let (session, turn_context, rx_event) = make_session_and_context_with_rx().await;
|
||||
|
||||
notify_mcp_tool_call_started(
|
||||
&session,
|
||||
&turn_context,
|
||||
"call-plugin",
|
||||
McpInvocation {
|
||||
server: "sample".to_string(),
|
||||
tool: "echo".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
McpToolCallItemMetadata {
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx_event.recv())
|
||||
.await
|
||||
.expect("tool call item timed out")
|
||||
.expect("tool call item event");
|
||||
let EventMsg::ItemStarted(item_started) = event.msg else {
|
||||
panic!("expected ItemStarted event");
|
||||
};
|
||||
let TurnItem::McpToolCall(item) = item_started.item else {
|
||||
panic!("expected MCP tool call item");
|
||||
};
|
||||
|
||||
assert_eq!(item.plugin_id.as_deref(), Some("sample@test"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps_meta() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
|
||||
@@ -16,12 +16,16 @@ use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_config::permissions_toml::NetworkMitmActionToml;
|
||||
use codex_config::permissions_toml::NetworkMitmHookToml;
|
||||
use codex_config::permissions_toml::NetworkMitmToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::overlay_network_domain_permissions;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraintError;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
@@ -30,6 +34,7 @@ use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -208,6 +213,7 @@ fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<Netw
|
||||
Ok(profile.profile.network)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
network.apply_to_network_proxy_config(config);
|
||||
@@ -215,11 +221,57 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NetworkConfigAccumulator {
|
||||
config: NetworkProxyConfig,
|
||||
mitm_hooks: IndexMap<String, NetworkMitmHookToml>,
|
||||
mitm_actions: IndexMap<String, NetworkMitmActionToml>,
|
||||
}
|
||||
|
||||
impl NetworkConfigAccumulator {
|
||||
fn apply_network_tables(&mut self, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
self.apply_network(network);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_network(&mut self, mut network: NetworkToml) {
|
||||
let mitm = network.mitm.take();
|
||||
network.apply_to_network_proxy_config(&mut self.config);
|
||||
|
||||
if let Some(mitm) = mitm {
|
||||
if let Some(actions) = mitm.actions {
|
||||
self.mitm_actions.extend(actions);
|
||||
}
|
||||
if let Some(hooks) = mitm.hooks {
|
||||
self.mitm_hooks.extend(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Result<NetworkProxyConfig> {
|
||||
if !self.mitm_hooks.is_empty() {
|
||||
let actions = self.mitm_actions;
|
||||
let mitm = NetworkMitmToml {
|
||||
hooks: Some(self.mitm_hooks),
|
||||
actions: Some(actions.clone()),
|
||||
};
|
||||
mitm.validate_action_references(&actions)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.config.network.mitm_hooks = mitm.to_runtime_hooks(Some(&actions));
|
||||
}
|
||||
|
||||
self.config.network.mitm = self.config.network.mode == NetworkMode::Limited
|
||||
|| !self.config.network.mitm_hooks.is_empty();
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_layers(
|
||||
layers: &ConfigLayerStack,
|
||||
exec_policy: &codex_execpolicy::Policy,
|
||||
) -> Result<NetworkProxyConfig> {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
let mut merged = toml::Value::Table(toml::map::Map::new());
|
||||
for layer in layers.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
@@ -228,7 +280,9 @@ fn config_from_layers(
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
let parsed = network_tables_from_toml(&merged)?;
|
||||
apply_network_tables(&mut config, parsed)?;
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator.apply_network_tables(parsed)?;
|
||||
let mut config = accumulator.finish()?;
|
||||
apply_exec_policy_network_rules(&mut config, exec_policy);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -112,6 +112,76 @@ default_permissions = "dev"
|
||||
assert_eq!(config.network.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_named_mitm_actions() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["x-api-key"]
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
let config = accumulator.finish().expect("merged config should build");
|
||||
|
||||
assert_eq!(config.network.mode, codex_network_proxy::NetworkMode::Full);
|
||||
assert!(config.network.mitm);
|
||||
assert_eq!(
|
||||
config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"lower.example.com".to_string(),
|
||||
"higher.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(config.network.mitm_hooks.len(), 1);
|
||||
assert_eq!(config.network.mitm_hooks[0].host, "api.github.com");
|
||||
assert_eq!(config.network.mitm_hooks[0].matcher.methods, vec!["POST"]);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].actions.strip_request_headers,
|
||||
vec!["x-api-key"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execpolicy_network_rules_overlay_network_lists() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
@@ -641,12 +641,14 @@ async fn prepare_realtime_start(
|
||||
realtime_request_headers(
|
||||
requested_realtime_session_id.as_deref(),
|
||||
Some(realtime_api_key.as_str()),
|
||||
version,
|
||||
)?
|
||||
}
|
||||
ConversationStartTransport::Webrtc { .. } => {
|
||||
realtime_request_headers(
|
||||
requested_realtime_session_id.as_deref(),
|
||||
/*api_key*/ None,
|
||||
version,
|
||||
)?
|
||||
}
|
||||
};
|
||||
@@ -973,9 +975,14 @@ fn realtime_api_key(auth: Option<&CodexAuth>, provider: &ModelProviderInfo) -> C
|
||||
fn realtime_request_headers(
|
||||
realtime_session_id: Option<&str>,
|
||||
api_key: Option<&str>,
|
||||
version: RealtimeWsVersion,
|
||||
) -> CodexResult<Option<HeaderMap>> {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
if version == RealtimeWsVersion::V1 {
|
||||
headers.insert("openai-alpha", HeaderValue::from_static("quicksilver=v1"));
|
||||
}
|
||||
|
||||
if let Some(realtime_session_id) = realtime_session_id
|
||||
&& let Ok(realtime_session_id) = HeaderValue::from_str(realtime_session_id)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::RealtimeHandoffState;
|
||||
use super::RealtimeSessionKind;
|
||||
use super::realtime_delegation_from_handoff;
|
||||
use super::realtime_request_headers;
|
||||
use super::realtime_text_from_handoff_request;
|
||||
use super::wrap_realtime_delegation_input;
|
||||
use async_channel::bounded;
|
||||
use codex_config::config_toml::RealtimeWsVersion;
|
||||
use codex_protocol::protocol::RealtimeHandoffRequested;
|
||||
use codex_protocol::protocol::RealtimeTranscriptEntry;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -137,3 +139,28 @@ async fn clears_active_handoff_explicitly() {
|
||||
*state.active_handoff.lock().await = None;
|
||||
assert_eq!(state.active_handoff.lock().await.clone(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_quicksilver_alpha_header_for_realtime_v1() {
|
||||
let headers =
|
||||
realtime_request_headers(Some("session_1"), Some("sk-test"), RealtimeWsVersion::V1)
|
||||
.expect("headers")
|
||||
.expect("headers");
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get("openai-alpha")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("quicksilver=v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omits_quicksilver_alpha_header_for_realtime_v2() {
|
||||
let headers =
|
||||
realtime_request_headers(Some("session_1"), Some("sk-test"), RealtimeWsVersion::V2)
|
||||
.expect("headers")
|
||||
.expect("headers");
|
||||
|
||||
assert!(headers.get("openai-alpha").is_none());
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ use codex_otel::current_span_trace_id;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
@@ -80,6 +79,7 @@ use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
@@ -320,7 +320,6 @@ use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
@@ -589,14 +588,10 @@ impl Codex {
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let account_plan_type = auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.account_plan_type());
|
||||
let service_tier = get_service_tier(
|
||||
config.service_tier.clone(),
|
||||
config.notices.fast_default_opt_out.unwrap_or(false),
|
||||
account_plan_type,
|
||||
config.features.enabled(Feature::FastMode),
|
||||
&model_info,
|
||||
);
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
@@ -798,17 +793,16 @@ impl Codex {
|
||||
|
||||
fn get_service_tier(
|
||||
configured_service_tier: Option<String>,
|
||||
fast_default_opt_out: bool,
|
||||
account_plan_type: Option<AccountPlanType>,
|
||||
fast_mode_enabled: bool,
|
||||
model_info: &ModelInfo,
|
||||
) -> Option<String> {
|
||||
if configured_service_tier.is_some() || fast_default_opt_out || !fast_mode_enabled {
|
||||
return configured_service_tier;
|
||||
if !fast_mode_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
account_plan_type
|
||||
.is_some_and(is_enterprise_default_service_tier_plan)
|
||||
.then_some(ServiceTier::Fast.request_value().to_string())
|
||||
configured_service_tier.filter(|service_tier| {
|
||||
service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
|| model_info.supports_service_tier(service_tier)
|
||||
})
|
||||
}
|
||||
|
||||
fn session_permission_profile_state_from_config(
|
||||
@@ -817,12 +811,6 @@ fn session_permission_profile_state_from_config(
|
||||
Ok(config.permissions.permission_profile_state().clone())
|
||||
}
|
||||
|
||||
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
|
||||
plan_type == AccountPlanType::Enterprise
|
||||
|| plan_type.is_business_like()
|
||||
|| plan_type.is_team_like()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination {
|
||||
futures::future::ready(()).boxed().shared()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user