Merge branch 'main' into dev/cc/zsh-fork-debug-2

This commit is contained in:
Celia Chen
2026-03-19 20:00:10 -07:00
committed by GitHub
416 changed files with 29813 additions and 5136 deletions
+79 -20
View File
@@ -1427,7 +1427,7 @@ dependencies = [
"codex-chatgpt",
"codex-cloud-requirements",
"codex-core",
"codex-environment",
"codex-exec-server",
"codex-feedback",
"codex-file-search",
"codex-login",
@@ -1570,11 +1570,13 @@ name = "codex-artifacts"
version = "0.0.0"
dependencies = [
"codex-package-manager",
"flate2",
"pretty_assertions",
"reqwest",
"serde",
"serde_json",
"sha2",
"tar",
"tempfile",
"thiserror 2.0.18",
"tokio",
@@ -1665,6 +1667,7 @@ dependencies = [
"codex-shell-escalation",
"codex-state",
"codex-stdio-to-uds",
"codex-terminal-detection",
"codex-tui",
"codex-tui-app-server",
"codex-utils-absolute-path",
@@ -1843,15 +1846,14 @@ dependencies = [
"codex-arg0",
"codex-artifacts",
"codex-async-utils",
"codex-client",
"codex-config",
"codex-connectors",
"codex-environment",
"codex-exec-server",
"codex-execpolicy",
"codex-file-search",
"codex-git",
"codex-hooks",
"codex-keyring-store",
"codex-login",
"codex-network-proxy",
"codex-otel",
"codex-protocol",
@@ -1861,6 +1863,7 @@ dependencies = [
"codex-shell-escalation",
"codex-skills",
"codex-state",
"codex-terminal-detection",
"codex-test-macros",
"codex-utils-absolute-path",
"codex-utils-cache",
@@ -1887,7 +1890,6 @@ dependencies = [
"image",
"indexmap 2.13.0",
"insta",
"keyring",
"landlock",
"libc",
"maplit",
@@ -1896,7 +1898,6 @@ dependencies = [
"openssl-sys",
"opentelemetry",
"opentelemetry_sdk",
"os_info",
"predicates",
"pretty_assertions",
"rand 0.9.2",
@@ -1910,7 +1911,6 @@ dependencies = [
"serde_yaml",
"serial_test",
"sha1",
"sha2",
"shlex",
"similar",
"tempfile",
@@ -1950,17 +1950,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "codex-environment"
version = "0.0.0"
dependencies = [
"async-trait",
"codex-utils-absolute-path",
"pretty_assertions",
"tempfile",
"tokio",
]
[[package]]
name = "codex-exec"
version = "0.0.0"
@@ -2006,6 +1995,30 @@ dependencies = [
"wiremock",
]
[[package]]
name = "codex-exec-server"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"clap",
"codex-app-server-protocol",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-pty",
"futures",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
"test-case",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite",
"tracing",
]
[[package]]
name = "codex-execpolicy"
version = "0.0.0"
@@ -2161,19 +2174,30 @@ name = "codex-login"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-client",
"codex-core",
"codex-config",
"codex-keyring-store",
"codex-protocol",
"codex-terminal-detection",
"core_test_support",
"keyring",
"once_cell",
"os_info",
"pretty_assertions",
"rand 0.9.2",
"regex-lite",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"serial_test",
"sha2",
"tempfile",
"thiserror 2.0.18",
"tiny_http",
"tokio",
"tracing",
@@ -2265,6 +2289,7 @@ version = "0.0.0"
dependencies = [
"chrono",
"codex-api",
"codex-app-server-protocol",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-string",
@@ -2330,8 +2355,8 @@ dependencies = [
"icu_decimal",
"icu_locale_core",
"icu_provider",
"mime_guess",
"pretty_assertions",
"quick-xml",
"schemars 0.8.22",
"serde",
"serde_json",
@@ -2480,6 +2505,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum 0.27.2",
"tokio",
"tracing",
"tracing-subscriber",
@@ -2497,6 +2523,14 @@ dependencies = [
"uds_windows",
]
[[package]]
name = "codex-terminal-detection"
version = "0.0.0"
dependencies = [
"pretty_assertions",
"tracing",
]
[[package]]
name = "codex-test-macros"
version = "0.0.0"
@@ -2532,6 +2566,7 @@ dependencies = [
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-terminal-detection",
"codex-tui-app-server",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
@@ -2623,6 +2658,7 @@ dependencies = [
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-terminal-detection",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
"codex-utils-cargo-bin",
@@ -2761,6 +2797,7 @@ dependencies = [
"base64 0.22.1",
"codex-utils-cache",
"image",
"mime_guess",
"thiserror 2.0.18",
"tokio",
]
@@ -3071,6 +3108,8 @@ dependencies = [
"ctor 0.6.3",
"futures",
"notify",
"opentelemetry",
"opentelemetry_sdk",
"pretty_assertions",
"regex-lite",
"reqwest",
@@ -3079,6 +3118,9 @@ dependencies = [
"tempfile",
"tokio",
"tokio-tungstenite",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
"walkdir",
"wiremock",
"zstd",
@@ -5799,6 +5841,7 @@ dependencies = [
"anyhow",
"codex-core",
"codex-mcp-server",
"codex-terminal-detection",
"codex-utils-cargo-bin",
"core_test_support",
"os_info",
@@ -7251,6 +7294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
"serde",
]
[[package]]
@@ -9370,6 +9414,9 @@ name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.2",
]
[[package]]
name = "strum_macros"
@@ -9384,6 +9431,18 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
+5 -2
View File
@@ -22,10 +22,10 @@ members = [
"shell-escalation",
"skills",
"core",
"environment",
"hooks",
"secrets",
"exec",
"exec-server",
"execpolicy",
"execpolicy-legacy",
"keyring-store",
@@ -66,6 +66,7 @@ members = [
"codex-client",
"codex-api",
"state",
"terminal-detection",
"codex-experimental-api-macros",
"test-macros",
"package-manager",
@@ -104,8 +105,8 @@ codex-cloud-requirements = { path = "cloud-requirements" }
codex-connectors = { path = "connectors" }
codex-config = { path = "config" }
codex-core = { path = "core" }
codex-environment = { path = "environment" }
codex-exec = { path = "exec" }
codex-exec-server = { path = "exec-server" }
codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-feedback = { path = "feedback" }
@@ -131,6 +132,7 @@ codex-skills = { path = "skills" }
codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-test-macros = { path = "test-macros" }
codex-terminal-detection = { path = "terminal-detection" }
codex-tui = { path = "tui" }
codex-tui-app-server = { path = "tui_app_server" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
@@ -232,6 +234,7 @@ portable-pty = "0.9.0"
predicates = "3"
pretty_assertions = "1.4.1"
pulldown-cmark = "0.10"
quick-xml = "0.38.4"
rand = "0.9"
ratatui = "0.29.0"
ratatui-macros = "0.6.0"
@@ -1759,6 +1759,12 @@
"call_id": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -2875,6 +2881,22 @@
],
"type": "object"
},
"ThreadShellCommandParams": {
"properties": {
"command": {
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"command",
"threadId"
],
"type": "object"
},
"ThreadSortKey": {
"enum": [
"created_at",
@@ -3580,6 +3602,30 @@
"title": "Thread/compact/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/shellCommand"
],
"title": "Thread/shellCommandRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadShellCommandParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/shellCommandRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1,6 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@@ -18,6 +25,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@@ -32,6 +42,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"
@@ -1,6 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@@ -18,6 +25,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@@ -32,6 +42,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"
@@ -745,6 +745,15 @@
],
"type": "object"
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -964,6 +973,13 @@
],
"type": "object"
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchResult": {
"description": "Superset of [`codex_file_search::FileMatch`]",
"properties": {
@@ -981,6 +997,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@@ -995,6 +1014,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"
@@ -1136,6 +1156,7 @@
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
@@ -1180,6 +1201,21 @@
],
"type": "string"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"HookRunStatus": {
"enum": [
"running",
@@ -1399,6 +1435,36 @@
],
"type": "object"
},
"McpServerStartupState": {
"enum": [
"starting",
"ready",
"failed",
"cancelled"
],
"type": "string"
},
"McpServerStatusUpdatedNotification": {
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
}
},
"required": [
"name",
"status"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -1454,6 +1520,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -1831,6 +1945,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -2177,9 +2304,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -2319,6 +2484,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -4071,6 +4244,26 @@
"title": "McpServer/oauthLogin/completedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"mcpServer/startupStatus/updated"
],
"title": "McpServer/startupStatus/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/McpServerStatusUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "McpServer/startupStatus/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -499,6 +499,30 @@
"title": "Thread/compact/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/shellCommand"
],
"title": "Thread/shellCommandRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadShellCommandParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/shellCommandRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -2034,6 +2058,13 @@
"title": "FileChangeRequestApprovalResponse",
"type": "object"
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -2093,6 +2124,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@@ -2107,6 +2141,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"
@@ -3938,6 +3973,26 @@
"title": "McpServer/oauthLogin/completedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"mcpServer/startupStatus/updated"
],
"title": "McpServer/startupStatus/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/McpServerStatusUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "McpServer/startupStatus/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -5197,11 +5252,15 @@
},
"name": {
"type": "string"
},
"needsAuth": {
"type": "boolean"
}
},
"required": [
"id",
"name"
"name",
"needsAuth"
],
"type": "object"
},
@@ -6110,6 +6169,15 @@
"title": "CommandExecutionOutputDeltaNotification",
"type": "object"
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -7882,6 +7950,7 @@
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
@@ -7926,6 +7995,21 @@
],
"type": "string"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"HookRunStatus": {
"enum": [
"running",
@@ -8506,6 +8590,15 @@
"title": "McpServerRefreshResponse",
"type": "object"
},
"McpServerStartupState": {
"enum": [
"starting",
"ready",
"failed",
"cancelled"
],
"type": "string"
},
"McpServerStatus": {
"properties": {
"authStatus": {
@@ -8542,6 +8635,29 @@
],
"type": "object"
},
"McpServerStatusUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/definitions/v2/McpServerStartupState"
}
},
"required": [
"name",
"status"
],
"title": "McpServerStatusUpdatedNotification",
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -8599,6 +8715,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/v2/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MergeStrategy": {
"enum": [
"replace",
@@ -9291,6 +9455,13 @@
"PluginListResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"featuredPluginIds": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"marketplaces": {
"items": {
"$ref": "#/definitions/v2/PluginMarketplaceEntry"
@@ -10311,6 +10482,12 @@
"call_id": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"output": {
"$ref": "#/definitions/v2/FunctionCallOutputBody"
},
@@ -10929,6 +11106,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -11837,9 +12027,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/v2/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/v2/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -11979,6 +12207,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/v2/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/v2/CommandExecutionStatus"
},
@@ -13018,6 +13254,29 @@
"title": "ThreadSetNameResponse",
"type": "object"
},
"ThreadShellCommandParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"command": {
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"command",
"threadId"
],
"title": "ThreadShellCommandParams",
"type": "object"
},
"ThreadShellCommandResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadShellCommandResponse",
"type": "object"
},
"ThreadSortKey": {
"enum": [
"created_at",
@@ -492,11 +492,15 @@
},
"name": {
"type": "string"
},
"needsAuth": {
"type": "boolean"
}
},
"required": [
"id",
"name"
"name",
"needsAuth"
],
"type": "object"
},
@@ -1026,6 +1030,30 @@
"title": "Thread/compact/startRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/shellCommand"
],
"title": "Thread/shellCommandRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadShellCommandParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/shellCommandRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -2754,6 +2782,15 @@
"title": "CommandExecutionOutputDeltaNotification",
"type": "object"
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -4327,6 +4364,13 @@
}
]
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
"directory"
],
"type": "string"
},
"FuzzyFileSearchParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -4370,6 +4414,9 @@
"null"
]
},
"match_type": {
"$ref": "#/definitions/FuzzyFileSearchMatchType"
},
"path": {
"type": "string"
},
@@ -4384,6 +4431,7 @@
},
"required": [
"file_name",
"match_type",
"path",
"root",
"score"
@@ -4626,6 +4674,7 @@
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
@@ -4670,6 +4719,21 @@
],
"type": "string"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"HookRunStatus": {
"enum": [
"running",
@@ -5294,6 +5358,15 @@
"title": "McpServerRefreshResponse",
"type": "object"
},
"McpServerStartupState": {
"enum": [
"starting",
"ready",
"failed",
"cancelled"
],
"type": "string"
},
"McpServerStatus": {
"properties": {
"authStatus": {
@@ -5330,6 +5403,29 @@
],
"type": "object"
},
"McpServerStatusUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
}
},
"required": [
"name",
"status"
],
"title": "McpServerStatusUpdatedNotification",
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -5387,6 +5483,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MergeStrategy": {
"enum": [
"replace",
@@ -6079,6 +6223,13 @@
"PluginListResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"featuredPluginIds": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"marketplaces": {
"items": {
"$ref": "#/definitions/PluginMarketplaceEntry"
@@ -7099,6 +7250,12 @@
"call_id": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -8248,6 +8405,26 @@
"title": "McpServer/oauthLogin/completedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"mcpServer/startupStatus/updated"
],
"title": "McpServer/startupStatus/updatedNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/McpServerStatusUpdatedNotification"
}
},
"required": [
"method",
"params"
],
"title": "McpServer/startupStatus/updatedNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -8689,6 +8866,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -9597,9 +9787,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -9739,6 +9967,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -10778,6 +11014,29 @@
"title": "ThreadSetNameResponse",
"type": "object"
},
"ThreadShellCommandParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"command": {
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"command",
"threadId"
],
"title": "ThreadShellCommandParams",
"type": "object"
},
"ThreadShellCommandResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadShellCommandResponse",
"type": "object"
},
"ThreadSortKey": {
"enum": [
"created_at",
@@ -4,6 +4,7 @@
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
@@ -4,6 +4,7 @@
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
@@ -177,6 +177,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -257,6 +266,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -289,6 +313,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -441,9 +513,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -583,6 +693,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -177,6 +177,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -257,6 +266,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -289,6 +313,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -441,9 +513,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -583,6 +693,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"McpServerStartupState": {
"enum": [
"starting",
"ready",
"failed",
"cancelled"
],
"type": "string"
}
},
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
}
},
"required": [
"name",
"status"
],
"title": "McpServerStatusUpdatedNotification",
"type": "object"
}
@@ -21,11 +21,15 @@
},
"name": {
"type": "string"
},
"needsAuth": {
"type": "boolean"
}
},
"required": [
"id",
"name"
"name",
"needsAuth"
],
"type": "object"
},
@@ -239,6 +239,13 @@
}
},
"properties": {
"featuredPluginIds": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"marketplaces": {
"items": {
"$ref": "#/definitions/PluginMarketplaceEntry"
@@ -25,11 +25,15 @@
},
"name": {
"type": "string"
},
"needsAuth": {
"type": "boolean"
}
},
"required": [
"id",
"name"
"name",
"needsAuth"
],
"type": "object"
},
@@ -607,6 +607,12 @@
"call_id": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -371,6 +380,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -403,6 +427,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -555,9 +627,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -697,6 +807,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -353,6 +353,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -456,6 +465,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -488,6 +512,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -778,6 +850,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -1035,9 +1120,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -1177,6 +1300,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -673,6 +673,12 @@
"call_id": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"output": {
"$ref": "#/definitions/FunctionCallOutputBody"
},
@@ -353,6 +353,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -456,6 +465,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -488,6 +512,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -778,6 +850,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -1035,9 +1120,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -1177,6 +1300,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"command": {
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
"type": "string"
},
"threadId": {
"type": "string"
}
},
"required": [
"command",
"threadId"
],
"title": "ThreadShellCommandParams",
"type": "object"
}
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ThreadShellCommandResponse",
"type": "object"
}
@@ -353,6 +353,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -456,6 +465,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -488,6 +512,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -778,6 +850,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -1035,9 +1120,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -1177,6 +1300,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -394,6 +403,21 @@
},
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -426,6 +450,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -536,6 +608,19 @@
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
"custom": {
"type": "string"
}
},
"required": [
"custom"
],
"title": "CustomSessionSource",
"type": "object"
},
{
"additionalProperties": false,
"properties": {
@@ -793,9 +878,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -935,6 +1058,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -371,6 +380,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -403,6 +427,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -555,9 +627,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -697,6 +807,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -371,6 +380,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -403,6 +427,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -555,9 +627,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -697,6 +807,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -291,6 +291,15 @@
}
]
},
"CommandExecutionSource": {
"enum": [
"agent",
"userShell",
"unifiedExecStartup",
"unifiedExecInteraction"
],
"type": "string"
},
"CommandExecutionStatus": {
"enum": [
"inProgress",
@@ -371,6 +380,21 @@
],
"type": "object"
},
"HookPromptFragment": {
"properties": {
"hookRunId": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"hookRunId",
"text"
],
"type": "object"
},
"McpToolCallError": {
"properties": {
"message": {
@@ -403,6 +427,54 @@
],
"type": "string"
},
"MemoryCitation": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/MemoryCitationEntry"
},
"type": "array"
},
"threadIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"threadIds"
],
"type": "object"
},
"MemoryCitationEntry": {
"properties": {
"lineEnd": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"lineStart": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"note": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"lineEnd",
"lineStart",
"note",
"path"
],
"type": "object"
},
"MessagePhase": {
"description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.",
"oneOf": [
@@ -555,9 +627,47 @@
},
{
"properties": {
"fragments": {
"items": {
"$ref": "#/definitions/HookPromptFragment"
},
"type": "array"
},
"id": {
"type": "string"
},
"type": {
"enum": [
"hookPrompt"
],
"title": "HookPromptThreadItemType",
"type": "string"
}
},
"required": [
"fragments",
"id",
"type"
],
"title": "HookPromptThreadItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"memoryCitation": {
"anyOf": [
{
"$ref": "#/definitions/MemoryCitation"
},
{
"type": "null"
}
],
"default": null
},
"phase": {
"anyOf": [
{
@@ -697,6 +807,14 @@
"null"
]
},
"source": {
"allOf": [
{
"$ref": "#/definitions/CommandExecutionSource"
}
],
"default": "agent"
},
"status": {
"$ref": "#/definitions/CommandExecutionStatus"
},
@@ -49,6 +49,7 @@ import type { ThreadReadParams } from "./v2/ThreadReadParams";
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
import type { ThreadStartParams } from "./v2/ThreadStartParams";
import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
@@ -60,4 +61,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
/**
* Request from the client to the server.
*/
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
@@ -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 FuzzyFileSearchMatchType = "file" | "directory";
@@ -1,8 +1,9 @@
// 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 { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
/**
* Superset of [`codex_file_search::FileMatch`]
*/
export type FuzzyFileSearchResult = { root: string, path: string, file_name: string, score: number, indices: Array<number> | null, };
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
@@ -15,4 +15,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array<Con
/**
* Set when using the Responses API.
*/
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction", encrypted_content: string, } | { "type": "other" };
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction", encrypted_content: string, } | { "type": "other" };
@@ -22,6 +22,7 @@ import type { ItemGuardianApprovalReviewCompletedNotification } from "./v2/ItemG
import type { ItemGuardianApprovalReviewStartedNotification } from "./v2/ItemGuardianApprovalReviewStartedNotification";
import type { ItemStartedNotification } from "./v2/ItemStartedNotification";
import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification";
import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification";
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
@@ -54,4 +55,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SubAgentSource } from "./SubAgentSource";
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "subagent": SubAgentSource } | "unknown";
export type SessionSource = "cli" | "vscode" | "exec" | "mcp" | { "custom": string } | { "subagent": SubAgentSource } | "unknown";
@@ -18,6 +18,7 @@ export type { FileChange } from "./FileChange";
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
export type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse";
export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
@@ -5,4 +5,4 @@
/**
* EXPERIMENTAL - app metadata summary for plugin responses.
*/
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, };
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, needsAuth: 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 CommandExecutionSource = "agent" | "userShell" | "unifiedExecStartup" | "unifiedExecInteraction";
@@ -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 = "sessionStart" | "stop";
export type HookEventName = "sessionStart" | "userPromptSubmit" | "stop";
@@ -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 HookPromptFragment = { text: string, hookRunId: 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 McpServerStartupState = "starting" | "ready" | "failed" | "cancelled";
@@ -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 { McpServerStartupState } from "./McpServerStartupState";
export type McpServerStatusUpdatedNotification = { name: string, status: McpServerStartupState, error: string | 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 { MemoryCitationEntry } from "./MemoryCitationEntry";
export type MemoryCitation = { entries: Array<MemoryCitationEntry>, threadIds: Array<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 MemoryCitationEntry = { path: string, lineStart: number, lineEnd: number, note: string, };
@@ -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 { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
export type PluginListResponse = { marketplaces: Array<PluginMarketplaceEntry>, remoteSyncError: string | null, };
export type PluginListResponse = { marketplaces: Array<PluginMarketplaceEntry>, remoteSyncError: string | null, featuredPluginIds: Array<string>, };
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SubAgentSource } from "../SubAgentSource";
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "subAgent": SubAgentSource } | "unknown";
export type SessionSource = "cli" | "vscode" | "exec" | "appServer" | { "custom": string } | { "subAgent": SubAgentSource } | "unknown";
@@ -8,18 +8,21 @@ import type { CollabAgentState } from "./CollabAgentState";
import type { CollabAgentTool } from "./CollabAgentTool";
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
import type { CommandAction } from "./CommandAction";
import type { CommandExecutionSource } from "./CommandExecutionSource";
import type { CommandExecutionStatus } from "./CommandExecutionStatus";
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
import type { FileUpdateChange } from "./FileUpdateChange";
import type { HookPromptFragment } from "./HookPromptFragment";
import type { McpToolCallError } from "./McpToolCallError";
import type { McpToolCallResult } from "./McpToolCallResult";
import type { McpToolCallStatus } from "./McpToolCallStatus";
import type { MemoryCitation } from "./MemoryCitation";
import type { PatchApplyStatus } from "./PatchApplyStatus";
import type { UserInput } from "./UserInput";
import type { WebSearchAction } from "./WebSearchAction";
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
export type ThreadItem = { "type": "userMessage", id: string, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
/**
* The command to be executed.
*/
@@ -31,7 +34,7 @@ cwd: string,
/**
* Identifier for the underlying PTY process (when available).
*/
processId: string | null, status: CommandExecutionStatus,
processId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus,
/**
* A best-effort parsing of the command to understand the action(s) it will perform.
* This returns a list of CommandAction objects because a single shell command may
@@ -0,0 +1,12 @@
// 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 ThreadShellCommandParams = { threadId: string,
/**
* Shell command string evaluated by the thread's configured shell.
* Unlike `command/exec`, this intentionally preserves shell syntax
* such as pipes, redirects, and quoting. This runs unsandboxed with full
* access rather than inheriting the thread sandbox policy.
*/
command: 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 ThreadShellCommandResponse = Record<string, never>;
@@ -55,6 +55,7 @@ export type { CommandExecutionOutputDeltaNotification } from "./CommandExecution
export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams";
export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse";
export type { CommandExecutionRequestApprovalSkillMetadata } from "./CommandExecutionRequestApprovalSkillMetadata";
export type { CommandExecutionSource } from "./CommandExecutionSource";
export type { CommandExecutionStatus } from "./CommandExecutionStatus";
export type { Config } from "./Config";
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
@@ -125,6 +126,7 @@ export type { HookExecutionMode } from "./HookExecutionMode";
export type { HookHandlerType } from "./HookHandlerType";
export type { HookOutputEntry } from "./HookOutputEntry";
export type { HookOutputEntryKind } from "./HookOutputEntryKind";
export type { HookPromptFragment } from "./HookPromptFragment";
export type { HookRunStatus } from "./HookRunStatus";
export type { HookRunSummary } from "./HookRunSummary";
export type { HookScope } from "./HookScope";
@@ -169,11 +171,15 @@ export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthL
export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams";
export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse";
export type { McpServerRefreshResponse } from "./McpServerRefreshResponse";
export type { McpServerStartupState } from "./McpServerStartupState";
export type { McpServerStatus } from "./McpServerStatus";
export type { McpServerStatusUpdatedNotification } from "./McpServerStatusUpdatedNotification";
export type { McpToolCallError } from "./McpToolCallError";
export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification";
export type { McpToolCallResult } from "./McpToolCallResult";
export type { McpToolCallStatus } from "./McpToolCallStatus";
export type { MemoryCitation } from "./MemoryCitation";
export type { MemoryCitationEntry } from "./MemoryCitationEntry";
export type { MergeStrategy } from "./MergeStrategy";
export type { Model } from "./Model";
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
@@ -281,6 +287,8 @@ export type { ThreadRollbackParams } from "./ThreadRollbackParams";
export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
export type { ThreadShellCommandParams } from "./ThreadShellCommandParams";
export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse";
export type { ThreadSortKey } from "./ThreadSortKey";
export type { ThreadSourceKind } from "./ThreadSourceKind";
export type { ThreadStartParams } from "./ThreadStartParams";
@@ -267,6 +267,10 @@ client_request_definitions! {
params: v2::ThreadCompactStartParams,
response: v2::ThreadCompactStartResponse,
},
ThreadShellCommand => "thread/shellCommand" {
params: v2::ThreadShellCommandParams,
response: v2::ThreadShellCommandResponse,
},
#[experimental("thread/backgroundTerminals/clean")]
ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" {
params: v2::ThreadBackgroundTerminalsCleanParams,
@@ -800,11 +804,20 @@ pub struct FuzzyFileSearchParams {
pub struct FuzzyFileSearchResult {
pub root: String,
pub path: String,
pub match_type: FuzzyFileSearchMatchType,
pub file_name: String,
pub score: u32,
pub indices: Option<Vec<u32>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum FuzzyFileSearchMatchType {
File,
Directory,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
pub struct FuzzyFileSearchResponse {
pub files: Vec<FuzzyFileSearchResult>,
@@ -892,6 +905,7 @@ server_notification_definitions! {
ServerRequestResolved => "serverRequest/resolved" (v2::ServerRequestResolvedNotification),
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification),
McpServerStatusUpdated => "mcpServer/startupStatus/updated" (v2::McpServerStatusUpdatedNotification),
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),
AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification),
@@ -18,6 +18,7 @@ use crate::protocol::v2::TurnError;
use crate::protocol::v2::TurnStatus;
use crate::protocol::v2::UserInput;
use crate::protocol::v2::WebSearchAction;
use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::models::MessagePhase;
use codex_protocol::protocol::AgentReasoningEvent;
use codex_protocol::protocol::AgentReasoningRawContentEvent;
@@ -118,9 +119,11 @@ impl ThreadHistoryBuilder {
pub fn handle_event(&mut self, event: &EventMsg) {
match event {
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
EventMsg::AgentMessage(payload) => {
self.handle_agent_message(payload.message.clone(), payload.phase.clone())
}
EventMsg::AgentMessage(payload) => self.handle_agent_message(
payload.message.clone(),
payload.phase.clone(),
payload.memory_citation.clone().map(Into::into),
),
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
EventMsg::AgentReasoningRawContent(payload) => {
self.handle_agent_reasoning_raw_content(payload)
@@ -182,12 +185,37 @@ impl ThreadHistoryBuilder {
match item {
RolloutItem::EventMsg(event) => self.handle_event(event),
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
RolloutItem::TurnContext(_)
| RolloutItem::SessionMeta(_)
| RolloutItem::ResponseItem(_) => {}
RolloutItem::ResponseItem(item) => self.handle_response_item(item),
RolloutItem::TurnContext(_) | RolloutItem::SessionMeta(_) => {}
}
}
fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) {
let codex_protocol::models::ResponseItem::Message {
role, content, id, ..
} = item
else {
return;
};
if role != "user" {
return;
}
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
return;
};
self.ensure_turn().items.push(ThreadItem::HookPrompt {
id: hook_prompt.id,
fragments: hook_prompt
.fragments
.into_iter()
.map(crate::protocol::v2::HookPromptFragment::from)
.collect(),
});
}
fn handle_user_message(&mut self, payload: &UserMessageEvent) {
// User messages should stay in explicitly opened turns. For backward
// compatibility with older streams that did not open turns explicitly,
@@ -208,15 +236,23 @@ impl ThreadHistoryBuilder {
self.current_turn = Some(turn);
}
fn handle_agent_message(&mut self, text: String, phase: Option<MessagePhase>) {
fn handle_agent_message(
&mut self,
text: String,
phase: Option<MessagePhase>,
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
) {
if text.is_empty() {
return;
}
let id = self.next_item_id();
self.ensure_turn()
.items
.push(ThreadItem::AgentMessage { id, text, phase });
self.ensure_turn().items.push(ThreadItem::AgentMessage {
id,
text,
phase,
memory_citation,
});
}
fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) {
@@ -271,6 +307,7 @@ impl ThreadHistoryBuilder {
);
}
codex_protocol::items::TurnItem::UserMessage(_)
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
| codex_protocol::items::TurnItem::Reasoning(_)
| codex_protocol::items::TurnItem::WebSearch(_)
@@ -291,6 +328,7 @@ impl ThreadHistoryBuilder {
);
}
codex_protocol::items::TurnItem::UserMessage(_)
| codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::AgentMessage(_)
| codex_protocol::items::TurnItem::Reasoning(_)
| codex_protocol::items::TurnItem::WebSearch(_)
@@ -331,6 +369,7 @@ impl ThreadHistoryBuilder {
command,
cwd: payload.cwd.clone(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions,
aggregated_output: None,
@@ -361,6 +400,7 @@ impl ThreadHistoryBuilder {
command,
cwd: payload.cwd.clone(),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status,
command_actions,
aggregated_output,
@@ -1134,10 +1174,13 @@ impl From<&PendingTurn> for Turn {
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::v2::CommandExecutionSource;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
use codex_protocol::items::build_hook_prompt_message;
use codex_protocol::models::MessagePhase as CoreMessagePhase;
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
use codex_protocol::parse_command::ParsedCommand;
@@ -1178,6 +1221,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Hi there".into(),
phase: None,
memory_citation: None,
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "thinking".into(),
@@ -1194,6 +1238,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Reply two".into(),
phase: None,
memory_citation: None,
}),
];
@@ -1229,6 +1274,7 @@ mod tests {
id: "item-2".into(),
text: "Hi there".into(),
phase: None,
memory_citation: None,
}
);
assert_eq!(
@@ -1260,6 +1306,7 @@ mod tests {
id: "item-5".into(),
text: "Reply two".into(),
phase: None,
memory_citation: None,
}
);
}
@@ -1318,6 +1365,7 @@ mod tests {
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
message: "Final reply".into(),
phase: Some(CoreMessagePhase::FinalAnswer),
memory_citation: None,
})];
let items = events
@@ -1332,6 +1380,7 @@ mod tests {
id: "item-1".into(),
text: "Final reply".into(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: None,
}
);
}
@@ -1354,6 +1403,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "interlude".into(),
phase: None,
memory_citation: None,
}),
EventMsg::AgentReasoning(AgentReasoningEvent {
text: "second summary".into(),
@@ -1399,6 +1449,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Working...".into(),
phase: None,
memory_citation: None,
}),
EventMsg::TurnAborted(TurnAbortedEvent {
turn_id: Some("turn-1".into()),
@@ -1413,6 +1464,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "Second attempt complete.".into(),
phase: None,
memory_citation: None,
}),
];
@@ -1442,6 +1494,7 @@ mod tests {
id: "item-2".into(),
text: "Working...".into(),
phase: None,
memory_citation: None,
}
);
@@ -1464,6 +1517,7 @@ mod tests {
id: "item-4".into(),
text: "Second attempt complete.".into(),
phase: None,
memory_citation: None,
}
);
}
@@ -1480,6 +1534,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
phase: None,
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Second".into(),
@@ -1490,6 +1545,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),
phase: None,
memory_citation: None,
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
EventMsg::UserMessage(UserMessageEvent {
@@ -1501,6 +1557,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A3".into(),
phase: None,
memory_citation: None,
}),
];
@@ -1529,6 +1586,7 @@ mod tests {
id: "item-2".into(),
text: "A1".into(),
phase: None,
memory_citation: None,
},
]
);
@@ -1546,6 +1604,7 @@ mod tests {
id: "item-4".into(),
text: "A3".into(),
phase: None,
memory_citation: None,
},
]
);
@@ -1563,6 +1622,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A1".into(),
phase: None,
memory_citation: None,
}),
EventMsg::UserMessage(UserMessageEvent {
message: "Two".into(),
@@ -1573,6 +1633,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "A2".into(),
phase: None,
memory_citation: None,
}),
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
];
@@ -1717,6 +1778,7 @@ mod tests {
command: "echo 'hello world'".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-1".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
command: "echo hello world".into(),
@@ -1865,6 +1927,7 @@ mod tests {
command: "ls".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-2".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Declined,
command_actions: vec![CommandAction::Unknown {
command: "ls".into(),
@@ -1959,6 +2022,7 @@ mod tests {
command: "echo done".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-42".into()),
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
command: "echo done".into(),
@@ -2209,6 +2273,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
phase: None,
memory_citation: None,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-b".into(),
@@ -2263,6 +2328,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "still in b".into(),
phase: None,
memory_citation: None,
}),
];
@@ -2497,6 +2563,7 @@ mod tests {
EventMsg::AgentMessage(AgentMessageEvent {
message: "done".into(),
phase: None,
memory_citation: None,
}),
EventMsg::Error(ErrorEvent {
message: "rollback failed".into(),
@@ -2608,4 +2675,80 @@ mod tests {
})
);
}
#[test]
fn rebuilds_hook_prompt_items_from_rollout_response_items() {
let hook_prompt = build_hook_prompt_message(&[
CoreHookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
CoreHookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
])
.expect("hook prompt message");
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "hello".into(),
images: None,
text_elements: Vec::new(),
local_images: Vec::new(),
})),
RolloutItem::ResponseItem(hook_prompt),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
})),
];
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].items.len(), 2);
assert_eq!(
turns[0].items[1],
ThreadItem::HookPrompt {
id: turns[0].items[1].id().to_string(),
fragments: vec![
crate::protocol::v2::HookPromptFragment {
text: "Retry with tests.".into(),
hook_run_id: "hook-run-1".into(),
},
crate::protocol::v2::HookPromptFragment {
text: "Then summarize cleanly.".into(),
hook_run_id: "hook-run-2".into(),
},
],
}
);
}
#[test]
fn ignores_plain_user_response_items_in_rollout_replay() {
let items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-a".into(),
model_context_window: None,
collaboration_mode_kind: Default::default(),
})),
RolloutItem::ResponseItem(codex_protocol::models::ResponseItem::Message {
id: Some("msg-1".into()),
role: "user".into(),
content: vec![codex_protocol::models::ContentItem::InputText {
text: "plain text".into(),
}],
end_turn: None,
phase: None,
}),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-a".into(),
last_agent_message: None,
})),
];
let turns = build_turns_from_rollout_items(&items);
assert_eq!(turns.len(), 1);
assert!(turns[0].items.is_empty());
}
}
+215 -1
View File
@@ -30,6 +30,8 @@ use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::mcp::Resource as McpResource;
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
use codex_protocol::mcp::Tool as McpTool;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission;
use codex_protocol::models::MacOsContactsPermission as CoreMacOsContactsPermission;
@@ -50,6 +52,7 @@ use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;
use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel;
@@ -90,6 +93,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_with::serde_as;
use thiserror::Error;
use ts_rs::TS;
@@ -343,7 +347,7 @@ v2_enum_from_core!(
v2_enum_from_core!(
pub enum HookEventName from CoreHookEventName {
SessionStart, Stop
SessionStart, UserPromptSubmit, Stop
}
);
@@ -1465,6 +1469,7 @@ pub enum SessionSource {
VsCode,
Exec,
AppServer,
Custom(String),
SubAgent(CoreSubAgentSource),
#[serde(other)]
Unknown,
@@ -1477,6 +1482,7 @@ impl From<CoreSessionSource> for SessionSource {
CoreSessionSource::VSCode => SessionSource::VsCode,
CoreSessionSource::Exec => SessionSource::Exec,
CoreSessionSource::Mcp => SessionSource::AppServer,
CoreSessionSource::Custom(source) => SessionSource::Custom(source),
CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub),
CoreSessionSource::Unknown => SessionSource::Unknown,
}
@@ -1490,6 +1496,7 @@ impl From<SessionSource> for CoreSessionSource {
SessionSource::VsCode => CoreSessionSource::VSCode,
SessionSource::Exec => CoreSessionSource::Exec,
SessionSource::AppServer => CoreSessionSource::Mcp,
SessionSource::Custom(source) => CoreSessionSource::Custom(source),
SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub),
SessionSource::Unknown => CoreSessionSource::Unknown,
}
@@ -2028,6 +2035,7 @@ pub struct AppSummary {
pub name: String,
pub description: Option<String>,
pub install_url: Option<String>,
pub needs_auth: bool,
}
impl From<AppInfo> for AppSummary {
@@ -2037,6 +2045,7 @@ impl From<AppInfo> for AppSummary {
name: value.name,
description: value.description,
install_url: value.install_url,
needs_auth: false,
}
}
}
@@ -2869,6 +2878,23 @@ pub struct ThreadCompactStartParams {
#[ts(export_to = "v2/")]
pub struct ThreadCompactStartResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadShellCommandParams {
pub thread_id: String,
/// Shell command string evaluated by the thread's configured shell.
/// Unlike `command/exec`, this intentionally preserves shell syntax
/// such as pipes, redirects, and quoting. This runs unsandboxed with full
/// access rather than inheriting the thread sandbox policy.
pub command: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadShellCommandResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3091,6 +3117,8 @@ pub struct PluginListParams {
pub struct PluginListResponse {
pub marketplaces: Vec<PluginMarketplaceEntry>,
pub remote_sync_error: Option<String>,
#[serde(default)]
pub featured_plugin_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -3568,6 +3596,44 @@ pub struct Turn {
pub error: Option<TurnError>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MemoryCitation {
pub entries: Vec<MemoryCitationEntry>,
pub thread_ids: Vec<String>,
}
impl From<CoreMemoryCitation> for MemoryCitation {
fn from(value: CoreMemoryCitation) -> Self {
Self {
entries: value.entries.into_iter().map(Into::into).collect(),
thread_ids: value.rollout_ids,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct MemoryCitationEntry {
pub path: String,
pub line_start: u32,
pub line_end: u32,
pub note: String,
}
impl From<CoreMemoryCitationEntry> for MemoryCitationEntry {
fn from(value: CoreMemoryCitationEntry) -> Self {
Self {
path: value.path,
line_start: value.line_start,
line_end: value.line_end,
note: value.note,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -4063,11 +4129,19 @@ pub enum ThreadItem {
UserMessage { id: String, content: Vec<UserInput> },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
HookPrompt {
id: String,
fragments: Vec<HookPromptFragment>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
AgentMessage {
id: String,
text: String,
#[serde(default)]
phase: Option<MessagePhase>,
#[serde(default)]
memory_citation: Option<MemoryCitation>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
@@ -4093,6 +4167,8 @@ pub enum ThreadItem {
cwd: PathBuf,
/// Identifier for the underlying PTY process (when available).
process_id: Option<String>,
#[serde(default)]
source: CommandExecutionSource,
status: CommandExecutionStatus,
/// A best-effort parsing of the command to understand the action(s) it will perform.
/// This returns a list of CommandAction objects because a single shell command may
@@ -4192,10 +4268,19 @@ pub enum ThreadItem {
ContextCompaction { id: String },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]
pub struct HookPromptFragment {
pub text: String,
pub hook_run_id: String,
}
impl ThreadItem {
pub fn id(&self) -> &str {
match self {
ThreadItem::UserMessage { id, .. }
| ThreadItem::HookPrompt { id, .. }
| ThreadItem::AgentMessage { id, .. }
| ThreadItem::Plan { id, .. }
| ThreadItem::Reasoning { id, .. }
@@ -4305,6 +4390,14 @@ impl From<CoreTurnItem> for ThreadItem {
id: user.id,
content: user.content.into_iter().map(UserInput::from).collect(),
},
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {
id: hook_prompt.id,
fragments: hook_prompt
.fragments
.into_iter()
.map(HookPromptFragment::from)
.collect(),
},
CoreTurnItem::AgentMessage(agent) => {
let text = agent
.content
@@ -4317,6 +4410,7 @@ impl From<CoreTurnItem> for ThreadItem {
id: agent.id,
text,
phase: agent.phase,
memory_citation: agent.memory_citation.map(Into::into),
}
}
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
@@ -4346,6 +4440,15 @@ impl From<CoreTurnItem> for ThreadItem {
}
}
impl From<codex_protocol::items::HookPromptFragment> for HookPromptFragment {
fn from(value: codex_protocol::items::HookPromptFragment) -> Self {
Self {
text: value.text,
hook_run_id: value.hook_run_id,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -4372,6 +4475,17 @@ impl From<&CoreExecCommandStatus> for CommandExecutionStatus {
}
}
v2_enum_from_core! {
#[derive(Default)]
pub enum CommandExecutionSource from CoreExecCommandSource {
#[default]
Agent,
UserShell,
UnifiedExecStartup,
UnifiedExecInteraction,
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -4818,6 +4932,7 @@ pub struct TerminalInteractionNotification {
pub stdin: String,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -4887,6 +5002,25 @@ pub struct McpServerOauthLoginCompletedNotification {
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum McpServerStartupState {
Starting,
Ready,
Failed,
Cancelled,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpServerStatusUpdatedNotification {
pub name: String,
pub status: McpServerStartupState,
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -6268,6 +6402,40 @@ mod tests {
assert_eq!(decoded, params);
}
#[test]
fn thread_shell_command_params_round_trip() {
let params = ThreadShellCommandParams {
thread_id: "thr_123".to_string(),
command: "printf 'hello world\\n'".to_string(),
};
let value = serde_json::to_value(&params).expect("serialize thread/shellCommand params");
assert_eq!(
value,
json!({
"threadId": "thr_123",
"command": "printf 'hello world\\n'",
})
);
let decoded = serde_json::from_value::<ThreadShellCommandParams>(value)
.expect("deserialize thread/shellCommand params");
assert_eq!(decoded, params);
}
#[test]
fn thread_shell_command_response_round_trip() {
let response = ThreadShellCommandResponse {};
let value =
serde_json::to_value(&response).expect("serialize thread/shellCommand response");
assert_eq!(value, json!({}));
let decoded = serde_json::from_value::<ThreadShellCommandResponse>(value)
.expect("deserialize thread/shellCommand response");
assert_eq!(decoded, response);
}
#[test]
fn command_exec_params_default_optional_streaming_flags() {
let params = serde_json::from_value::<CommandExecParams>(json!({
@@ -6562,6 +6730,32 @@ mod tests {
assert_eq!(decoded, notification);
}
#[test]
fn command_execution_output_delta_round_trips() {
let notification = CommandExecutionOutputDeltaNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
delta: "\u{fffd}a\n".to_string(),
};
let value = serde_json::to_value(&notification)
.expect("serialize item/commandExecution/outputDelta notification");
assert_eq!(
value,
json!({
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "item-1",
"delta": "\u{fffd}a\n",
})
);
let decoded = serde_json::from_value::<CommandExecutionOutputDeltaNotification>(value)
.expect("deserialize round-trip");
assert_eq!(decoded, notification);
}
#[test]
fn sandbox_policy_round_trips_external_sandbox_network_access() {
let v2_policy = SandboxPolicy::ExternalSandbox {
@@ -7393,6 +7587,7 @@ mod tests {
},
],
phase: None,
memory_citation: None,
});
assert_eq!(
@@ -7401,6 +7596,7 @@ mod tests {
id: "agent-1".to_string(),
text: "Hello world".to_string(),
phase: None,
memory_citation: None,
}
);
@@ -7410,6 +7606,15 @@ mod tests {
text: "final".to_string(),
}],
phase: Some(MessagePhase::FinalAnswer),
memory_citation: Some(CoreMemoryCitation {
entries: vec![CoreMemoryCitationEntry {
path: "MEMORY.md".to_string(),
line_start: 1,
line_end: 2,
note: "summary".to_string(),
}],
rollout_ids: vec!["rollout-1".to_string()],
}),
});
assert_eq!(
@@ -7418,6 +7623,15 @@ mod tests {
id: "agent-2".to_string(),
text: "final".to_string(),
phase: Some(MessagePhase::FinalAnswer),
memory_citation: Some(MemoryCitation {
entries: vec![MemoryCitationEntry {
path: "MEMORY.md".to_string(),
line_start: 1,
line_end: 2,
note: "summary".to_string(),
}],
thread_ids: vec!["rollout-1".to_string()],
}),
}
);
+1 -1
View File
@@ -32,7 +32,7 @@ axum = { workspace = true, default-features = false, features = [
codex-arg0 = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-environment = { workspace = true }
codex-exec-server = { workspace = true }
codex-otel = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-cli = { workspace = true }
+47 -17
View File
@@ -115,10 +115,7 @@ Example with notification opt-out:
},
"capabilities": {
"experimentalApi": true,
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
]
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
}
}
}
@@ -139,6 +136,7 @@ Example with notification opt-out:
- `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
@@ -165,12 +163,12 @@ Example with notification opt-out:
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names (**under development; do not call from production clients yet**).
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
- `skills/changed` — notification emitted when watched local skill files change.
- `app/list` — list available apps.
- `skills/config/write` — write user-level skill config by path.
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
- `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**).
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
@@ -228,7 +226,11 @@ Start a fresh thread when you need a new Codex conversation.
Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `"none"` is selected, the personality placeholder is replaced with an empty string.
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`:
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`.
By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead.
Example:
```json
{ "method": "thread/resume", "id": 11, "params": {
@@ -301,10 +303,13 @@ When `nextCursor` is `null`, you’ve reached the final page.
- `thread/start`, `thread/fork`, and detached review threads do not emit a separate initial `thread/status/changed`; their `thread/started` notification already carries the current `thread.status`.
```json
{ "method": "thread/status/changed", "params": {
{
"method": "thread/status/changed",
"params": {
"threadId": "thr_123",
"status": { "type": "active", "activeFlags": [] }
} }
}
}
```
### Example: Unsubscribe from a loaded thread
@@ -411,6 +416,31 @@ While compaction is running, the thread is effectively in a turn so clients shou
{ "id": 25, "result": {} }
```
### Example: Run a thread shell command
Use `thread/shellCommand` for the TUI `!` workflow. The request returns immediately with `{}`.
This API runs unsandboxed with full access; it does not inherit the thread
sandbox policy.
If the thread already has an active turn, the command runs as an auxiliary action on that turn. In that case, progress is emitted as standard `item/*` notifications on the existing turn and the formatted output is injected into the turn’s message stream:
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
- zero or more `item/commandExecution/outputDelta`
- `item/completed` with the same `commandExecution` item id
If the thread does not already have an active turn, the server starts a standalone turn for the shell command. In that case clients should expect:
- `turn/started`
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
- zero or more `item/commandExecution/outputDelta`
- `item/completed` with the same `commandExecution` item id
- `turn/completed`
```json
{ "method": "thread/shellCommand", "id": 26, "params": { "threadId": "thr_b", "command": "git status --short" } }
{ "id": 26, "result": {} }
```
### Example: Start a turn (send user input)
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
@@ -806,6 +836,10 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
- `windowsSandbox/setupCompleted` — `{ mode, success, error }` after a `windowsSandbox/setupStart` request finishes.
### MCP server startup events
- `mcpServer/startupStatus/updated` — `{ name, status, error }` when app-server observes an MCP server startup transition. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` is `null` except for `failed`.
### Turn events
The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
@@ -953,10 +987,7 @@ The built-in `request_permissions` tool sends an `item/permissions/requestApprov
"reason": "Select a workspace root",
"permissions": {
"fileSystem": {
"write": [
"/Users/me/project",
"/Users/me/shared"
]
"write": ["/Users/me/project", "/Users/me/shared"]
}
}
}
@@ -972,9 +1003,7 @@ The client responds with `result.permissions`, which should be the granted subse
"scope": "session",
"permissions": {
"fileSystem": {
"write": [
"/Users/me/project"
]
"write": ["/Users/me/project"]
}
}
}
@@ -1233,6 +1262,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes for a loaded thread; payload includes `{ name, status, error }` where `status` is `starting`, `ready`, `failed`, or `cancelled`.
### 1) Check auth state
@@ -107,6 +107,7 @@ fn app_server_request_span_template(
app_server.api_version = "v2",
app_server.client_name = field::Empty,
app_server.client_version = field::Empty,
turn.id = field::Empty,
)
}
@@ -27,6 +27,7 @@ use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::CommandExecutionRequestApprovalSkillMetadata;
use codex_app_server_protocol::CommandExecutionSource;
use codex_app_server_protocol::CommandExecutionStatus;
use codex_app_server_protocol::ContextCompactedNotification;
use codex_app_server_protocol::DeprecationNoticeNotification;
@@ -56,6 +57,8 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_app_server_protocol::McpServerElicitationRequestResponse;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::McpToolCallError;
use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::McpToolCallStatus;
@@ -110,6 +113,7 @@ use codex_core::sandboxing::intersect_permission_profiles;
use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
@@ -308,6 +312,34 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::McpStartupUpdate(update) => {
if let ApiVersion::V2 = api_version {
let (status, error) = match update.status {
codex_protocol::protocol::McpStartupStatus::Starting => {
(McpServerStartupState::Starting, None)
}
codex_protocol::protocol::McpStartupStatus::Ready => {
(McpServerStartupState::Ready, None)
}
codex_protocol::protocol::McpStartupStatus::Failed { error } => {
(McpServerStartupState::Failed, Some(error))
}
codex_protocol::protocol::McpStartupStatus::Cancelled => {
(McpServerStartupState::Cancelled, None)
}
};
let notification = McpServerStatusUpdatedNotification {
name: update.server,
status,
error,
};
outgoing
.send_server_notification(ServerNotification::McpServerStatusUpdated(
notification,
))
.await;
}
}
EventMsg::Warning(_warning_event) => {}
EventMsg::GuardianAssessment(assessment) => {
if let ApiVersion::V2 = api_version {
@@ -1483,6 +1515,14 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::RawResponseItem(raw_response_item_event) => {
maybe_emit_hook_prompt_item_completed(
api_version,
conversation_id,
&event_turn_id,
&raw_response_item_event.item,
&outgoing,
)
.await;
maybe_emit_raw_response_item_completed(
api_version,
conversation_id,
@@ -1563,6 +1603,7 @@ pub(crate) async fn apply_bespoke_event_handling(
command,
cwd,
process_id,
source: exec_command_begin_event.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions,
aggregated_output: None,
@@ -1580,7 +1621,6 @@ pub(crate) async fn apply_bespoke_event_handling(
}
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
let item_id = exec_command_output_delta_event.call_id.clone();
let delta = String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
// The underlying EventMsg::ExecCommandOutputDelta is used for shell, unified_exec,
// and apply_patch tool calls. We represent apply_patch with the FileChange item, and
// everything else with the CommandExecution item.
@@ -1592,6 +1632,8 @@ pub(crate) async fn apply_bespoke_event_handling(
state.turn_summary.file_change_started.contains(&item_id)
};
if is_file_change {
let delta =
String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
let notification = FileChangeOutputDeltaNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
@@ -1608,7 +1650,8 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item_id,
delta,
delta: String::from_utf8_lossy(&exec_command_output_delta_event.chunk)
.to_string(),
};
outgoing
.send_server_notification(ServerNotification::CommandExecutionOutputDelta(
@@ -1641,6 +1684,7 @@ pub(crate) async fn apply_bespoke_event_handling(
aggregated_output,
exit_code,
duration,
source,
status,
..
} = exec_command_end_event;
@@ -1672,6 +1716,7 @@ pub(crate) async fn apply_bespoke_event_handling(
command: shlex_join(&command),
cwd,
process_id,
source: source.into(),
status,
command_actions,
aggregated_output,
@@ -1935,6 +1980,7 @@ async fn complete_command_execution_item(
command: String,
cwd: PathBuf,
process_id: Option<String>,
source: CommandExecutionSource,
command_actions: Vec<V2ParsedCommand>,
status: CommandExecutionStatus,
outgoing: &ThreadScopedOutgoingMessageSender,
@@ -1944,6 +1990,7 @@ async fn complete_command_execution_item(
command,
cwd,
process_id,
source,
status,
command_actions,
aggregated_output: None,
@@ -1981,6 +2028,49 @@ async fn maybe_emit_raw_response_item_completed(
.await;
}
async fn maybe_emit_hook_prompt_item_completed(
api_version: ApiVersion,
conversation_id: ThreadId,
turn_id: &str,
item: &codex_protocol::models::ResponseItem,
outgoing: &ThreadScopedOutgoingMessageSender,
) {
let ApiVersion::V2 = api_version else {
return;
};
let codex_protocol::models::ResponseItem::Message {
role, content, id, ..
} = item
else {
return;
};
if role != "user" {
return;
}
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
return;
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
turn_id: turn_id.to_string(),
item: ThreadItem::HookPrompt {
id: hook_prompt.id,
fragments: hook_prompt
.fragments
.into_iter()
.map(codex_app_server_protocol::HookPromptFragment::from)
.collect(),
},
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))
.await;
}
async fn find_and_remove_turn_summary(
_conversation_id: ThreadId,
thread_state: &Arc<Mutex<ThreadState>>,
@@ -2607,6 +2697,7 @@ async fn on_command_execution_request_approval_response(
completion_item.command,
completion_item.cwd,
/*process_id*/ None,
CommandExecutionSource::Agent,
completion_item.command_actions,
status,
&outgoing,
@@ -2751,6 +2842,8 @@ mod tests {
use codex_app_server_protocol::GuardianApprovalReviewStatus;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::TurnPlanStepStatus;
use codex_protocol::items::HookPromptFragment;
use codex_protocol::items::build_hook_prompt_message;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
@@ -3785,4 +3878,59 @@ mod tests {
assert!(rx.try_recv().is_err(), "no messages expected");
Ok(())
}
#[tokio::test]
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()> {
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
let conversation_id = ThreadId::new();
let outgoing = ThreadScopedOutgoingMessageSender::new(
outgoing,
vec![ConnectionId(1)],
conversation_id,
);
let item = build_hook_prompt_message(&[
HookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
])
.expect("hook prompt message");
maybe_emit_hook_prompt_item_completed(
ApiVersion::V2,
conversation_id,
"turn-1",
&item,
&outgoing,
)
.await;
let msg = recv_broadcast_message(&mut rx).await?;
match msg {
OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(
notification,
)) => {
assert_eq!(notification.thread_id, conversation_id.to_string());
assert_eq!(notification.turn_id, "turn-1");
assert_eq!(
notification.item,
ThreadItem::HookPrompt {
id: notification.item.id().to_string(),
fragments: vec![
codex_app_server_protocol::HookPromptFragment {
text: "Retry with tests.".into(),
hook_run_id: "hook-run-1".into(),
},
codex_app_server_protocol::HookPromptFragment {
text: "Then summarize cleanly.".into(),
hook_run_id: "hook-run-2".into(),
},
],
}
);
}
other => bail!("unexpected message: {other:?}"),
}
assert!(rx.try_recv().is_err(), "no extra messages expected");
Ok(())
}
}
@@ -146,6 +146,8 @@ use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadRollbackParams;
use codex_app_server_protocol::ThreadSetNameParams;
use codex_app_server_protocol::ThreadSetNameResponse;
use codex_app_server_protocol::ThreadShellCommandParams;
use codex_app_server_protocol::ThreadShellCommandResponse;
use codex_app_server_protocol::ThreadSortKey;
use codex_app_server_protocol::ThreadSourceKind;
use codex_app_server_protocol::ThreadStartParams;
@@ -189,7 +191,6 @@ use codex_core::ThreadSortKey as CoreThreadSortKey;
use codex_core::auth::AuthMode as CoreAuthMode;
use codex_core::auth::CLIENT_ID;
use codex_core::auth::login_with_api_key;
use codex_core::auth::login_with_chatgpt_auth_tokens;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
@@ -202,6 +203,7 @@ use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::error::CodexErr;
use codex_core::error::Result as CodexResult;
use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecExpiration;
use codex_core::exec::ExecParams;
use codex_core::exec_env::create_env;
@@ -221,11 +223,13 @@ use codex_core::models_manager::collaboration_mode_presets::CollaborationModesCo
use codex_core::parse_cursor;
use codex_core::plugins::MarketplaceError;
use codex_core::plugins::MarketplacePluginSource;
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
use codex_core::plugins::PluginInstallRequest;
use codex_core::plugins::PluginReadRequest;
use codex_core::plugins::PluginUninstallError as CorePluginUninstallError;
use codex_core::plugins::load_plugin_apps;
use codex_core::plugins::load_plugin_mcp_servers;
use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
@@ -239,6 +243,7 @@ use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
use codex_feedback::CodexFeedback;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::auth::login_with_chatgpt_auth_tokens;
use codex_login::run_login_server;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode;
@@ -271,6 +276,7 @@ use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use codex_protocol::user_input::UserInput as CoreInputItem;
use codex_rmcp_client::perform_oauth_login_return_url;
use codex_state::StateRuntime;
use codex_state::ThreadMetadata;
use codex_state::ThreadMetadataBuilder;
use codex_state::log_db::LogDbLayer;
use codex_utils_json_to_toml::json_to_toml;
@@ -307,6 +313,7 @@ use codex_app_server_protocol::ServerRequest;
mod apps_list_helpers;
mod plugin_app_helpers;
mod plugin_mcp_oauth;
use crate::filters::compute_source_filters;
use crate::filters::source_kind_matches;
@@ -423,7 +430,10 @@ impl CodexMessageProcessor {
Ok(config) => self
.thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config),
.maybe_start_curated_repo_sync_for_config(
&config,
self.thread_manager.auth_manager(),
),
Err(err) => warn!("failed to load latest config for curated plugin sync: {err:?}"),
}
}
@@ -690,6 +700,10 @@ impl CodexMessageProcessor {
self.thread_read(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadShellCommand { request_id, params } => {
self.thread_shell_command(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::SkillsList { request_id, params } => {
self.skills_list(to_connection_request_id(request_id), params)
.await;
@@ -1400,7 +1414,7 @@ impl CodexMessageProcessor {
let account = match self.auth_manager.auth_cached() {
Some(auth) => match auth.auth_mode() {
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
CoreAuthMode::Chatgpt => {
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
let email = auth.get_account_email();
let plan_type = auth.account_plan_type();
@@ -1661,11 +1675,17 @@ impl CodexMessageProcessor {
None => ExecExpiration::DefaultTimeout,
}
};
let capture_policy = if disable_output_cap {
ExecCapturePolicy::FullBuffer
} else {
ExecCapturePolicy::ShellTool
};
let sandbox_cwd = self.config.cwd.clone();
let exec_params = ExecParams {
command,
cwd: cwd.clone(),
expiration,
capture_policy,
env,
network: started_network_proxy
.as_ref()
@@ -2969,6 +2989,58 @@ impl CodexMessageProcessor {
}
}
async fn thread_shell_command(
&self,
request_id: ConnectionRequestId,
params: ThreadShellCommandParams,
) {
let ThreadShellCommandParams { thread_id, command } = params;
let command = command.trim().to_string();
if command.is_empty() {
self.outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: "command must not be empty".to_string(),
data: None,
},
)
.await;
return;
}
let (_, thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
match self
.submit_core_op(
&request_id,
thread.as_ref(),
Op::RunUserShellCommand { command },
)
.await
{
Ok(_) => {
self.outgoing
.send_response(request_id, ThreadShellCommandResponse {})
.await;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to start shell command: {err}"),
)
.await;
}
}
}
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
let ThreadListParams {
cursor,
@@ -3346,7 +3418,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
config: request_overrides,
config: mut request_overrides,
base_instructions,
developer_instructions,
personality,
@@ -3372,7 +3444,7 @@ impl CodexMessageProcessor {
};
let history_cwd = thread_history.session_cwd();
let typesafe_overrides = self.build_thread_config_overrides(
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
service_tier,
@@ -3384,6 +3456,13 @@ impl CodexMessageProcessor {
developer_instructions,
personality,
);
let persisted_resume_metadata = self
.load_and_apply_persisted_resume_metadata(
&thread_history,
&mut request_overrides,
&mut typesafe_overrides,
)
.await;
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
let cloud_requirements = self.current_cloud_requirements();
@@ -3447,18 +3526,22 @@ impl CodexMessageProcessor {
"thread",
);
let Some(mut thread) = self
let mut thread = match self
.load_thread_from_resume_source_or_send_internal(
request_id.clone(),
thread_id,
thread.as_ref(),
&response_history,
rollout_path.as_path(),
fallback_model_provider.as_str(),
persisted_resume_metadata.as_ref(),
)
.await
else {
return;
{
Ok(thread) => thread,
Err(message) => {
self.send_internal_error(request_id, message).await;
return;
}
};
self.thread_watch_manager
@@ -3501,6 +3584,25 @@ impl CodexMessageProcessor {
}
}
async fn load_and_apply_persisted_resume_metadata(
&self,
thread_history: &InitialHistory,
request_overrides: &mut Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: &mut ConfigOverrides,
) -> Option<ThreadMetadata> {
let InitialHistory::Resumed(resumed_history) = thread_history else {
return None;
};
let state_db_ctx = get_state_db(&self.config).await?;
let persisted_metadata = state_db_ctx
.get_thread(resumed_history.conversation_id)
.await
.ok()
.flatten()?;
merge_persisted_resume_metadata(request_overrides, typesafe_overrides, &persisted_metadata);
Some(persisted_metadata)
}
async fn resume_running_thread(
&mut self,
request_id: ConnectionRequestId,
@@ -3617,6 +3719,7 @@ impl CodexMessageProcessor {
existing_thread_id,
rollout_path.as_path(),
config_snapshot.model_provider_id.as_str(),
/*persisted_metadata*/ None,
)
.await
{
@@ -3748,13 +3851,13 @@ impl CodexMessageProcessor {
async fn load_thread_from_resume_source_or_send_internal(
&self,
request_id: ConnectionRequestId,
thread_id: ThreadId,
thread: &CodexThread,
thread_history: &InitialHistory,
rollout_path: &Path,
fallback_provider: &str,
) -> Option<Thread> {
persisted_resume_metadata: Option<&ThreadMetadata>,
) -> std::result::Result<Thread, String> {
let thread = match thread_history {
InitialHistory::Resumed(resumed) => {
load_thread_summary_for_rollout(
@@ -3762,6 +3865,7 @@ impl CodexMessageProcessor {
resumed.conversation_id,
resumed.rollout_path.as_path(),
fallback_provider,
persisted_resume_metadata,
)
.await
}
@@ -3779,28 +3883,18 @@ impl CodexMessageProcessor {
"failed to build resume response for thread {thread_id}: initial history missing"
)),
};
let mut thread = match thread {
Ok(thread) => thread,
Err(message) => {
self.send_internal_error(request_id, message).await;
return None;
}
};
let mut thread = thread?;
thread.id = thread_id.to_string();
thread.path = Some(rollout_path.to_path_buf());
let history_items = thread_history.get_rollout_items();
if let Err(message) = populate_thread_turns(
populate_thread_turns(
&mut thread,
ThreadTurnSource::HistoryItems(&history_items),
/*active_turn*/ None,
)
.await
{
self.send_internal_error(request_id, message).await;
return None;
}
.await?;
self.attach_thread_name(thread_id, &mut thread).await;
Some(thread)
Ok(thread)
}
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
@@ -4502,20 +4596,28 @@ impl CodexMessageProcessor {
}
};
let configured_servers = self
.thread_manager
.mcp_manager()
.configured_servers(&config);
if let Err(error) = self.queue_mcp_server_refresh_for_config(&config).await {
self.outgoing.send_error(request_id, error).await;
return;
}
let response = McpServerRefreshResponse {};
self.outgoing.send_response(request_id, response).await;
}
async fn queue_mcp_server_refresh_for_config(
&self,
config: &Config,
) -> Result<(), JSONRPCErrorError> {
let configured_servers = self.thread_manager.mcp_manager().configured_servers(config);
let mcp_servers = match serde_json::to_value(configured_servers) {
Ok(value) => value,
Err(err) => {
let error = JSONRPCErrorError {
return Err(JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to serialize MCP servers: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
});
}
};
@@ -4523,15 +4625,13 @@ impl CodexMessageProcessor {
match serde_json::to_value(config.mcp_oauth_credentials_store_mode) {
Ok(value) => value,
Err(err) => {
let error = JSONRPCErrorError {
return Err(JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!(
"failed to serialize MCP OAuth credentials store mode: {err}"
),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
});
}
};
@@ -4544,8 +4644,7 @@ impl CodexMessageProcessor {
// active turn to avoid work for threads that never resume.
let thread_manager = Arc::clone(&self.thread_manager);
thread_manager.refresh_mcp_servers(refresh_config).await;
let response = McpServerRefreshResponse {};
self.outgoing.send_response(request_id, response).await;
Ok(())
}
async fn mcp_server_oauth_login(
@@ -4819,6 +4918,7 @@ impl CodexMessageProcessor {
MarketplaceError::InvalidMarketplaceFile { .. }
| MarketplaceError::PluginNotFound { .. }
| MarketplaceError::PluginNotAvailable { .. }
| MarketplaceError::PluginsDisabled
| MarketplaceError::InvalidPlugin(_) => {
self.send_invalid_request_error(request_id, err.to_string())
.await;
@@ -5340,6 +5440,13 @@ impl CodexMessageProcessor {
.extend(valid_extra_roots);
}
let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
let skills_manager = self.thread_manager.skills_manager();
let mut data = Vec::new();
for cwd in cwds {
@@ -5347,7 +5454,7 @@ impl CodexMessageProcessor {
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(&cwd, force_reload, extra_roots)
.skills_for_cwd_with_extra_user_roots(&cwd, &config, force_reload, extra_roots)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
@@ -5378,9 +5485,9 @@ impl CodexMessageProcessor {
}
};
let mut remote_sync_error = None;
let auth = self.auth_manager.auth().await;
if force_remote_sync {
let auth = self.auth_manager.auth().await;
match plugins_manager
.sync_plugins_from_remote(&config, auth.as_ref())
.await
@@ -5412,8 +5519,11 @@ impl CodexMessageProcessor {
};
}
let config_for_marketplace_listing = config.clone();
let plugins_manager_for_marketplace_listing = plugins_manager.clone();
let data = match tokio::task::spawn_blocking(move || {
let marketplaces = plugins_manager.list_marketplaces_for_config(&config, &roots)?;
let marketplaces = plugins_manager_for_marketplace_listing
.list_marketplaces_for_config(&config_for_marketplace_listing, &roots)?;
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
marketplaces
.into_iter()
@@ -5459,12 +5569,34 @@ impl CodexMessageProcessor {
}
};
let featured_plugin_ids = if data
.iter()
.any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
{
match plugins_manager
.featured_plugin_ids_for_config(&config, auth.as_ref())
.await
{
Ok(featured_plugin_ids) => featured_plugin_ids,
Err(err) => {
warn!(
error = %err,
"plugin/list featured plugin fetch failed; returning empty featured ids"
);
Vec::new()
}
}
} else {
Vec::new()
};
self.outgoing
.send_response(
request_id,
PluginListResponse {
marketplaces: data,
remote_sync_error,
featured_plugin_ids,
},
)
.await;
@@ -5513,6 +5645,17 @@ impl CodexMessageProcessor {
};
let app_summaries =
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
let visible_skills = outcome
.plugin
.skills
.iter()
.filter(|skill| {
skill.matches_product_restriction_for_product(
self.thread_manager.session_source().restriction_product(),
)
})
.cloned()
.collect::<Vec<_>>();
let plugin = PluginDetail {
marketplace_name: outcome.marketplace_name,
marketplace_path: outcome.marketplace_path,
@@ -5527,7 +5670,7 @@ impl CodexMessageProcessor {
interface: outcome.plugin.interface.map(plugin_interface_to_info),
},
description: outcome.plugin.description,
skills: plugin_skills_to_info(&outcome.plugin.skills),
skills: plugin_skills_to_info(&visible_skills),
apps: app_summaries,
mcp_servers: outcome.plugin.mcp_server_names,
};
@@ -5613,6 +5756,22 @@ impl CodexMessageProcessor {
self.config.as_ref().clone()
}
};
self.clear_plugin_related_caches();
let plugin_mcp_servers = load_plugin_mcp_servers(result.installed_path.as_path());
if !plugin_mcp_servers.is_empty() {
if let Err(err) = self.queue_mcp_server_refresh_for_config(&config).await {
warn!(
plugin = result.plugin_id.as_key(),
"failed to queue MCP refresh after plugin install: {err:?}"
);
}
self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers)
.await;
}
let plugin_apps = load_plugin_apps(result.installed_path.as_path());
let apps_needing_auth = if plugin_apps.is_empty()
|| !config.features.apps_enabled(Some(&self.auth_manager)).await
@@ -5673,7 +5832,6 @@ impl CodexMessageProcessor {
)
};
self.clear_plugin_related_caches();
self.outgoing
.send_response(
request_id,
@@ -5895,6 +6053,9 @@ impl CodexMessageProcessor {
match turn_id {
Ok(turn_id) => {
self.outgoing
.record_request_turn_id(&request_id, &turn_id)
.await;
let turn = Turn {
id: turn_id.clone(),
items: vec![],
@@ -5947,6 +6108,9 @@ impl CodexMessageProcessor {
.await;
return;
}
self.outgoing
.record_request_turn_id(&request_id, &params.expected_turn_id)
.await;
if let Err(error) = Self::validate_v2_input_limit(&params.input) {
self.outgoing.send_error(request_id, error).await;
return;
@@ -6427,7 +6591,10 @@ impl CodexMessageProcessor {
request_id: ConnectionRequestId,
params: TurnInterruptParams,
) {
let TurnInterruptParams { thread_id, .. } = params;
let TurnInterruptParams { thread_id, turn_id } = params;
self.outgoing
.record_request_turn_id(&request_id, &turn_id)
.await;
let (thread_uuid, thread) = match self.load_thread(&thread_id).await {
Ok(v) => v,
@@ -7391,6 +7558,36 @@ fn collect_resume_override_mismatches(
mismatch_details
}
fn merge_persisted_resume_metadata(
request_overrides: &mut Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: &mut ConfigOverrides,
persisted_metadata: &ThreadMetadata,
) {
if has_model_resume_override(request_overrides.as_ref(), typesafe_overrides) {
return;
}
typesafe_overrides.model = persisted_metadata.model.clone();
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort {
request_overrides.get_or_insert_with(HashMap::new).insert(
"model_reasoning_effort".to_string(),
serde_json::Value::String(reasoning_effort.to_string()),
);
}
}
fn has_model_resume_override(
request_overrides: Option<&HashMap<String, serde_json::Value>>,
typesafe_overrides: &ConfigOverrides,
) -> bool {
typesafe_overrides.model.is_some()
|| typesafe_overrides.model_provider.is_some()
|| request_overrides.is_some_and(|overrides| overrides.contains_key("model"))
|| request_overrides
.is_some_and(|overrides| overrides.contains_key("model_reasoning_effort"))
}
fn skills_to_info(
skills: &[codex_core::skills::SkillMetadata],
disabled_paths: &std::collections::HashSet<PathBuf>,
@@ -7705,26 +7902,7 @@ async fn read_summary_from_state_db_context_by_thread_id(
Ok(Some(metadata)) => metadata,
Ok(None) | Err(_) => return None,
};
Some(summary_from_state_db_metadata(
metadata.id,
metadata.rollout_path,
metadata.first_user_message,
metadata
.created_at
.to_rfc3339_opts(SecondsFormat::Secs, true),
metadata
.updated_at
.to_rfc3339_opts(SecondsFormat::Secs, true),
metadata.model_provider,
metadata.cwd,
metadata.cli_version,
metadata.source,
metadata.agent_nickname,
metadata.agent_role,
metadata.git_sha,
metadata.git_branch,
metadata.git_origin_url,
))
Some(summary_from_thread_metadata(&metadata))
}
async fn summary_from_thread_list_item(
@@ -7835,6 +8013,29 @@ fn summary_from_state_db_metadata(
}
}
fn summary_from_thread_metadata(metadata: &ThreadMetadata) -> ConversationSummary {
summary_from_state_db_metadata(
metadata.id,
metadata.rollout_path.clone(),
metadata.first_user_message.clone(),
metadata
.created_at
.to_rfc3339_opts(SecondsFormat::Secs, true),
metadata
.updated_at
.to_rfc3339_opts(SecondsFormat::Secs, true),
metadata.model_provider.clone(),
metadata.cwd.clone(),
metadata.cli_version.clone(),
metadata.source.clone(),
metadata.agent_nickname.clone(),
metadata.agent_role.clone(),
metadata.git_sha.clone(),
metadata.git_branch.clone(),
metadata.git_origin_url.clone(),
)
}
pub(crate) async fn read_summary_from_rollout(
path: &Path,
fallback_provider: &str,
@@ -7982,6 +8183,7 @@ async fn load_thread_summary_for_rollout(
thread_id: ThreadId,
rollout_path: &Path,
fallback_provider: &str,
persisted_metadata: Option<&ThreadMetadata>,
) -> std::result::Result<Thread, String> {
let mut thread = read_summary_from_rollout(rollout_path, fallback_provider)
.await
@@ -7992,7 +8194,12 @@ async fn load_thread_summary_for_rollout(
rollout_path.display()
)
})?;
if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await {
if let Some(persisted_metadata) = persisted_metadata {
merge_mutable_thread_metadata(
&mut thread,
summary_to_thread(summary_from_thread_metadata(persisted_metadata)),
);
} else if let Some(summary) = read_summary_from_state_db_by_thread_id(config, thread_id).await {
merge_mutable_thread_metadata(&mut thread, summary_to_thread(summary));
}
Ok(thread)
@@ -8144,6 +8351,7 @@ mod tests {
use anyhow::Result;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use pretty_assertions::assert_eq;
@@ -8275,6 +8483,178 @@ mod tests {
);
}
fn test_thread_metadata(
model: Option<&str>,
reasoning_effort: Option<ReasoningEffort>,
) -> Result<ThreadMetadata> {
let thread_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?;
let mut builder = ThreadMetadataBuilder::new(
thread_id,
PathBuf::from("/tmp/rollout.jsonl"),
Utc::now(),
codex_protocol::protocol::SessionSource::default(),
);
builder.model_provider = Some("mock_provider".to_string());
let mut metadata = builder.build("mock_provider");
metadata.model = model.map(ToString::to_string);
metadata.reasoning_effort = reasoning_effort;
Ok(metadata)
}
#[test]
fn merge_persisted_resume_metadata_prefers_persisted_model_and_reasoning_effort() -> Result<()>
{
let mut request_overrides = None;
let mut typesafe_overrides = ConfigOverrides::default();
let persisted_metadata =
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(
typesafe_overrides.model,
Some("gpt-5.1-codex-max".to_string())
);
assert_eq!(
request_overrides,
Some(HashMap::from([(
"model_reasoning_effort".to_string(),
serde_json::Value::String("high".to_string()),
)]))
);
Ok(())
}
#[test]
fn merge_persisted_resume_metadata_preserves_explicit_overrides() -> Result<()> {
let mut request_overrides = Some(HashMap::from([(
"model_reasoning_effort".to_string(),
serde_json::Value::String("low".to_string()),
)]));
let mut typesafe_overrides = ConfigOverrides {
model: Some("gpt-5.2-codex".to_string()),
..Default::default()
};
let persisted_metadata =
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(typesafe_overrides.model, Some("gpt-5.2-codex".to_string()));
assert_eq!(
request_overrides,
Some(HashMap::from([(
"model_reasoning_effort".to_string(),
serde_json::Value::String("low".to_string()),
)]))
);
Ok(())
}
#[test]
fn merge_persisted_resume_metadata_skips_persisted_values_when_model_overridden() -> Result<()>
{
let mut request_overrides = Some(HashMap::from([(
"model".to_string(),
serde_json::Value::String("gpt-5.2-codex".to_string()),
)]));
let mut typesafe_overrides = ConfigOverrides::default();
let persisted_metadata =
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(typesafe_overrides.model, None);
assert_eq!(
request_overrides,
Some(HashMap::from([(
"model".to_string(),
serde_json::Value::String("gpt-5.2-codex".to_string()),
)]))
);
Ok(())
}
#[test]
fn merge_persisted_resume_metadata_skips_persisted_values_when_provider_overridden()
-> Result<()> {
let mut request_overrides = None;
let mut typesafe_overrides = ConfigOverrides {
model_provider: Some("oss".to_string()),
..Default::default()
};
let persisted_metadata =
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(typesafe_overrides.model, None);
assert_eq!(typesafe_overrides.model_provider, Some("oss".to_string()));
assert_eq!(request_overrides, None);
Ok(())
}
#[test]
fn merge_persisted_resume_metadata_skips_persisted_values_when_reasoning_effort_overridden()
-> Result<()> {
let mut request_overrides = Some(HashMap::from([(
"model_reasoning_effort".to_string(),
serde_json::Value::String("low".to_string()),
)]));
let mut typesafe_overrides = ConfigOverrides::default();
let persisted_metadata =
test_thread_metadata(Some("gpt-5.1-codex-max"), Some(ReasoningEffort::High))?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(typesafe_overrides.model, None);
assert_eq!(
request_overrides,
Some(HashMap::from([(
"model_reasoning_effort".to_string(),
serde_json::Value::String("low".to_string()),
)]))
);
Ok(())
}
#[test]
fn merge_persisted_resume_metadata_skips_missing_values() -> Result<()> {
let mut request_overrides = None;
let mut typesafe_overrides = ConfigOverrides::default();
let persisted_metadata = test_thread_metadata(None, None)?;
merge_persisted_resume_metadata(
&mut request_overrides,
&mut typesafe_overrides,
&persisted_metadata,
);
assert_eq!(typesafe_overrides.model, None);
assert_eq!(request_overrides, None);
Ok(())
}
#[test]
fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {
let conversation_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?;
@@ -26,9 +26,47 @@ pub(super) async fn load_plugin_app_summaries(
}
};
connectors::connectors_for_plugin_apps(connectors, plugin_apps)
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
let accessible_connectors =
match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
config, /*force_refetch*/ false,
)
.await
{
Ok(status) if status.codex_apps_ready => status.connectors,
Ok(_) => {
return plugin_connectors
.into_iter()
.map(AppSummary::from)
.collect();
}
Err(err) => {
warn!("failed to load app auth state for plugin/read: {err:#}");
return plugin_connectors
.into_iter()
.map(AppSummary::from)
.collect();
}
};
let accessible_ids = accessible_connectors
.iter()
.map(|connector| connector.id.as_str())
.collect::<HashSet<_>>();
plugin_connectors
.into_iter()
.map(AppSummary::from)
.map(|connector| {
let needs_auth = !accessible_ids.contains(connector.id.as_str());
AppSummary {
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
needs_auth,
}
})
.collect()
}
@@ -58,7 +96,13 @@ pub(super) fn plugin_apps_needing_auth(
&& !accessible_ids.contains(connector.id.as_str())
})
.cloned()
.map(AppSummary::from)
.map(|connector| AppSummary {
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
needs_auth: true,
})
.collect()
}
@@ -0,0 +1,95 @@
use std::collections::HashMap;
use std::sync::Arc;
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_core::config::Config;
use codex_core::config::types::McpServerConfig;
use codex_core::mcp::auth::McpOAuthLoginSupport;
use codex_core::mcp::auth::oauth_login_support;
use codex_core::mcp::auth::resolve_oauth_scopes;
use codex_core::mcp::auth::should_retry_without_scopes;
use codex_rmcp_client::perform_oauth_login;
use tracing::warn;
use super::CodexMessageProcessor;
impl CodexMessageProcessor {
pub(super) async fn start_plugin_mcp_oauth_logins(
&self,
config: &Config,
plugin_mcp_servers: HashMap<String, McpServerConfig>,
) {
for (name, server) in plugin_mcp_servers {
let oauth_config = match oauth_login_support(&server.transport).await {
McpOAuthLoginSupport::Supported(config) => config,
McpOAuthLoginSupport::Unsupported => continue,
McpOAuthLoginSupport::Unknown(err) => {
warn!(
"MCP server may or may not require login for plugin install {name}: {err}"
);
continue;
}
};
let resolved_scopes = resolve_oauth_scopes(
/*explicit_scopes*/ None,
server.scopes.clone(),
oauth_config.discovered_scopes.clone(),
);
let store_mode = config.mcp_oauth_credentials_store_mode;
let callback_port = config.mcp_oauth_callback_port;
let callback_url = config.mcp_oauth_callback_url.clone();
let outgoing = Arc::clone(&self.outgoing);
let notification_name = name.clone();
tokio::spawn(async move {
let first_attempt = perform_oauth_login(
&name,
&oauth_config.url,
store_mode,
oauth_config.http_headers.clone(),
oauth_config.env_http_headers.clone(),
&resolved_scopes.scopes,
server.oauth_resource.as_deref(),
callback_port,
callback_url.as_deref(),
)
.await;
let final_result = match first_attempt {
Err(err) if should_retry_without_scopes(&resolved_scopes, &err) => {
perform_oauth_login(
&name,
&oauth_config.url,
store_mode,
oauth_config.http_headers,
oauth_config.env_http_headers,
&[],
server.oauth_resource.as_deref(),
callback_port,
callback_url.as_deref(),
)
.await
}
result => result,
};
let (success, error) = match final_result {
Ok(()) => (true, None),
Err(err) => (false, Some(err.to_string())),
};
let notification = ServerNotification::McpServerOauthLoginCompleted(
McpServerOauthLoginCompletedNotification {
name: notification_name,
success,
error,
},
);
outgoing.send_server_notification(notification).await;
});
}
}
}
+2
View File
@@ -733,6 +733,7 @@ mod tests {
env: HashMap::new(),
network: None,
expiration: ExecExpiration::DefaultTimeout,
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
sandbox: SandboxType::WindowsRestrictedToken,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
@@ -845,6 +846,7 @@ mod tests {
env: HashMap::new(),
network: None,
expiration: ExecExpiration::Cancellation(CancellationToken::new()),
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
sandbox: SandboxType::None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
+2
View File
@@ -298,6 +298,7 @@ mod tests {
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
guardian_developer_instructions: None,
feature_requirements: Some(codex_core::config_loader::FeatureRequirementsToml {
entries: std::collections::BTreeMap::from([
("apps".to_string(), false),
@@ -374,6 +375,7 @@ mod tests {
allowed_approval_policies: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: Some(Vec::new()),
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
+6 -6
View File
@@ -18,11 +18,11 @@ use codex_app_server_protocol::FsRemoveResponse;
use codex_app_server_protocol::FsWriteFileParams;
use codex_app_server_protocol::FsWriteFileResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_environment::CopyOptions;
use codex_environment::CreateDirectoryOptions;
use codex_environment::Environment;
use codex_environment::ExecutorFileSystem;
use codex_environment::RemoveOptions;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use std::io;
use std::sync::Arc;
@@ -34,7 +34,7 @@ pub(crate) struct FsApi {
impl Default for FsApi {
fn default() -> Self {
Self {
file_system: Arc::new(Environment.get_filesystem()),
file_system: Environment::default().get_filesystem(),
}
}
}
@@ -5,6 +5,7 @@ use std::sync::Mutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use codex_app_server_protocol::FuzzyFileSearchMatchType;
use codex_app_server_protocol::FuzzyFileSearchResult;
use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
@@ -60,6 +61,10 @@ pub(crate) async fn run_fuzzy_file_search(
FuzzyFileSearchResult {
root: m.root.to_string_lossy().to_string(),
path: m.path.to_string_lossy().to_string(),
match_type: match m.match_type {
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
},
file_name: file_name.to_string_lossy().to_string(),
score: m.score,
indices: m.indices,
@@ -231,6 +236,10 @@ fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec<FuzzyFileSea
FuzzyFileSearchResult {
root: m.root.to_string_lossy().to_string(),
path: m.path.to_string_lossy().to_string(),
match_type: match m.match_type {
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
},
file_name: file_name.to_string_lossy().to_string(),
score: m.score,
indices: m.indices.clone(),
+3 -1
View File
@@ -336,6 +336,7 @@ pub async fn run_main(
loader_overrides,
default_analytics_enabled,
AppServerTransport::Stdio,
SessionSource::VSCode,
)
.await
}
@@ -346,6 +347,7 @@ pub async fn run_main_with_transport(
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
transport: AppServerTransport,
session_source: SessionSource,
) -> IoResult<()> {
let (transport_event_tx, mut transport_event_rx) =
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
@@ -621,7 +623,7 @@ pub async fn run_main_with_transport(
feedback: feedback.clone(),
log_db,
config_warnings,
session_source: SessionSource::VSCode,
session_source,
enable_codex_api_key_env: false,
});
let mut thread_created_rx = processor.thread_created_receiver();
+12
View File
@@ -4,6 +4,7 @@ use codex_app_server::run_main_with_transport;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_core::config_loader::LoaderOverrides;
use codex_protocol::protocol::SessionSource;
use codex_utils_cli::CliConfigOverrides;
use std::path::PathBuf;
@@ -21,6 +22,15 @@ struct AppServerArgs {
default_value = AppServerTransport::DEFAULT_LISTEN_URL
)]
listen: AppServerTransport,
/// Session source used to derive product restrictions and metadata.
#[arg(
long = "session-source",
value_name = "SOURCE",
default_value = "vscode",
value_parser = SessionSource::from_startup_arg
)]
session_source: SessionSource,
}
fn main() -> anyhow::Result<()> {
@@ -32,6 +42,7 @@ fn main() -> anyhow::Result<()> {
..Default::default()
};
let transport = args.listen;
let session_source = args.session_source;
run_main_with_transport(
arg0_paths,
@@ -39,6 +50,7 @@ fn main() -> anyhow::Result<()> {
loader_overrides,
/*default_analytics_enabled*/ false,
transport,
session_source,
)
.await?;
Ok(())
+10 -8
View File
@@ -50,10 +50,6 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::AnalyticsEventsClient;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::auth::ExternalAuthRefreshContext;
use codex_core::auth::ExternalAuthRefreshReason;
use codex_core::auth::ExternalAuthRefresher;
use codex_core::auth::ExternalAuthTokens;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
@@ -64,6 +60,10 @@ use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::default_client::set_default_originator;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_feedback::CodexFeedback;
use codex_login::auth::ExternalAuthRefreshContext;
use codex_login::auth::ExternalAuthRefreshReason;
use codex_login::auth::ExternalAuthRefresher;
use codex_login::auth::ExternalAuthTokens;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
@@ -228,10 +228,7 @@ impl MessageProcessor {
thread_manager
.plugins_manager()
.set_analytics_events_client(analytics_events_client.clone());
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config);
let cloud_requirements = Arc::new(RwLock::new(cloud_requirements));
let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs {
auth_manager: auth_manager.clone(),
@@ -244,6 +241,11 @@ impl MessageProcessor {
feedback,
log_db,
});
// Keep plugin startup warmups aligned at app-server startup.
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_curated_repo_sync_for_config(&config, auth_manager.clone());
let config_api = ConfigApi::new(
config.codex_home.clone(),
cli_overrides,
@@ -580,7 +580,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
parent_span_id: remote_parent_span_id,
context: remote_trace,
} = RemoteTrace::new("00000000000000000000000000000077", "0000000000000088");
let _: TurnStartResponse = harness
let turn_start_response: TurnStartResponse = harness
.request(
ClientRequest::TurnStart {
request_id: RequestId::Integer(3),
@@ -628,6 +628,10 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
assert_eq!(server_request_span.parent_span_id, remote_parent_span_id);
assert!(server_request_span.parent_span_is_remote);
assert_eq!(server_request_span.span_context.trace_id(), remote_trace_id);
assert_eq!(
span_attr(server_request_span, "turn.id"),
Some(turn_start_response.turn.id.as_str())
);
assert_span_descends_from(&spans, core_turn_span, server_request_span);
harness.shutdown().await;
@@ -75,6 +75,10 @@ impl RequestContext {
pub(crate) fn span(&self) -> Span {
self.span.clone()
}
fn record_turn_id(&self, turn_id: &str) {
self.span.record("turn.id", turn_id);
}
}
#[derive(Debug, Clone)]
@@ -217,6 +221,17 @@ impl OutgoingMessageSender {
.and_then(RequestContext::request_trace)
}
pub(crate) async fn record_request_turn_id(
&self,
request_id: &ConnectionRequestId,
turn_id: &str,
) {
let request_contexts = self.request_contexts.lock().await;
if let Some(request_context) = request_contexts.get(request_id) {
request_context.record_turn_id(turn_id);
}
}
async fn take_request_context(
&self,
request_id: &ConnectionRequestId,
@@ -68,6 +68,7 @@ use codex_app_server_protocol::ThreadRealtimeStopParams;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadRollbackParams;
use codex_app_server_protocol::ThreadSetNameParams;
use codex_app_server_protocol::ThreadShellCommandParams;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadUnarchiveParams;
use codex_app_server_protocol::ThreadUnsubscribeParams;
@@ -95,7 +96,11 @@ pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
impl McpProcess {
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
Self::new_with_env(codex_home, &[]).await
Self::new_with_env_and_args(codex_home, &[], &[]).await
}
pub async fn new_with_args(codex_home: &Path, args: &[&str]) -> anyhow::Result<Self> {
Self::new_with_env_and_args(codex_home, &[], args).await
}
/// Creates a new MCP process, allowing tests to override or remove
@@ -106,6 +111,14 @@ impl McpProcess {
pub async fn new_with_env(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
) -> anyhow::Result<Self> {
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
}
async fn new_with_env_and_args(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
args: &[&str],
) -> anyhow::Result<Self> {
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
.context("should find binary for codex-app-server")?;
@@ -118,6 +131,7 @@ impl McpProcess {
cmd.env("CODEX_HOME", codex_home);
cmd.env("RUST_LOG", "info");
cmd.env_remove(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR);
cmd.args(args);
for (k, v) in env_overrides {
match v {
@@ -386,6 +400,15 @@ impl McpProcess {
self.send_request("thread/compact/start", params).await
}
/// Send a `thread/shellCommand` JSON-RPC request.
pub async fn send_thread_shell_command_request(
&mut self,
params: ThreadShellCommandParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("thread/shellCommand", params).await
}
/// Send a `thread/rollback` JSON-RPC request.
pub async fn send_thread_rollback_request(
&mut self,
@@ -257,6 +257,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": "abexy",
"match_type": "file",
"file_name": "abexy",
"score": 84,
"indices": [0, 1, 2],
@@ -264,6 +265,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": sub_abce_rel,
"match_type": "file",
"file_name": "abce",
"score": expected_score,
"indices": [4, 5, 7],
@@ -271,6 +273,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
{
"root": root_path.clone(),
"path": "abcde",
"match_type": "file",
"file_name": "abcde",
"score": 71,
"indices": [0, 1, 4],
@@ -38,6 +38,7 @@ mod thread_name_websocket;
mod thread_read;
mod thread_resume;
mod thread_rollback;
mod thread_shell_command;
mod thread_start;
mod thread_status;
mod thread_unarchive;
@@ -146,6 +146,56 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
Ok(())
}
#[tokio::test]
async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
"name": "debug",
"plugins": [
{
"name": "sample-plugin",
"source": {
"source": "local",
"path": "./sample-plugin"
},
"policy": {
"products": ["CHATGPT"]
}
}
]
}"#,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let mut mcp =
McpProcess::new_with_args(codex_home.path(), &["--session-source", "atlas"]).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path,
plugin_name: "sample-plugin".to_string(),
force_remote_sync: false,
})
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.code, -32600);
assert!(err.error.message.contains("not available for install"));
Ok(())
}
#[tokio::test]
async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_install() -> Result<()>
{
@@ -261,21 +311,22 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
let response: PluginInstallResponse = to_response(response)?;
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
let payloads = timeout(DEFAULT_TIMEOUT, async {
let payload = timeout(DEFAULT_TIMEOUT, async {
loop {
let Some(requests) = analytics_server.received_requests().await else {
tokio::time::sleep(Duration::from_millis(25)).await;
continue;
};
if !requests.is_empty() {
break requests;
if let Some(request) = requests.iter().find(|request| {
request.method == "POST" && request.url.path() == "/codex/analytics-events/events"
}) {
break request.body.clone();
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
})
.await?;
let payload: serde_json::Value =
serde_json::from_slice(&payloads[0].body).expect("analytics payload");
let payload: serde_json::Value = serde_json::from_slice(&payload).expect("analytics payload");
assert_eq!(
payload,
json!({
@@ -384,6 +435,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
needs_auth: true,
}],
}
);
@@ -467,6 +519,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
needs_auth: true,
}],
}
);
@@ -476,6 +529,79 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"[features]\nplugins = true\n",
)?;
let repo_root = TempDir::new()?;
write_plugin_marketplace(
repo_root.path(),
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
std::fs::write(
repo_root.path().join("sample-plugin/.mcp.json"),
r#"{
"mcpServers": {
"sample-mcp": {
"command": "echo"
}
}
}"#,
)?;
let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_install_request(PluginInstallParams {
marketplace_path,
plugin_name: "sample-plugin".to_string(),
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginInstallResponse = to_response(response)?;
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(!config.contains("[mcp_servers.sample-mcp]"));
assert!(!config.contains("command = \"echo\""));
let request_id = mcp
.send_raw_request(
"mcpServer/oauth/login",
Some(json!({
"name": "sample-mcp",
})),
)
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.code, -32600);
assert_eq!(
err.error.message,
"OAuth login is only supported for streamable HTTP servers."
);
Ok(())
}
#[derive(Clone)]
struct AppsServerState {
response: Arc<StdMutex<serde_json::Value>>,
@@ -1,6 +1,7 @@
use std::time::Duration;
use anyhow::Result;
use anyhow::bail;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::to_response;
@@ -28,12 +29,22 @@ use wiremock::matchers::path;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
r#"[features]
plugins = true
"#,
)
}
#[tokio::test]
async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> Result<()> {
async fn plugin_list_skips_invalid_marketplace_file() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
"{not json",
@@ -57,14 +68,23 @@ async fn plugin_list_returns_invalid_request_for_invalid_marketplace_file() -> R
})
.await?;
let err = timeout(
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(err.error.code, -32600);
assert!(err.error.message.contains("invalid marketplace file"));
assert!(
response.marketplaces.iter().all(|marketplace| {
marketplace.path
!= AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)
.expect("absolute marketplace path")
}),
"invalid marketplace should be skipped"
);
Ok(())
}
@@ -98,6 +118,7 @@ async fn plugin_list_rejects_relative_cwds() -> Result<()> {
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
codex_home.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -385,6 +406,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -518,6 +540,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
write_plugins_enabled_config(codex_home.path())?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -652,6 +675,16 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/backend-api/plugins/featured"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(r#"["linear@openai-curated","calendar@openai-curated"]"#),
)
.mount(&server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -670,6 +703,13 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(response.remote_sync_error, None);
assert_eq!(
response.featured_plugin_ids,
vec![
"linear@openai-curated".to_string(),
"calendar@openai-curated".to_string(),
]
);
let curated_marketplace = response
.marketplaces
@@ -715,6 +755,114 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu
Ok(())
}
#[tokio::test]
async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
Mock::given(method("GET"))
.and(path("/backend-api/plugins/featured"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
.mount(&server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: None,
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(
response.featured_plugin_ids,
vec!["linear@openai-curated".to_string()]
);
assert_eq!(response.remote_sync_error, None);
Ok(())
}
#[tokio::test]
async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
Mock::given(method("GET"))
.and(path("/backend-api/plugins/featured"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
.expect(1)
.mount(&server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_featured_plugin_request_count(&server, 1).await?;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: None,
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
assert_eq!(
response.featured_plugin_ids,
vec!["linear@openai-curated".to_string()]
);
assert_eq!(response.remote_sync_error, None);
Ok(())
}
async fn wait_for_featured_plugin_request_count(
server: &MockServer,
expected_count: usize,
) -> Result<()> {
timeout(DEFAULT_TIMEOUT, async {
loop {
let Some(requests) = server.received_requests().await else {
bail!("wiremock did not record requests");
};
let featured_request_count = requests
.iter()
.filter(|request| {
request.method == "GET" && request.url.path().ends_with("/plugins/featured")
})
.count();
if featured_request_count == expected_count {
return Ok::<(), anyhow::Error>(());
}
if featured_request_count > expected_count {
bail!(
"expected exactly {expected_count} /plugins/featured requests, got {featured_request_count}"
);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await??;
Ok(())
}
fn write_installed_plugin(
codex_home: &TempDir,
marketplace_name: &str,
@@ -1,17 +1,46 @@
use std::borrow::Cow;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use axum::Json;
use axum::Router;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::http::Uri;
use axum::http::header::AUTHORIZATION;
use axum::routing::get;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginReadParams;
use codex_app_server_protocol::PluginReadResponse;
use codex_app_server_protocol::RequestId;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use rmcp::handler::server::ServerHandler;
use rmcp::model::JsonObject;
use rmcp::model::ListToolsResult;
use rmcp::model::Meta;
use rmcp::model::ServerCapabilities;
use rmcp::model::ServerInfo;
use rmcp::model::Tool;
use rmcp::model::ToolAnnotations;
use rmcp::transport::StreamableHttpServerConfig;
use rmcp::transport::StreamableHttpService;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use serde_json::json;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -25,6 +54,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer"))?;
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
@@ -79,6 +109,32 @@ description: Summarize email threads
---
# Thread Summarizer
"#,
)?;
std::fs::write(
plugin_root.join("skills/chatgpt-only/SKILL.md"),
r#"---
name: chatgpt-only
description: Visible only for ChatGPT
---
# ChatGPT Only
"#,
)?;
std::fs::create_dir_all(plugin_root.join("skills/thread-summarizer/agents"))?;
std::fs::write(
plugin_root.join("skills/thread-summarizer/agents/openai.yaml"),
r#"policy:
products:
- CODEX
"#,
)?;
std::fs::create_dir_all(plugin_root.join("skills/chatgpt-only/agents"))?;
std::fs::write(
plugin_root.join("skills/chatgpt-only/agents/openai.yaml"),
r#"policy:
products:
- CHATGPT
"#,
)?;
std::fs::write(
@@ -195,11 +251,103 @@ enabled = true
response.plugin.apps[0].install_url.as_deref(),
Some("https://chatgpt.com/apps/gmail/gmail")
);
assert_eq!(response.plugin.apps[0].needs_auth, true);
assert_eq!(response.plugin.mcp_servers.len(), 1);
assert_eq!(response.plugin.mcp_servers[0], "demo");
Ok(())
}
#[tokio::test]
async fn plugin_read_returns_app_needs_auth() -> Result<()> {
let connectors = vec![
AppInfo {
id: "alpha".to_string(),
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: Some("featured".to_string()),
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
name: "Beta".to_string(),
description: Some("Beta connector".to_string()),
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
branding: None,
app_metadata: None,
labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
},
];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) = start_apps_server(connectors, tools).await?;
let codex_home = TempDir::new()?;
write_connectors_config(codex_home.path(), &server_url)?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let repo_root = TempDir::new()?;
write_plugin_marketplace(
repo_root.path(),
"debug",
"sample-plugin",
"./sample-plugin",
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
let marketplace_path =
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path,
plugin_name: "sample-plugin".to_string(),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginReadResponse = to_response(response)?;
assert_eq!(
response
.plugin
.apps
.iter()
.map(|app| (app.id.as_str(), app.needs_auth))
.collect::<Vec<_>>(),
vec![("alpha", true), ("beta", false)]
);
server_handle.abort();
let _ = server_handle.await;
Ok(())
}
#[tokio::test]
async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -232,6 +380,7 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
}
}"##,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -285,6 +434,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<
]
}"#,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -336,6 +486,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -
]
}"#,
)?;
write_plugins_enabled_config(&codex_home)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
@@ -382,3 +533,211 @@ fn write_installed_plugin(
)?;
Ok(())
}
fn write_plugins_enabled_config(codex_home: &TempDir) -> Result<()> {
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
"#,
)?;
Ok(())
}
#[derive(Clone)]
struct AppsServerState {
response: Arc<StdMutex<serde_json::Value>>,
}
#[derive(Clone)]
struct PluginReadMcpServer {
tools: Arc<StdMutex<Vec<Tool>>>,
}
impl ServerHandler for PluginReadMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
..ServerInfo::default()
}
}
fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> impl std::future::Future<Output = Result<ListToolsResult, rmcp::ErrorData>> + Send + '_
{
let tools = self.tools.clone();
async move {
let tools = tools
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
Ok(ListToolsResult {
tools,
next_cursor: None,
meta: None,
})
}
}
}
async fn start_apps_server(
connectors: Vec<AppInfo>,
tools: Vec<Tool>,
) -> Result<(String, JoinHandle<()>)> {
let state = Arc::new(AppsServerState {
response: Arc::new(StdMutex::new(
json!({ "apps": connectors, "next_token": null }),
)),
});
let tools = Arc::new(StdMutex::new(tools));
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let mcp_service = StreamableHttpService::new(
{
let tools = tools.clone();
move || {
Ok(PluginReadMcpServer {
tools: tools.clone(),
})
}
},
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
let router = Router::new()
.route("/connectors/directory/list", get(list_directory_connectors))
.route(
"/connectors/directory/list_workspace",
get(list_directory_connectors),
)
.with_state(state)
.nest_service("/api/codex/apps", mcp_service);
let handle = tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
Ok((format!("http://{addr}"), handle))
}
async fn list_directory_connectors(
State(state): State<Arc<AppsServerState>>,
headers: HeaderMap,
uri: Uri,
) -> Result<impl axum::response::IntoResponse, StatusCode> {
let bearer_ok = headers
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == "Bearer chatgpt-token");
let account_ok = headers
.get("chatgpt-account-id")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == "account-123");
let external_logos_ok = uri
.query()
.is_some_and(|query| query.split('&').any(|pair| pair == "external_logos=true"));
if !bearer_ok || !account_ok {
Err(StatusCode::UNAUTHORIZED)
} else if !external_logos_ok {
Err(StatusCode::BAD_REQUEST)
} else {
let response = state
.response
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
Ok(Json(response))
}
}
fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool> {
let schema: JsonObject = serde_json::from_value(json!({
"type": "object",
"additionalProperties": false
}))?;
let mut tool = Tool::new(
Cow::Owned(format!("connector_{connector_id}")),
Cow::Borrowed("Connector test tool"),
Arc::new(schema),
);
tool.annotations = Some(ToolAnnotations::new().read_only(true));
let mut meta = Meta::new();
meta.0
.insert("connector_id".to_string(), json!(connector_id));
meta.0
.insert("connector_name".to_string(), json!(connector_name));
tool.meta = Some(meta);
Ok(tool)
}
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
chatgpt_base_url = "{base_url}"
mcp_oauth_credentials_store = "file"
[features]
plugins = true
connectors = true
"#
),
)
}
fn write_plugin_marketplace(
repo_root: &std::path::Path,
marketplace_name: &str,
plugin_name: &str,
source_path: &str,
) -> std::io::Result<()> {
std::fs::create_dir_all(repo_root.join(".git"))?;
std::fs::create_dir_all(repo_root.join(".agents/plugins"))?;
std::fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "{marketplace_name}",
"plugins": [
{{
"name": "{plugin_name}",
"source": {{
"source": "local",
"path": "{source_path}"
}}
}}
]
}}"#
),
)
}
fn write_plugin_source(
repo_root: &std::path::Path,
plugin_name: &str,
app_ids: &[&str],
) -> Result<()> {
let plugin_root = repo_root.join(plugin_name);
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
format!(r#"{{"name":"{plugin_name}"}}"#),
)?;
let apps = app_ids
.iter()
.map(|app_id| ((*app_id).to_string(), json!({ "id": app_id })))
.collect::<serde_json::Map<_, _>>();
std::fs::write(
plugin_root.join(".app.json"),
serde_json::to_vec_pretty(&json!({ "apps": apps }))?,
)?;
Ok(())
}
@@ -183,21 +183,22 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> {
let response: PluginUninstallResponse = to_response(response)?;
assert_eq!(response, PluginUninstallResponse {});
let payloads = timeout(DEFAULT_TIMEOUT, async {
let payload = timeout(DEFAULT_TIMEOUT, async {
loop {
let Some(requests) = analytics_server.received_requests().await else {
tokio::time::sleep(Duration::from_millis(25)).await;
continue;
};
if !requests.is_empty() {
break requests;
if let Some(request) = requests.iter().find(|request| {
request.method == "POST" && request.url.path() == "/codex/analytics-events/events"
}) {
break request.body.clone();
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
})
.await?;
let payload: serde_json::Value =
serde_json::from_slice(&payloads[0].body).expect("analytics payload");
let payload: serde_json::Value = serde_json::from_slice(&payload).expect("analytics payload");
assert_eq!(
payload,
json!({
@@ -450,6 +450,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
"payload": serde_json::to_value(EventMsg::AgentMessage(AgentMessageEvent {
message: "Still running".to_string(),
phase: None,
memory_citation: None,
}))?,
})
.to_string(),
@@ -0,0 +1,439 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_sequence;
use app_test_support::create_shell_command_sse_response;
use app_test_support::format_with_current_shell_display;
use app_test_support::to_response;
use codex_app_server_protocol::CommandExecutionApprovalDecision;
use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
use codex_app_server_protocol::CommandExecutionSource;
use codex_app_server_protocol::CommandExecutionStatus;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadReadParams;
use codex_app_server_protocol::ThreadReadResponse;
use codex_app_server_protocol::ThreadShellCommandParams;
use codex_app_server_protocol::ThreadShellCommandResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_core::features::FEATURES;
use codex_core::features::Feature;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test]
async fn thread_shell_command_runs_as_standalone_turn_and_persists_history() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let server = create_mock_responses_server_sequence(vec![]).await;
create_config_toml(
codex_home.as_path(),
&server.uri(),
"never",
&BTreeMap::default(),
)?;
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
persist_extended_history: true,
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let shell_id = mcp
.send_thread_shell_command_request(ThreadShellCommandParams {
thread_id: thread.id.clone(),
command: "printf 'hello from bang\\n'".to_string(),
})
.await?;
let shell_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
)
.await??;
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
let started = wait_for_command_execution_started(&mut mcp, None).await?;
let ThreadItem::CommandExecution {
id, source, status, ..
} = &started.item
else {
unreachable!("helper returns command execution item");
};
let command_id = id.clone();
assert_eq!(source, &CommandExecutionSource::UserShell);
assert_eq!(status, &CommandExecutionStatus::InProgress);
let delta = wait_for_command_execution_output_delta(&mut mcp, &command_id).await?;
assert_eq!(delta.delta, "hello from bang\n");
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
let ThreadItem::CommandExecution {
id,
source,
status,
aggregated_output,
exit_code,
..
} = &completed.item
else {
unreachable!("helper returns command execution item");
};
assert_eq!(id, &command_id);
assert_eq!(source, &CommandExecutionSource::UserShell);
assert_eq!(status, &CommandExecutionStatus::Completed);
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
assert_eq!(*exit_code, Some(0));
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: thread.id,
include_turns: true,
})
.await?;
let read_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
)
.await??;
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
assert_eq!(thread.turns.len(), 1);
let ThreadItem::CommandExecution {
source,
status,
aggregated_output,
..
} = thread.turns[0]
.items
.iter()
.find(|item| matches!(item, ThreadItem::CommandExecution { .. }))
.expect("expected persisted command execution item")
else {
unreachable!("matched command execution item");
};
assert_eq!(source, &CommandExecutionSource::UserShell);
assert_eq!(status, &CommandExecutionStatus::Completed);
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
Ok(())
}
#[tokio::test]
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let workspace = tmp.path().join("workspace");
std::fs::create_dir(&workspace)?;
let responses = vec![
create_shell_command_sse_response(
vec![
"python3".to_string(),
"-c".to_string(),
"print(42)".to_string(),
],
None,
Some(5000),
"call-approve",
)?,
create_final_assistant_message_sse_response("done")?,
];
let server = create_mock_responses_server_sequence(responses).await;
create_config_toml(
codex_home.as_path(),
&server.uri(),
"untrusted",
&BTreeMap::default(),
)?;
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
persist_extended_history: true,
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run python".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
let agent_started = wait_for_command_execution_started(&mut mcp, Some("call-approve")).await?;
let ThreadItem::CommandExecution {
command, source, ..
} = &agent_started.item
else {
unreachable!("helper returns command execution item");
};
assert_eq!(source, &CommandExecutionSource::Agent);
assert_eq!(
command,
&format_with_current_shell_display("python3 -c 'print(42)'")
);
let server_req = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_request_message(),
)
.await??;
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = server_req else {
panic!("expected approval request");
};
let shell_id = mcp
.send_thread_shell_command_request(ThreadShellCommandParams {
thread_id: thread.id.clone(),
command: "printf 'active turn bang\\n'".to_string(),
})
.await?;
let shell_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
)
.await??;
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
let started =
wait_for_command_execution_started_by_source(&mut mcp, CommandExecutionSource::UserShell)
.await?;
assert_eq!(started.turn_id, turn.id);
let command_id = match &started.item {
ThreadItem::CommandExecution { id, .. } => id.clone(),
_ => unreachable!("helper returns command execution item"),
};
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
assert_eq!(completed.turn_id, turn.id);
let ThreadItem::CommandExecution {
source,
aggregated_output,
..
} = &completed.item
else {
unreachable!("helper returns command execution item");
};
assert_eq!(source, &CommandExecutionSource::UserShell);
assert_eq!(aggregated_output.as_deref(), Some("active turn bang\n"));
mcp.send_response(
request_id,
serde_json::to_value(CommandExecutionRequestApprovalResponse {
decision: CommandExecutionApprovalDecision::Decline,
})?,
)
.await?;
let _: TurnCompletedNotification = serde_json::from_value(
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??
.params
.expect("turn/completed params"),
)?;
let read_id = mcp
.send_thread_read_request(ThreadReadParams {
thread_id: thread.id,
include_turns: true,
})
.await?;
let read_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
)
.await??;
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
assert_eq!(thread.turns.len(), 1);
assert!(
thread.turns[0].items.iter().any(|item| {
matches!(
item,
ThreadItem::CommandExecution {
source: CommandExecutionSource::UserShell,
aggregated_output,
..
} if aggregated_output.as_deref() == Some("active turn bang\n")
)
}),
"expected active-turn shell command to be persisted on the existing turn"
);
Ok(())
}
async fn wait_for_command_execution_started(
mcp: &mut McpProcess,
expected_id: Option<&str>,
) -> Result<ItemStartedNotification> {
loop {
let notif = mcp
.read_stream_until_notification_message("item/started")
.await?;
let started: ItemStartedNotification = serde_json::from_value(
notif
.params
.ok_or_else(|| anyhow::anyhow!("missing item/started params"))?,
)?;
let ThreadItem::CommandExecution { id, .. } = &started.item else {
continue;
};
if expected_id.is_none() || expected_id == Some(id.as_str()) {
return Ok(started);
}
}
}
async fn wait_for_command_execution_started_by_source(
mcp: &mut McpProcess,
expected_source: CommandExecutionSource,
) -> Result<ItemStartedNotification> {
loop {
let started = wait_for_command_execution_started(mcp, None).await?;
let ThreadItem::CommandExecution { source, .. } = &started.item else {
continue;
};
if source == &expected_source {
return Ok(started);
}
}
}
async fn wait_for_command_execution_completed(
mcp: &mut McpProcess,
expected_id: Option<&str>,
) -> Result<ItemCompletedNotification> {
loop {
let notif = mcp
.read_stream_until_notification_message("item/completed")
.await?;
let completed: ItemCompletedNotification = serde_json::from_value(
notif
.params
.ok_or_else(|| anyhow::anyhow!("missing item/completed params"))?,
)?;
let ThreadItem::CommandExecution { id, .. } = &completed.item else {
continue;
};
if expected_id.is_none() || expected_id == Some(id.as_str()) {
return Ok(completed);
}
}
}
async fn wait_for_command_execution_output_delta(
mcp: &mut McpProcess,
item_id: &str,
) -> Result<CommandExecutionOutputDeltaNotification> {
loop {
let notif = mcp
.read_stream_until_notification_message("item/commandExecution/outputDelta")
.await?;
let delta: CommandExecutionOutputDeltaNotification = serde_json::from_value(
notif
.params
.ok_or_else(|| anyhow::anyhow!("missing output delta params"))?,
)?;
if delta.item_id == item_id {
return Ok(delta);
}
}
}
fn create_config_toml(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
feature_flags: &BTreeMap<Feature, bool>,
) -> std::io::Result<()> {
let feature_entries = feature_flags
.iter()
.map(|(feature, enabled)| {
let key = FEATURES
.iter()
.find(|spec| spec.id == *feature)
.map(|spec| spec.key)
.unwrap_or_else(|| panic!("missing feature key for {feature:?}"));
format!("{key} = {enabled}")
})
.collect::<Vec<_>>()
.join("\n");
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
model = "mock-model"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[features]
{feature_entries}
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#
),
)
}
@@ -7,7 +7,10 @@ use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStartedNotification;
@@ -328,6 +331,103 @@ async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Re
Ok(())
}
#[tokio::test]
async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_with_optional_broken_mcp(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::default())
.await?;
let _: ThreadStartResponse = to_response(
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??,
)?;
let starting = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_matching_notification(
"mcpServer/startupStatus/updated starting",
|notification| {
notification.method == "mcpServer/startupStatus/updated"
&& notification
.params
.as_ref()
.and_then(|params| params.get("name"))
.and_then(Value::as_str)
== Some("optional_broken")
&& notification
.params
.as_ref()
.and_then(|params| params.get("status"))
.and_then(Value::as_str)
== Some("starting")
},
),
)
.await??;
let starting: ServerNotification = starting.try_into()?;
let ServerNotification::McpServerStatusUpdated(starting) = starting else {
anyhow::bail!("unexpected notification variant");
};
assert_eq!(
starting,
McpServerStatusUpdatedNotification {
name: "optional_broken".to_string(),
status: McpServerStartupState::Starting,
error: None,
}
);
let failed = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_matching_notification(
"mcpServer/startupStatus/updated failed",
|notification| {
notification.method == "mcpServer/startupStatus/updated"
&& notification
.params
.as_ref()
.and_then(|params| params.get("name"))
.and_then(Value::as_str)
== Some("optional_broken")
&& notification
.params
.as_ref()
.and_then(|params| params.get("status"))
.and_then(Value::as_str)
== Some("failed")
},
),
)
.await??;
let failed: ServerNotification = failed.try_into()?;
let ServerNotification::McpServerStatusUpdated(failed) = failed else {
anyhow::bail!("unexpected notification variant");
};
assert_eq!(failed.name, "optional_broken");
assert_eq!(failed.status, McpServerStartupState::Failed);
assert!(
failed
.error
.as_deref()
.is_some_and(|error| error.contains("MCP client for `optional_broken` failed to start")),
"unexpected MCP startup error: {:?}",
failed.error
);
Ok(())
}
#[tokio::test]
async fn thread_start_surfaces_cloud_requirements_load_errors() -> Result<()> {
let server = MockServer::start().await;
@@ -491,3 +591,32 @@ required = true
),
)
}
fn create_config_toml_with_optional_broken_mcp(
codex_home: &Path,
server_uri: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
config_toml,
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
[mcp_servers.optional_broken]
command = "codex-definitely-not-a-real-binary"
"#
),
)
}
@@ -13,7 +13,6 @@ use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
use codex_app_server_protocol::ByteRange;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::CollabAgentState;
use codex_app_server_protocol::CollabAgentStatus;
use codex_app_server_protocol::CollabAgentTool;
use codex_app_server_protocol::CollabAgentToolCallStatus;
@@ -1826,16 +1825,18 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
assert_eq!(prompt, Some(CHILD_PROMPT.to_string()));
assert_eq!(model, Some(REQUESTED_MODEL.to_string()));
assert_eq!(reasoning_effort, Some(REQUESTED_REASONING_EFFORT));
assert_eq!(
agents_states,
HashMap::from([(
receiver_thread_id,
CollabAgentState {
status: CollabAgentStatus::PendingInit,
message: None,
},
)])
let agent_state = agents_states
.get(&receiver_thread_id)
.expect("spawn completion should include child agent state");
assert!(
matches!(
agent_state.status,
CollabAgentStatus::PendingInit | CollabAgentStatus::Running
),
"child agent should still be initializing or already running, got {:?}",
agent_state.status
);
assert_eq!(agent_state.message, None);
let turn_completed = timeout(DEFAULT_READ_TIMEOUT, async {
loop {
@@ -2008,16 +2009,18 @@ config_file = "./custom-role.toml"
assert_eq!(prompt, Some(CHILD_PROMPT.to_string()));
assert_eq!(model, Some(ROLE_MODEL.to_string()));
assert_eq!(reasoning_effort, Some(ROLE_REASONING_EFFORT));
assert_eq!(
agents_states,
HashMap::from([(
receiver_thread_id,
CollabAgentState {
status: CollabAgentStatus::PendingInit,
message: None,
},
)])
let agent_state = agents_states
.get(&receiver_thread_id)
.expect("spawn completion should include child agent state");
assert!(
matches!(
agent_state.status,
CollabAgentStatus::PendingInit | CollabAgentStatus::Running
),
"child agent should still be initializing or already running, got {:?}",
agent_state.status
);
assert_eq!(agent_state.message, None);
let turn_completed = timeout(DEFAULT_READ_TIMEOUT, async {
loop {
+2
View File
@@ -19,8 +19,10 @@ which = { workspace = true }
workspace = true
[dev-dependencies]
flate2 = { workspace = true }
pretty_assertions = { workspace = true }
sha2 = { workspace = true }
tar = { workspace = true }
tokio = { workspace = true, features = ["fs", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }
wiremock = { workspace = true }
zip = { workspace = true }
+38 -128
View File
@@ -11,10 +11,11 @@ use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
use tokio::time::timeout;
use url::Url;
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
/// Executes artifact build and render commands against a resolved runtime.
/// Executes artifact build commands against a resolved runtime.
#[derive(Clone, Debug)]
pub struct ArtifactsClient {
runtime_source: RuntimeSource,
@@ -54,7 +55,18 @@ impl ArtifactsClient {
source,
})?;
let script_path = staging_dir.path().join("artifact-build.mjs");
let wrapped_script = build_wrapped_script(&request.source);
let build_entrypoint_url =
Url::from_file_path(runtime.build_js_path()).map_err(|()| ArtifactsError::Io {
context: format!(
"failed to convert artifact build entrypoint to a file URL: {}",
runtime.build_js_path().display()
),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid artifact build entrypoint path",
),
})?;
let wrapped_script = build_wrapped_script(&build_entrypoint_url, &request.source);
fs::write(&script_path, wrapped_script)
.await
.map_err(|source| ArtifactsError::Io {
@@ -63,44 +75,8 @@ impl ArtifactsClient {
})?;
let mut command = Command::new(js_runtime.executable_path());
command
.arg(&script_path)
.current_dir(&request.cwd)
.env("CODEX_ARTIFACT_BUILD_ENTRYPOINT", runtime.build_js_path())
.env(
"CODEX_ARTIFACT_RENDER_ENTRYPOINT",
runtime.render_cli_path(),
)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if js_runtime.requires_electron_run_as_node() {
command.env("ELECTRON_RUN_AS_NODE", "1");
}
for (key, value) in &request.env {
command.env(key, value);
}
run_command(
command,
request.timeout.unwrap_or(DEFAULT_EXECUTION_TIMEOUT),
)
.await
}
/// Executes the artifact render CLI against the configured runtime.
pub async fn execute_render(
&self,
request: ArtifactRenderCommandRequest,
) -> Result<ArtifactCommandOutput, ArtifactsError> {
let runtime = self.resolve_runtime().await?;
let js_runtime = runtime.resolve_js_runtime()?;
let mut command = Command::new(js_runtime.executable_path());
command
.arg(runtime.render_cli_path())
.args(request.target.to_args())
.current_dir(&request.cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.arg(&script_path).current_dir(&request.cwd);
command.stdout(Stdio::piped()).stderr(Stdio::piped());
if js_runtime.requires_electron_run_as_node() {
command.env("ELECTRON_RUN_AS_NODE", "1");
}
@@ -132,76 +108,6 @@ pub struct ArtifactBuildRequest {
pub env: BTreeMap<String, String>,
}
/// Request payload for the artifact render CLI.
#[derive(Clone, Debug)]
pub struct ArtifactRenderCommandRequest {
pub cwd: PathBuf,
pub timeout: Option<Duration>,
pub env: BTreeMap<String, String>,
pub target: ArtifactRenderTarget,
}
/// Render targets supported by the packaged artifact runtime.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ArtifactRenderTarget {
Presentation(PresentationRenderTarget),
Spreadsheet(SpreadsheetRenderTarget),
}
impl ArtifactRenderTarget {
/// Converts a render target to the CLI args expected by `render_cli.mjs`.
pub fn to_args(&self) -> Vec<String> {
match self {
Self::Presentation(target) => {
vec![
"pptx".to_string(),
"render".to_string(),
"--in".to_string(),
target.input_path.display().to_string(),
"--slide".to_string(),
target.slide_number.to_string(),
"--out".to_string(),
target.output_path.display().to_string(),
]
}
Self::Spreadsheet(target) => {
let mut args = vec![
"xlsx".to_string(),
"render".to_string(),
"--in".to_string(),
target.input_path.display().to_string(),
"--sheet".to_string(),
target.sheet_name.clone(),
"--out".to_string(),
target.output_path.display().to_string(),
];
if let Some(range) = &target.range {
args.push("--range".to_string());
args.push(range.clone());
}
args
}
}
}
}
/// Presentation render request parameters.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PresentationRenderTarget {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub slide_number: u32,
}
/// Spreadsheet render request parameters.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SpreadsheetRenderTarget {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub sheet_name: String,
pub range: Option<String>,
}
/// Captured stdout, stderr, and exit status from an artifact subprocess.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactCommandOutput {
@@ -232,24 +138,28 @@ pub enum ArtifactsError {
TimedOut { timeout: Duration },
}
fn build_wrapped_script(source: &str) -> String {
format!(
concat!(
"import {{ pathToFileURL }} from \"node:url\";\n",
"const artifactTool = await import(pathToFileURL(process.env.CODEX_ARTIFACT_BUILD_ENTRYPOINT).href);\n",
"globalThis.artifactTool = artifactTool;\n",
"globalThis.artifacts = artifactTool;\n",
"globalThis.codexArtifacts = artifactTool;\n",
"for (const [name, value] of Object.entries(artifactTool)) {{\n",
" if (name === \"default\" || Object.prototype.hasOwnProperty.call(globalThis, name)) {{\n",
" continue;\n",
" }}\n",
" globalThis[name] = value;\n",
"}}\n\n",
"{}\n"
),
source
)
fn build_wrapped_script(build_entrypoint_url: &Url, source: &str) -> String {
let mut wrapped = String::new();
wrapped.push_str("const artifactTool = await import(");
wrapped.push_str(
&serde_json::to_string(build_entrypoint_url.as_str()).unwrap_or_else(|error| {
panic!("artifact build entrypoint URL must serialize: {error}")
}),
);
wrapped.push_str(");\n");
wrapped.push_str(
r#"globalThis.artifactTool = artifactTool;
for (const [name, value] of Object.entries(artifactTool)) {
if (name === "default" || Object.prototype.hasOwnProperty.call(globalThis, name)) {
continue;
}
globalThis[name] = value;
}
"#,
);
wrapped.push_str(source);
wrapped.push('\n');
wrapped
}
async fn run_command(
-7
View File
@@ -5,12 +5,8 @@ mod tests;
pub use client::ArtifactBuildRequest;
pub use client::ArtifactCommandOutput;
pub use client::ArtifactRenderCommandRequest;
pub use client::ArtifactRenderTarget;
pub use client::ArtifactsClient;
pub use client::ArtifactsError;
pub use client::PresentationRenderTarget;
pub use client::SpreadsheetRenderTarget;
pub use runtime::ArtifactRuntimeError;
pub use runtime::ArtifactRuntimeManager;
pub use runtime::ArtifactRuntimeManagerConfig;
@@ -19,13 +15,10 @@ pub use runtime::ArtifactRuntimeReleaseLocator;
pub use runtime::DEFAULT_CACHE_ROOT_RELATIVE;
pub use runtime::DEFAULT_RELEASE_BASE_URL;
pub use runtime::DEFAULT_RELEASE_TAG_PREFIX;
pub use runtime::ExtractedRuntimeManifest;
pub use runtime::InstalledArtifactRuntime;
pub use runtime::JsRuntime;
pub use runtime::JsRuntimeKind;
pub use runtime::ReleaseManifest;
pub use runtime::RuntimeEntrypoints;
pub use runtime::RuntimePathEntry;
pub use runtime::can_manage_artifact_runtime;
pub use runtime::is_js_runtime_available;
pub use runtime::load_cached_runtime;
+2 -2
View File
@@ -13,8 +13,8 @@ pub enum ArtifactRuntimeError {
#[source]
source: std::io::Error,
},
#[error("invalid manifest at {path}")]
InvalidManifest {
#[error("invalid package metadata at {path}")]
InvalidPackageMetadata {
path: PathBuf,
#[source]
source: serde_json::Error,
+134 -51
View File
@@ -1,15 +1,17 @@
use super::ArtifactRuntimeError;
use super::ArtifactRuntimePlatform;
use super::ExtractedRuntimeManifest;
use super::JsRuntime;
use super::codex_app_runtime_candidates;
use super::resolve_js_runtime_from_candidates;
use super::system_electron_runtime;
use super::system_node_runtime;
use std::collections::BTreeMap;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
const ARTIFACT_TOOL_PACKAGE_NAME: &str = "@oai/artifact-tool";
/// Loads a previously installed runtime from a caller-provided cache root.
pub fn load_cached_runtime(
cache_root: &Path,
@@ -36,10 +38,7 @@ pub struct InstalledArtifactRuntime {
root_dir: PathBuf,
runtime_version: String,
platform: ArtifactRuntimePlatform,
manifest: ExtractedRuntimeManifest,
node_path: PathBuf,
build_js_path: PathBuf,
render_cli_path: PathBuf,
}
impl InstalledArtifactRuntime {
@@ -48,19 +47,13 @@ impl InstalledArtifactRuntime {
root_dir: PathBuf,
runtime_version: String,
platform: ArtifactRuntimePlatform,
manifest: ExtractedRuntimeManifest,
node_path: PathBuf,
build_js_path: PathBuf,
render_cli_path: PathBuf,
) -> Self {
Self {
root_dir,
runtime_version,
platform,
manifest,
node_path,
build_js_path,
render_cli_path,
}
}
@@ -69,35 +62,16 @@ impl InstalledArtifactRuntime {
root_dir: PathBuf,
platform: ArtifactRuntimePlatform,
) -> Result<Self, ArtifactRuntimeError> {
let manifest_path = root_dir.join("manifest.json");
let manifest_bytes =
std::fs::read(&manifest_path).map_err(|source| ArtifactRuntimeError::Io {
context: format!("failed to read {}", manifest_path.display()),
source,
})?;
let manifest = serde_json::from_slice::<ExtractedRuntimeManifest>(&manifest_bytes)
.map_err(|source| ArtifactRuntimeError::InvalidManifest {
path: manifest_path,
source,
})?;
let node_path = resolve_relative_runtime_path(&root_dir, &manifest.node.relative_path)?;
let package_metadata = load_package_metadata(&root_dir)?;
let build_js_path =
resolve_relative_runtime_path(&root_dir, &manifest.entrypoints.build_js.relative_path)?;
let render_cli_path = resolve_relative_runtime_path(
&root_dir,
&manifest.entrypoints.render_cli.relative_path,
)?;
resolve_relative_runtime_path(&root_dir, &package_metadata.build_js_relative_path)?;
verify_required_runtime_path(&build_js_path)?;
verify_required_runtime_path(&render_cli_path)?;
Ok(Self::new(
root_dir,
manifest.runtime_version.clone(),
package_metadata.version,
platform,
manifest,
node_path,
build_js_path,
render_cli_path,
))
}
@@ -106,7 +80,7 @@ impl InstalledArtifactRuntime {
&self.root_dir
}
/// Returns the runtime version recorded in the extracted manifest.
/// Returns the runtime version recorded in `package.json`.
pub fn runtime_version(&self) -> &str {
&self.runtime_version
}
@@ -116,33 +90,17 @@ impl InstalledArtifactRuntime {
self.platform
}
/// Returns the parsed extracted-runtime manifest.
pub fn manifest(&self) -> &ExtractedRuntimeManifest {
&self.manifest
}
/// Returns the bundled Node executable path advertised by the runtime manifest.
pub fn node_path(&self) -> &Path {
&self.node_path
}
/// Returns the artifact build entrypoint path.
pub fn build_js_path(&self) -> &Path {
&self.build_js_path
}
/// Returns the artifact render CLI entrypoint path.
pub fn render_cli_path(&self) -> &Path {
&self.render_cli_path
}
/// Resolves the best executable to use for artifact commands.
///
/// Preference order is the bundled Node path, then a machine Node install,
/// then Electron from the machine or a Codex desktop app bundle.
/// Preference order is a machine Node install, then Electron from the
/// machine or a Codex desktop app bundle.
pub fn resolve_js_runtime(&self) -> Result<JsRuntime, ArtifactRuntimeError> {
resolve_js_runtime_from_candidates(
Some(self.node_path()),
system_node_runtime(),
system_electron_runtime(),
codex_app_runtime_candidates(),
@@ -198,3 +156,128 @@ fn verify_required_runtime_path(path: &Path) -> Result<(), ArtifactRuntimeError>
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing runtime file"),
})
}
pub(crate) fn detect_runtime_root(extraction_root: &Path) -> Result<PathBuf, ArtifactRuntimeError> {
if is_runtime_root(extraction_root) {
return Ok(extraction_root.to_path_buf());
}
let mut directory_candidates = Vec::new();
for entry in std::fs::read_dir(extraction_root).map_err(|source| ArtifactRuntimeError::Io {
context: format!("failed to read {}", extraction_root.display()),
source,
})? {
let entry = entry.map_err(|source| ArtifactRuntimeError::Io {
context: format!("failed to read entry in {}", extraction_root.display()),
source,
})?;
let path = entry.path();
if path.is_dir() {
directory_candidates.push(path);
}
}
if directory_candidates.len() == 1 {
let candidate = &directory_candidates[0];
if is_runtime_root(candidate) {
return Ok(candidate.clone());
}
}
Err(ArtifactRuntimeError::Io {
context: format!(
"failed to detect artifact runtime root under {}",
extraction_root.display()
),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
"missing artifact runtime root",
),
})
}
fn is_runtime_root(root_dir: &Path) -> bool {
let Ok(package_metadata) = load_package_metadata(root_dir) else {
return false;
};
let Ok(build_js_path) =
resolve_relative_runtime_path(root_dir, &package_metadata.build_js_relative_path)
else {
return false;
};
build_js_path.is_file()
}
struct PackageMetadata {
version: String,
build_js_relative_path: String,
}
fn load_package_metadata(root_dir: &Path) -> Result<PackageMetadata, ArtifactRuntimeError> {
#[derive(serde::Deserialize)]
struct PackageJson {
name: String,
version: String,
exports: PackageExports,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum PackageExports {
Main(String),
Map(BTreeMap<String, String>),
}
impl PackageExports {
fn build_entrypoint(&self) -> Option<&str> {
match self {
Self::Main(path) => Some(path),
Self::Map(exports) => exports.get(".").map(String::as_str),
}
}
}
let package_json_path = root_dir.join("package.json");
let package_json_bytes =
std::fs::read(&package_json_path).map_err(|source| ArtifactRuntimeError::Io {
context: format!("failed to read {}", package_json_path.display()),
source,
})?;
let package_json =
serde_json::from_slice::<PackageJson>(&package_json_bytes).map_err(|source| {
ArtifactRuntimeError::InvalidPackageMetadata {
path: package_json_path.clone(),
source,
}
})?;
if package_json.name != ARTIFACT_TOOL_PACKAGE_NAME {
return Err(ArtifactRuntimeError::Io {
context: format!(
"unsupported artifact runtime package at {}; expected name `{ARTIFACT_TOOL_PACKAGE_NAME}`, got `{}`",
package_json_path.display(),
package_json.name
),
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
"unsupported package name",
),
});
}
let Some(build_js_relative_path) = package_json.exports.build_entrypoint() else {
return Err(ArtifactRuntimeError::Io {
context: format!(
"unsupported artifact runtime package at {}; expected `exports[\".\"]` to point at the JS entrypoint",
package_json_path.display()
),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, "missing package export"),
});
};
Ok(PackageMetadata {
version: package_json.version,
build_js_relative_path: build_js_relative_path.trim_start_matches("./").to_string(),
})
}
+5 -11
View File
@@ -74,7 +74,6 @@ pub fn can_manage_artifact_runtime() -> bool {
pub(crate) fn resolve_machine_js_runtime() -> Option<JsRuntime> {
resolve_js_runtime_from_candidates(
/*preferred_node_path*/ None,
system_node_runtime(),
system_electron_runtime(),
codex_app_runtime_candidates(),
@@ -82,20 +81,15 @@ pub(crate) fn resolve_machine_js_runtime() -> Option<JsRuntime> {
}
pub(crate) fn resolve_js_runtime_from_candidates(
preferred_node_path: Option<&Path>,
node_runtime: Option<JsRuntime>,
electron_runtime: Option<JsRuntime>,
codex_app_candidates: Vec<PathBuf>,
) -> Option<JsRuntime> {
preferred_node_path
.and_then(node_runtime_from_path)
.or(node_runtime)
.or(electron_runtime)
.or_else(|| {
codex_app_candidates
.into_iter()
.find_map(|candidate| electron_runtime_from_path(&candidate))
})
node_runtime.or(electron_runtime).or_else(|| {
codex_app_candidates
.into_iter()
.find_map(|candidate| electron_runtime_from_path(&candidate))
})
}
pub(crate) fn system_node_runtime() -> Option<JsRuntime> {
+8 -6
View File
@@ -2,6 +2,7 @@ use super::ArtifactRuntimeError;
use super::ArtifactRuntimePlatform;
use super::InstalledArtifactRuntime;
use super::ReleaseManifest;
use super::detect_runtime_root;
use codex_package_manager::ManagedPackage;
use codex_package_manager::PackageManager;
use codex_package_manager::PackageManagerConfig;
@@ -79,12 +80,9 @@ impl ArtifactRuntimeReleaseLocator {
/// Returns the default GitHub-release locator for a runtime version.
pub fn default(runtime_version: impl Into<String>) -> Self {
Self::new(
match Url::parse(DEFAULT_RELEASE_BASE_URL) {
Ok(url) => url,
Err(error) => {
panic!("hard-coded artifact runtime release base URL must be valid: {error}")
}
},
Url::parse(DEFAULT_RELEASE_BASE_URL).unwrap_or_else(|error| {
panic!("hard-coded artifact runtime release base URL must be valid: {error}")
}),
runtime_version,
)
}
@@ -250,4 +248,8 @@ impl ManagedPackage for ArtifactRuntimePackage {
) -> Result<Self::Installed, Self::Error> {
InstalledArtifactRuntime::load(root_dir, platform)
}
fn detect_extracted_root(&self, extraction_root: &Path) -> Result<PathBuf, Self::Error> {
detect_runtime_root(extraction_root)
}
}
@@ -13,25 +13,3 @@ pub struct ReleaseManifest {
pub node_version: Option<String>,
pub platforms: BTreeMap<String, PackageReleaseArchive>,
}
/// Manifest shipped inside the extracted runtime payload.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct ExtractedRuntimeManifest {
pub schema_version: u32,
pub runtime_version: String,
pub node: RuntimePathEntry,
pub entrypoints: RuntimeEntrypoints,
}
/// A relative path entry inside an extracted runtime manifest.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct RuntimePathEntry {
pub relative_path: String,
}
/// Entrypoints required to build and render artifacts.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct RuntimeEntrypoints {
pub build_js: RuntimePathEntry,
pub render_cli: RuntimePathEntry,
}
+1 -3
View File
@@ -18,12 +18,10 @@ pub use manager::ArtifactRuntimeReleaseLocator;
pub use manager::DEFAULT_CACHE_ROOT_RELATIVE;
pub use manager::DEFAULT_RELEASE_BASE_URL;
pub use manager::DEFAULT_RELEASE_TAG_PREFIX;
pub use manifest::ExtractedRuntimeManifest;
pub use manifest::ReleaseManifest;
pub use manifest::RuntimeEntrypoints;
pub use manifest::RuntimePathEntry;
pub(crate) use installed::default_cached_runtime_root;
pub(crate) use installed::detect_runtime_root;
pub(crate) use js_runtime::codex_app_runtime_candidates;
pub(crate) use js_runtime::resolve_js_runtime_from_candidates;
pub(crate) use js_runtime::system_electron_runtime;
+210 -278
View File
@@ -1,24 +1,17 @@
use crate::ArtifactBuildRequest;
use crate::ArtifactCommandOutput;
use crate::ArtifactRenderCommandRequest;
use crate::ArtifactRenderTarget;
use crate::ArtifactRuntimeManager;
use crate::ArtifactRuntimeManagerConfig;
use crate::ArtifactRuntimePlatform;
use crate::ArtifactRuntimeReleaseLocator;
use crate::ArtifactsClient;
use crate::DEFAULT_CACHE_ROOT_RELATIVE;
use crate::ExtractedRuntimeManifest;
use crate::InstalledArtifactRuntime;
use crate::JsRuntime;
use crate::PresentationRenderTarget;
use crate::ReleaseManifest;
use crate::RuntimeEntrypoints;
use crate::RuntimePathEntry;
use crate::SpreadsheetRenderTarget;
use crate::load_cached_runtime;
use codex_package_manager::ArchiveFormat;
use codex_package_manager::PackageReleaseArchive;
use flate2::Compression;
use flate2::write::GzEncoder;
use pretty_assertions::assert_eq;
use sha2::Digest;
use sha2::Sha256;
@@ -26,11 +19,9 @@ use std::collections::BTreeMap;
use std::fs;
use std::io::Cursor;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use tar::Builder as TarBuilder;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
@@ -71,7 +62,7 @@ fn default_release_locator_uses_openai_codex_github_releases() {
#[test]
fn load_cached_runtime_reads_installed_runtime() {
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let runtime_version = "0.1.0";
let runtime_version = "2.5.6";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let install_dir = codex_home
@@ -79,11 +70,7 @@ fn load_cached_runtime_reads_installed_runtime() {
.join(DEFAULT_CACHE_ROOT_RELATIVE)
.join(runtime_version)
.join(platform.as_str());
write_installed_runtime(
&install_dir,
runtime_version,
Some(PathBuf::from("node/bin/node")),
);
write_installed_runtime(&install_dir, runtime_version);
let runtime = load_cached_runtime(
&codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE),
@@ -93,47 +80,17 @@ fn load_cached_runtime_reads_installed_runtime() {
assert_eq!(runtime.runtime_version(), runtime_version);
assert_eq!(runtime.platform(), platform);
assert!(runtime.node_path().ends_with(Path::new("node/bin/node")));
assert!(
runtime
.build_js_path()
.ends_with(Path::new("artifact-tool/dist/artifact_tool.mjs"))
);
}
#[test]
fn load_cached_runtime_rejects_parent_relative_paths() {
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let runtime_version = "0.1.0";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let install_dir = codex_home
.path()
.join(DEFAULT_CACHE_ROOT_RELATIVE)
.join(runtime_version)
.join(platform.as_str());
write_installed_runtime(
&install_dir,
runtime_version,
Some(PathBuf::from("../node/bin/node")),
);
let error = load_cached_runtime(
&codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE),
runtime_version,
)
.unwrap_err();
assert_eq!(
error.to_string(),
"runtime path `../node/bin/node` is invalid"
.ends_with(Path::new("dist/artifact_tool.mjs"))
);
}
#[test]
fn load_cached_runtime_requires_build_entrypoint() {
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let runtime_version = "0.1.0";
let runtime_version = "2.5.6";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let install_dir = codex_home
@@ -141,12 +98,8 @@ fn load_cached_runtime_requires_build_entrypoint() {
.join(DEFAULT_CACHE_ROOT_RELATIVE)
.join(runtime_version)
.join(platform.as_str());
write_installed_runtime(
&install_dir,
runtime_version,
Some(PathBuf::from("node/bin/node")),
);
fs::remove_file(install_dir.join("artifact-tool/dist/artifact_tool.mjs"))
write_installed_runtime(&install_dir, runtime_version);
fs::remove_file(install_dir.join("dist/artifact_tool.mjs"))
.unwrap_or_else(|error| panic!("{error}"));
let error = load_cached_runtime(
@@ -159,9 +112,7 @@ fn load_cached_runtime_requires_build_entrypoint() {
error.to_string(),
format!(
"required runtime file is missing: {}",
install_dir
.join("artifact-tool/dist/artifact_tool.mjs")
.display()
install_dir.join("dist/artifact_tool.mjs").display()
)
);
}
@@ -169,7 +120,7 @@ fn load_cached_runtime_requires_build_entrypoint() {
#[tokio::test]
async fn ensure_installed_downloads_and_extracts_zip_runtime() {
let server = MockServer::start().await;
let runtime_version = "0.1.0";
let runtime_version = "2.5.6";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let archive_name = format!(
@@ -182,7 +133,7 @@ async fn ensure_installed_downloads_and_extracts_zip_runtime() {
schema_version: 1,
runtime_version: runtime_version.to_string(),
release_tag: format!("artifact-runtime-v{runtime_version}"),
node_version: Some("22.0.0".to_string()),
node_version: None,
platforms: BTreeMap::from([(
platform.as_str().to_string(),
PackageReleaseArchive {
@@ -225,28 +176,128 @@ async fn ensure_installed_downloads_and_extracts_zip_runtime() {
assert_eq!(runtime.runtime_version(), runtime_version);
assert_eq!(runtime.platform(), platform);
assert!(runtime.node_path().ends_with(Path::new("node/bin/node")));
assert!(
runtime
.build_js_path()
.ends_with(Path::new("dist/artifact_tool.mjs"))
);
}
#[test]
fn load_cached_runtime_requires_package_export() {
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let runtime_version = "2.5.6";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let install_dir = codex_home
.path()
.join(DEFAULT_CACHE_ROOT_RELATIVE)
.join(runtime_version)
.join(platform.as_str());
write_installed_runtime(&install_dir, runtime_version);
fs::write(
install_dir.join("package.json"),
serde_json::json!({
"name": "@oai/artifact-tool",
"version": runtime_version,
"type": "module",
})
.to_string(),
)
.unwrap_or_else(|error| panic!("{error}"));
let error = load_cached_runtime(
&codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE),
runtime_version,
)
.unwrap_err();
assert_eq!(
runtime.resolve_js_runtime().expect("resolve js runtime"),
JsRuntime::node(runtime.node_path().to_path_buf())
error.to_string(),
format!(
"invalid package metadata at {}",
install_dir.join("package.json").display()
)
);
}
#[tokio::test]
async fn ensure_installed_downloads_and_extracts_tar_gz_runtime() {
let server = MockServer::start().await;
let runtime_version = "2.5.6";
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let archive_name = format!(
"artifact-runtime-v{runtime_version}-{}.tar.gz",
platform.as_str()
);
let archive_bytes = build_tar_gz_archive(runtime_version);
let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes));
let manifest = ReleaseManifest {
schema_version: 1,
runtime_version: runtime_version.to_string(),
release_tag: format!("artifact-runtime-v{runtime_version}"),
node_version: None,
platforms: BTreeMap::from([(
platform.as_str().to_string(),
PackageReleaseArchive {
archive: archive_name.clone(),
sha256: archive_sha,
format: ArchiveFormat::TarGz,
size_bytes: Some(archive_bytes.len() as u64),
},
)]),
};
Mock::given(method("GET"))
.and(path(format!(
"/artifact-runtime-v{runtime_version}/artifact-runtime-v{runtime_version}-manifest.json"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(&manifest))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!(
"/artifact-runtime-v{runtime_version}/{archive_name}"
)))
.respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes))
.mount(&server)
.await;
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let locator = ArtifactRuntimeReleaseLocator::new(
url::Url::parse(&format!("{}/", server.uri())).unwrap_or_else(|error| panic!("{error}")),
runtime_version,
);
let manager = ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::new(
codex_home.path().to_path_buf(),
locator,
));
let runtime = manager
.ensure_installed()
.await
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(runtime.runtime_version(), runtime_version);
assert_eq!(runtime.platform(), platform);
assert!(
runtime
.build_js_path()
.ends_with(Path::new("dist/artifact_tool.mjs"))
);
}
#[test]
fn load_cached_runtime_uses_custom_cache_root() {
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let runtime_version = "0.1.0";
let runtime_version = "2.5.6";
let custom_cache_root = codex_home.path().join("runtime-cache");
let platform =
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let install_dir = custom_cache_root
.join(runtime_version)
.join(platform.as_str());
write_installed_runtime(
&install_dir,
runtime_version,
Some(PathBuf::from("node/bin/node")),
);
write_installed_runtime(&install_dir, runtime_version);
let config = ArtifactRuntimeManagerConfig::with_default_release(
codex_home.path().to_path_buf(),
@@ -265,102 +316,38 @@ fn load_cached_runtime_uses_custom_cache_root() {
#[cfg(unix)]
async fn artifacts_client_execute_build_writes_wrapped_script_and_env() {
let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let output_path = temp.path().join("build-output.txt");
let wrapped_script_path = temp.path().join("wrapped-script.mjs");
let runtime = fake_installed_runtime(temp.path(), &output_path, &wrapped_script_path);
let runtime_root = temp.path().join("runtime");
write_installed_runtime(&runtime_root, "2.5.6");
let runtime = crate::InstalledArtifactRuntime::load(
runtime_root,
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")),
)
.unwrap_or_else(|error| panic!("{error}"));
let client = ArtifactsClient::from_installed_runtime(runtime);
let output = client
.execute_build(ArtifactBuildRequest {
source: "console.log('hello');".to_string(),
source: concat!(
"console.log(typeof artifacts);\n",
"console.log(typeof codexArtifacts);\n",
"console.log(artifactTool.ok);\n",
"console.log(ok);\n",
"console.error('stderr-ok');\n",
"console.log('stdout-ok');\n"
)
.to_string(),
cwd: temp.path().to_path_buf(),
timeout: Some(Duration::from_secs(5)),
env: BTreeMap::from([
(
"CODEX_TEST_OUTPUT".to_string(),
output_path.display().to_string(),
),
("CUSTOM_ENV".to_string(), "custom-value".to_string()),
]),
env: BTreeMap::new(),
})
.await
.unwrap_or_else(|error| panic!("{error}"));
assert_success(&output);
let command_log = fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("{error}"));
assert!(command_log.contains("arg0="));
assert!(command_log.contains("CODEX_ARTIFACT_BUILD_ENTRYPOINT="));
assert!(command_log.contains("CODEX_ARTIFACT_RENDER_ENTRYPOINT="));
assert!(command_log.contains("CUSTOM_ENV=custom-value"));
let wrapped_script =
fs::read_to_string(wrapped_script_path).unwrap_or_else(|error| panic!("{error}"));
assert!(wrapped_script.contains("globalThis.artifacts = artifactTool;"));
assert!(wrapped_script.contains("globalThis.codexArtifacts = artifactTool;"));
assert!(wrapped_script.contains("console.log('hello');"));
}
#[tokio::test]
#[cfg(unix)]
async fn artifacts_client_execute_render_passes_expected_args() {
let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let output_path = temp.path().join("render-output.txt");
let wrapped_script_path = temp.path().join("unused-script-copy.mjs");
let runtime = fake_installed_runtime(temp.path(), &output_path, &wrapped_script_path);
let client = ArtifactsClient::from_installed_runtime(runtime.clone());
let render_output = temp.path().join("slide.png");
let output = client
.execute_render(ArtifactRenderCommandRequest {
cwd: temp.path().to_path_buf(),
timeout: Some(Duration::from_secs(5)),
env: BTreeMap::from([(
"CODEX_TEST_OUTPUT".to_string(),
output_path.display().to_string(),
)]),
target: ArtifactRenderTarget::Presentation(PresentationRenderTarget {
input_path: temp.path().join("deck.pptx"),
output_path: render_output.clone(),
slide_number: 3,
}),
})
.await
.unwrap_or_else(|error| panic!("{error}"));
assert_success(&output);
let command_log = fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("{error}"));
assert!(command_log.contains(&format!("arg0={}", runtime.render_cli_path().display())));
assert!(command_log.contains("arg1=pptx"));
assert!(command_log.contains("arg2=render"));
assert!(command_log.contains("arg5=--slide"));
assert!(command_log.contains("arg6=3"));
assert!(command_log.contains("arg7=--out"));
assert!(command_log.contains(&format!("arg8={}", render_output.display())));
}
#[test]
fn spreadsheet_render_target_to_args_includes_optional_range() {
let target = ArtifactRenderTarget::Spreadsheet(SpreadsheetRenderTarget {
input_path: PathBuf::from("/tmp/input.xlsx"),
output_path: PathBuf::from("/tmp/output.png"),
sheet_name: "Summary".to_string(),
range: Some("A1:C8".to_string()),
});
assert_eq!(output.stderr.trim(), "stderr-ok");
assert_eq!(
target.to_args(),
vec![
"xlsx".to_string(),
"render".to_string(),
"--in".to_string(),
"/tmp/input.xlsx".to_string(),
"--sheet".to_string(),
"Summary".to_string(),
"--out".to_string(),
"/tmp/output.png".to_string(),
"--range".to_string(),
"A1:C8".to_string(),
]
output.stdout.lines().collect::<Vec<_>>(),
vec!["undefined", "undefined", "true", "true", "stdout-ok"]
);
}
@@ -369,94 +356,26 @@ fn assert_success(output: &ArtifactCommandOutput) {
assert_eq!(output.exit_code, Some(0));
}
#[cfg(unix)]
fn fake_installed_runtime(
root: &Path,
output_path: &Path,
wrapped_script_path: &Path,
) -> InstalledArtifactRuntime {
let runtime_root = root.join("runtime");
write_installed_runtime(&runtime_root, "0.1.0", Some(PathBuf::from("node/bin/node")));
write_fake_node_script(
&runtime_root.join("node/bin/node"),
output_path,
wrapped_script_path,
);
InstalledArtifactRuntime::load(
runtime_root,
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")),
)
.unwrap_or_else(|error| panic!("{error}"))
}
fn write_installed_runtime(
install_dir: &Path,
runtime_version: &str,
node_relative: Option<PathBuf>,
) {
fs::create_dir_all(install_dir.join("node/bin")).unwrap_or_else(|error| panic!("{error}"));
fs::create_dir_all(install_dir.join("artifact-tool/dist"))
.unwrap_or_else(|error| panic!("{error}"));
fs::create_dir_all(install_dir.join("granola-render/dist"))
.unwrap_or_else(|error| panic!("{error}"));
let node_relative = node_relative.unwrap_or_else(|| PathBuf::from("node/bin/node"));
fn write_installed_runtime(install_dir: &Path, runtime_version: &str) {
fs::create_dir_all(install_dir.join("dist")).unwrap_or_else(|error| panic!("{error}"));
fs::write(
install_dir.join("manifest.json"),
serde_json::json!(sample_extracted_manifest(runtime_version, node_relative)).to_string(),
install_dir.join("package.json"),
serde_json::json!({
"name": "@oai/artifact-tool",
"version": runtime_version,
"type": "module",
"exports": {
".": "./dist/artifact_tool.mjs",
}
})
.to_string(),
)
.unwrap_or_else(|error| panic!("{error}"));
fs::write(install_dir.join("node/bin/node"), "#!/bin/sh\n")
.unwrap_or_else(|error| panic!("{error}"));
fs::write(
install_dir.join("artifact-tool/dist/artifact_tool.mjs"),
install_dir.join("dist/artifact_tool.mjs"),
"export const ok = true;\n",
)
.unwrap_or_else(|error| panic!("{error}"));
fs::write(
install_dir.join("granola-render/dist/render_cli.mjs"),
"export const ok = true;\n",
)
.unwrap_or_else(|error| panic!("{error}"));
}
#[cfg(unix)]
fn write_fake_node_script(script_path: &Path, output_path: &Path, wrapped_script_path: &Path) {
fs::write(
script_path,
format!(
concat!(
"#!/bin/sh\n",
"printf 'arg0=%s\\n' \"$1\" > \"{}\"\n",
"cp \"$1\" \"{}\"\n",
"shift\n",
"i=1\n",
"for arg in \"$@\"; do\n",
" printf 'arg%s=%s\\n' \"$i\" \"$arg\" >> \"{}\"\n",
" i=$((i + 1))\n",
"done\n",
"printf 'CODEX_ARTIFACT_BUILD_ENTRYPOINT=%s\\n' \"$CODEX_ARTIFACT_BUILD_ENTRYPOINT\" >> \"{}\"\n",
"printf 'CODEX_ARTIFACT_RENDER_ENTRYPOINT=%s\\n' \"$CODEX_ARTIFACT_RENDER_ENTRYPOINT\" >> \"{}\"\n",
"printf 'CUSTOM_ENV=%s\\n' \"$CUSTOM_ENV\" >> \"{}\"\n",
"echo stdout-ok\n",
"echo stderr-ok >&2\n"
),
output_path.display(),
wrapped_script_path.display(),
output_path.display(),
output_path.display(),
output_path.display(),
output_path.display(),
),
)
.unwrap_or_else(|error| panic!("{error}"));
#[cfg(unix)]
{
let mut permissions = fs::metadata(script_path)
.unwrap_or_else(|error| panic!("{error}"))
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(script_path, permissions).unwrap_or_else(|error| panic!("{error}"));
}
}
fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
@@ -464,34 +383,22 @@ fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
{
let mut zip = ZipWriter::new(&mut bytes);
let options = SimpleFileOptions::default();
let manifest = serde_json::to_vec(&sample_extracted_manifest(
runtime_version,
PathBuf::from("node/bin/node"),
))
.unwrap_or_else(|error| panic!("{error}"));
zip.start_file("artifact-runtime/manifest.json", options)
let package_json = serde_json::json!({
"name": "@oai/artifact-tool",
"version": runtime_version,
"type": "module",
"exports": {
".": "./dist/artifact_tool.mjs",
}
})
.to_string()
.into_bytes();
zip.start_file("artifact-runtime/package.json", options)
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(&manifest)
zip.write_all(&package_json)
.unwrap_or_else(|error| panic!("{error}"));
zip.start_file(
"artifact-runtime/node/bin/node",
options.unix_permissions(0o755),
)
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(b"#!/bin/sh\n")
zip.start_file("artifact-runtime/dist/artifact_tool.mjs", options)
.unwrap_or_else(|error| panic!("{error}"));
zip.start_file(
"artifact-runtime/artifact-tool/dist/artifact_tool.mjs",
options,
)
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(b"export const ok = true;\n")
.unwrap_or_else(|error| panic!("{error}"));
zip.start_file(
"artifact-runtime/granola-render/dist/render_cli.mjs",
options,
)
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(b"export const ok = true;\n")
.unwrap_or_else(|error| panic!("{error}"));
zip.finish().unwrap_or_else(|error| panic!("{error}"));
@@ -499,23 +406,48 @@ fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
bytes.into_inner()
}
fn sample_extracted_manifest(
runtime_version: &str,
node_relative: PathBuf,
) -> ExtractedRuntimeManifest {
ExtractedRuntimeManifest {
schema_version: 1,
runtime_version: runtime_version.to_string(),
node: RuntimePathEntry {
relative_path: node_relative.display().to_string(),
},
entrypoints: RuntimeEntrypoints {
build_js: RuntimePathEntry {
relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(),
},
render_cli: RuntimePathEntry {
relative_path: "granola-render/dist/render_cli.mjs".to_string(),
},
},
fn build_tar_gz_archive(runtime_version: &str) -> Vec<u8> {
let mut bytes = Vec::new();
{
let encoder = GzEncoder::new(&mut bytes, Compression::default());
let mut archive = TarBuilder::new(encoder);
let package_json = serde_json::json!({
"name": "@oai/artifact-tool",
"version": runtime_version,
"type": "module",
"exports": {
".": "./dist/artifact_tool.mjs",
}
})
.to_string()
.into_bytes();
let mut package_header = tar::Header::new_gnu();
package_header.set_mode(0o644);
package_header.set_size(package_json.len() as u64);
package_header.set_cksum();
archive
.append_data(
&mut package_header,
"package/package.json",
package_json.as_slice(),
)
.unwrap_or_else(|error| panic!("{error}"));
let build_js = b"export const ok = true;\n";
let mut build_header = tar::Header::new_gnu();
build_header.set_mode(0o644);
build_header.set_size(build_js.len() as u64);
build_header.set_cksum();
archive
.append_data(
&mut build_header,
"package/dist/artifact_tool.mjs",
&build_js[..],
)
.unwrap_or_else(|error| panic!("{error}"));
archive.finish().unwrap_or_else(|error| panic!("{error}"));
}
bytes
}
+1
View File
@@ -40,6 +40,7 @@ codex-rmcp-client = { workspace = true }
codex-shell-escalation = { workspace = true }
codex-state = { workspace = true }
codex-stdio-to-uds = { workspace = true }
codex-terminal-detection = { workspace = true }
codex-tui = { workspace = true }
codex-tui-app-server = { workspace = true }
codex-utils-absolute-path = { workspace = true }
+1 -1
View File
@@ -328,7 +328,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
std::process::exit(1);
}
},
AuthMode::Chatgpt => {
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => {
eprintln!("Logged in using ChatGPT");
std::process::exit(0);
}
+3 -2
View File
@@ -50,7 +50,7 @@ use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::find_codex_home;
use codex_core::features::Stage;
use codex_core::features::is_known_feature_key;
use codex_core::terminal::TerminalName;
use codex_terminal_detection::TerminalName;
/// Codex CLI
///
@@ -649,6 +649,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
codex_core::config_loader::LoaderOverrides::default(),
app_server_cli.analytics_default_enabled,
transport,
codex_protocol::protocol::SessionSource::VSCode,
)
.await?;
}
@@ -1049,7 +1050,7 @@ async fn run_interactive_tui(
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
}
let terminal_info = codex_core::terminal::terminal_info();
let terminal_info = codex_terminal_detection::terminal_info();
if terminal_info.name == TerminalName::Dumb {
if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
return Ok(AppExitInfo::fatal(
+14
View File
@@ -1122,6 +1122,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1166,6 +1167,7 @@ mod tests {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1246,6 +1248,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1297,6 +1300,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1348,6 +1352,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1509,6 +1514,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1538,6 +1544,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1587,6 +1594,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1635,6 +1643,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1687,6 +1696,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1740,6 +1750,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1793,6 +1804,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1879,6 +1891,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
@@ -1904,6 +1917,7 @@ enabled = false
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
guardian_developer_instructions: None,
feature_requirements: None,
mcp_servers: None,
apps: None,
+26
View File
@@ -5,6 +5,7 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::W3cTraceContext;
use futures::Stream;
use serde::Deserialize;
use serde::Serialize;
@@ -15,6 +16,9 @@ use std::task::Context;
use std::task::Poll;
use tokio::sync::mpsc;
pub const WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY: &str = "ws_request_header_traceparent";
pub const WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY: &str = "ws_request_header_tracestate";
/// Canonical input payload for the compaction endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct CompactionInput<'a> {
@@ -215,6 +219,28 @@ pub struct ResponseCreateWsRequest {
pub client_metadata: Option<HashMap<String, String>>,
}
pub fn response_create_client_metadata(
client_metadata: Option<HashMap<String, String>>,
trace: Option<&W3cTraceContext>,
) -> Option<HashMap<String, String>> {
let mut client_metadata = client_metadata.unwrap_or_default();
if let Some(traceparent) = trace.and_then(|trace| trace.traceparent.as_deref()) {
client_metadata.insert(
WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY.to_string(),
traceparent.to_string(),
);
}
if let Some(tracestate) = trace.and_then(|trace| trace.tracestate.as_deref()) {
client_metadata.insert(
WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY.to_string(),
tracestate.to_string(),
);
}
(!client_metadata.is_empty()).then_some(client_metadata)
}
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
@@ -408,7 +408,9 @@ impl RealtimeWebsocketEvents {
append_transcript_delta(&mut active_transcript.entries, "assistant", delta);
}
RealtimeEvent::HandoffRequested(handoff) => {
handoff.active_transcript = std::mem::take(&mut active_transcript.entries);
if self.event_parser == RealtimeEventParser::V1 {
handoff.active_transcript = std::mem::take(&mut active_transcript.entries);
}
}
RealtimeEvent::SessionUpdated { .. }
| RealtimeEvent::AudioOut(_)
@@ -1173,7 +1175,10 @@ mod tests {
let fourth_json: Value = serde_json::from_str(&fourth).expect("json");
assert_eq!(fourth_json["type"], "conversation.handoff.append");
assert_eq!(fourth_json["handoff_id"], "handoff_1");
assert_eq!(fourth_json["output_text"], "hello from codex");
assert_eq!(
fourth_json["output_text"],
"\"Agent Final Message\":\n\nhello from codex"
);
ws.send(Message::Text(
json!({
@@ -1504,7 +1509,7 @@ mod tests {
);
assert_eq!(
third_json["item"]["output"],
Value::String("delegated result".to_string())
Value::String("\"Agent Final Message\":\n\ndelegated result".to_string())
);
});

Some files were not shown because too many files have changed in this diff Show More