mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge branch 'main' into jif/artifact-sandboxing
This commit is contained in:
Generated
+1
-1
@@ -1469,6 +1469,7 @@ dependencies = [
|
||||
"codex-utils-cargo-bin",
|
||||
"inventory",
|
||||
"pretty_assertions",
|
||||
"rmcp",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1986,7 +1987,6 @@ dependencies = [
|
||||
"sentry",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -24,6 +24,12 @@ serde_with = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"base64",
|
||||
"macros",
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
ts-rs = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -953,25 +953,34 @@
|
||||
},
|
||||
"PluginInstallParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListParams": {
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces are considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProductSurface": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
@@ -3264,6 +3273,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/list"
|
||||
],
|
||||
"title": "Plugin/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
+37
-39
@@ -31,38 +31,24 @@
|
||||
"AdditionalMacOsPermissions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsAutomationValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
},
|
||||
"calendar": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPreferencesValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessibility",
|
||||
"automations",
|
||||
"calendar",
|
||||
"preferences"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
@@ -300,28 +286,40 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsAutomationValue": {
|
||||
"anyOf": [
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsPreferencesValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
|
||||
@@ -544,6 +544,58 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ElicitationRequest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"form"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"requested_schema": true
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"mode",
|
||||
"requested_schema"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"elicitation_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"url"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"elicitation_id",
|
||||
"message",
|
||||
"mode",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"EventMsg": {
|
||||
"description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.",
|
||||
"oneOf": [
|
||||
@@ -1421,6 +1473,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"saved_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1963,12 +2021,19 @@
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
"request": {
|
||||
"$ref": "#/definitions/ElicitationRequest"
|
||||
},
|
||||
"server_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"turn_id": {
|
||||
"description": "Turn ID that this elicitation belongs to, when known.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"elicitation_request"
|
||||
@@ -1979,7 +2044,7 @@
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"message",
|
||||
"request",
|
||||
"server_name",
|
||||
"type"
|
||||
],
|
||||
@@ -3706,66 +3771,70 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsAutomationValue": {
|
||||
"anyOf": [
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsPermissions": {
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsSeatbeltProfileExtensions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"macos_accessibility": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"anyOf": [
|
||||
"macos_automation": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsAutomationValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
}
|
||||
]
|
||||
],
|
||||
"default": "none"
|
||||
},
|
||||
"calendar": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"macos_calendar": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"anyOf": [
|
||||
"macos_preferences": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPreferencesValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
}
|
||||
]
|
||||
],
|
||||
"default": "read_only"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsPreferencesValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpAuthStatus": {
|
||||
"enum": [
|
||||
"unsupported",
|
||||
@@ -4109,7 +4178,7 @@
|
||||
"macos": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPermissions"
|
||||
"$ref": "#/definitions/MacOsSeatbeltProfileExtensions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -5555,9 +5624,6 @@
|
||||
},
|
||||
"SessionNetworkProxyRuntime": {
|
||||
"properties": {
|
||||
"admin_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"http_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5566,7 +5632,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"admin_addr",
|
||||
"http_addr",
|
||||
"socks_addr"
|
||||
],
|
||||
@@ -6069,6 +6134,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"saved_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7214,6 +7285,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"saved_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7756,12 +7833,19 @@
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
"request": {
|
||||
"$ref": "#/definitions/ElicitationRequest"
|
||||
},
|
||||
"server_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"turn_id": {
|
||||
"description": "Turn ID that this elicitation belongs to, when known.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"elicitation_request"
|
||||
@@ -7772,7 +7856,7 @@
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"message",
|
||||
"request",
|
||||
"server_name",
|
||||
"type"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationBooleanSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationBooleanType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationBooleanType": {
|
||||
"enum": [
|
||||
"boolean"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationConstOption": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"const": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"const",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationLegacyTitledEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationLegacyTitledEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"enumNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationMultiSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledMultiSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationNumberSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"maximum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minimum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationNumberType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationNumberType": {
|
||||
"enum": [
|
||||
"number",
|
||||
"integer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationObjectType": {
|
||||
"enum": [
|
||||
"object"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationPrimitiveSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationStringSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationNumberSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationBooleanSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationSchema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/McpElicitationPrimitiveSchema"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"required": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationObjectType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"properties",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationSingleSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledSingleSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationStringFormat": {
|
||||
"enum": [
|
||||
"email",
|
||||
"uri",
|
||||
"date",
|
||||
"date-time"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationStringSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationStringFormat"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationStringType": {
|
||||
"enum": [
|
||||
"string"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationTitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"anyOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"anyOf"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationTitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"oneOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"oneOf",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationUntitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"form"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"requestedSchema": {
|
||||
"$ref": "#/definitions/McpElicitationSchema"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"mode",
|
||||
"requestedSchema"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"elicitationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"url"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"elicitationId",
|
||||
"message",
|
||||
"mode",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"description": "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"serverName",
|
||||
"threadId"
|
||||
],
|
||||
"title": "McpServerElicitationRequestParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"McpServerElicitationAction": {
|
||||
"enum": [
|
||||
"accept",
|
||||
"decline",
|
||||
"cancel"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Optional client metadata for form-mode action handling."
|
||||
},
|
||||
"action": {
|
||||
"$ref": "#/definitions/McpServerElicitationAction"
|
||||
},
|
||||
"content": {
|
||||
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"title": "McpServerElicitationRequestResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -201,6 +201,13 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -31,38 +31,24 @@
|
||||
"AdditionalMacOsPermissions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsAutomationValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
},
|
||||
"calendar": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPreferencesValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessibility",
|
||||
"automations",
|
||||
"calendar",
|
||||
"preferences"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdditionalNetworkPermissions": {
|
||||
@@ -629,29 +615,646 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsAutomationValue": {
|
||||
"anyOf": [
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationBooleanSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationBooleanType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationBooleanType": {
|
||||
"enum": [
|
||||
"boolean"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationConstOption": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"const": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"const",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationLegacyTitledEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationLegacyTitledEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"enumNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationMultiSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledMultiSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledMultiSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsPreferencesValue": {
|
||||
"McpElicitationNumberSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"maximum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minimum": {
|
||||
"format": "double",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationNumberType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationNumberType": {
|
||||
"enum": [
|
||||
"number",
|
||||
"integer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationObjectType": {
|
||||
"enum": [
|
||||
"object"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationPrimitiveSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/McpElicitationEnumSchema"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/McpElicitationStringSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationNumberSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationBooleanSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationSchema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/McpElicitationPrimitiveSchema"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"required": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationObjectType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"properties",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationSingleSelectEnumSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationUntitledSingleSelectEnumSchema"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationTitledSingleSelectEnumSchema"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationStringFormat": {
|
||||
"enum": [
|
||||
"email",
|
||||
"uri",
|
||||
"date",
|
||||
"date-time"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationStringSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpElicitationStringFormat"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minLength": {
|
||||
"format": "uint32",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationStringType": {
|
||||
"enum": [
|
||||
"string"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpElicitationTitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"anyOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"anyOf"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationTitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationTitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"oneOf": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationConstOption"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"oneOf",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledEnumItems": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledMultiSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"$ref": "#/definitions/McpElicitationUntitledEnumItems"
|
||||
},
|
||||
"maxItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"minItems": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationArrayType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpElicitationUntitledSingleSelectEnumSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/McpElicitationStringType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enum",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerElicitationRequestParams": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"form"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"requestedSchema": {
|
||||
"$ref": "#/definitions/McpElicitationSchema"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"mode",
|
||||
"requestedSchema"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"elicitationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"url"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"elicitationId",
|
||||
"message",
|
||||
"mode",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"description": "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"serverName",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
@@ -981,6 +1584,31 @@
|
||||
"title": "Item/tool/requestUserInputRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Request input for an MCP server elicitation.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"mcpServer/elicitation/request"
|
||||
],
|
||||
"title": "McpServer/elicitation/requestRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/McpServerElicitationRequestParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "McpServer/elicitation/requestRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Execute a dynamic tool call on the client.",
|
||||
"properties": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+291
-64
@@ -404,6 +404,13 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -553,6 +560,34 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AppSummary": {
|
||||
"description": "EXPERIMENTAL - app metadata summary for plugin-install responses.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"auto",
|
||||
@@ -1204,6 +1239,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"plugin/list"
|
||||
],
|
||||
"title": "Plugin/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PluginListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Plugin/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3286,6 +3345,58 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ElicitationRequest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"form"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"requested_schema": true
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"mode",
|
||||
"requested_schema"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"_meta": true,
|
||||
"elicitation_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"url"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"elicitation_id",
|
||||
"message",
|
||||
"mode",
|
||||
"url"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ErrorNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -4189,6 +4300,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"saved_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4731,12 +4848,19 @@
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
"request": {
|
||||
"$ref": "#/definitions/ElicitationRequest"
|
||||
},
|
||||
"server_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"turn_id": {
|
||||
"description": "Turn ID that this elicitation belongs to, when known.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"elicitation_request"
|
||||
@@ -4747,7 +4871,7 @@
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"message",
|
||||
"request",
|
||||
"server_name",
|
||||
"type"
|
||||
],
|
||||
@@ -7293,66 +7417,70 @@
|
||||
"title": "LogoutAccountResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsAutomationValue": {
|
||||
"anyOf": [
|
||||
"MacOsAutomationPermission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
"enum": [
|
||||
"none",
|
||||
"all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bundle_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
"required": [
|
||||
"bundle_ids"
|
||||
],
|
||||
"title": "BundleIdsMacOsAutomationPermission",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MacOsPermissions": {
|
||||
"MacOsPreferencesPermission": {
|
||||
"enum": [
|
||||
"none",
|
||||
"read_only",
|
||||
"read_write"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MacOsSeatbeltProfileExtensions": {
|
||||
"properties": {
|
||||
"accessibility": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"macos_accessibility": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"automations": {
|
||||
"anyOf": [
|
||||
"macos_automation": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsAutomationValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"$ref": "#/definitions/MacOsAutomationPermission"
|
||||
}
|
||||
]
|
||||
],
|
||||
"default": "none"
|
||||
},
|
||||
"calendar": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
"macos_calendar": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferences": {
|
||||
"anyOf": [
|
||||
"macos_preferences": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPreferencesValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
"$ref": "#/definitions/MacOsPreferencesPermission"
|
||||
}
|
||||
]
|
||||
],
|
||||
"default": "read_only"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MacOsPreferencesValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpAuthStatus": {
|
||||
"enum": [
|
||||
"unsupported",
|
||||
@@ -7973,12 +8101,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
@@ -8229,7 +8351,7 @@
|
||||
"macos": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MacOsPermissions"
|
||||
"$ref": "#/definitions/MacOsSeatbeltProfileExtensions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -8316,21 +8438,15 @@
|
||||
"PluginInstallParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"title": "PluginInstallParams",
|
||||
@@ -8338,9 +8454,118 @@
|
||||
},
|
||||
"PluginInstallResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"appsNeedingAuth": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"appsNeedingAuth"
|
||||
],
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces are considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaces"
|
||||
],
|
||||
"title": "PluginListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProductSurface": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
@@ -10917,9 +11142,6 @@
|
||||
},
|
||||
"SessionNetworkProxyRuntime": {
|
||||
"properties": {
|
||||
"admin_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"http_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10928,7 +11150,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"admin_addr",
|
||||
"http_addr",
|
||||
"socks_addr"
|
||||
],
|
||||
@@ -13847,6 +14068,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"saved_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -119,6 +119,13 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -119,6 +119,13 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -132,12 +132,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackAdmin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dangerouslyAllowNonLoopbackProxy": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"marketplaceName": {
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"marketplacePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"pluginName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"marketplacePath",
|
||||
"pluginName"
|
||||
],
|
||||
"title": "PluginInstallParams",
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AppSummary": {
|
||||
"description": "EXPERIMENTAL - app metadata summary for plugin-install responses.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"appsNeedingAuth": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"appsNeedingAuth"
|
||||
],
|
||||
"title": "PluginInstallResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces are considered.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PluginListParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PluginMarketplaceEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"local"
|
||||
],
|
||||
"title": "LocalPluginSourceType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalPluginSource",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PluginSummary": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/PluginSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"name",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"marketplaces": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginMarketplaceEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaces"
|
||||
],
|
||||
"title": "PluginListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import type { LoginAccountParams } from "./v2/LoginAccountParams";
|
||||
import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams";
|
||||
import type { ModelListParams } from "./v2/ModelListParams";
|
||||
import type { PluginInstallParams } from "./v2/PluginInstallParams";
|
||||
import type { PluginListParams } from "./v2/PluginListParams";
|
||||
import type { ReviewStartParams } from "./v2/ReviewStartParams";
|
||||
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
|
||||
import type { SkillsListParams } from "./v2/SkillsListParams";
|
||||
@@ -49,4 +50,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": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "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": "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/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": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "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": "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,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 { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
export type ElicitationRequest = { "mode": "form", _meta?: JsonValue, message: string, requested_schema: JsonValue, } | { "mode": "url", _meta?: JsonValue, message: string, url: string, elicitation_id: string, };
|
||||
@@ -1,5 +1,10 @@
|
||||
// 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 { ElicitationRequest } from "./ElicitationRequest";
|
||||
|
||||
export type ElicitationRequestEvent = { server_name: string, id: string | number, message: string, };
|
||||
export type ElicitationRequestEvent = {
|
||||
/**
|
||||
* Turn ID that this elicitation belongs to, when known.
|
||||
*/
|
||||
turn_id?: string, server_name: string, id: string | number, request: ElicitationRequest, };
|
||||
|
||||
@@ -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 ImageGenerationEndEvent = { call_id: string, status: string, revised_prompt?: string, result: string, };
|
||||
export type ImageGenerationEndEvent = { call_id: string, status: string, revised_prompt?: string, result: string, saved_path?: string, };
|
||||
|
||||
@@ -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 ImageGenerationItem = { id: string, status: string, revised_prompt?: string, result: string, };
|
||||
export type ImageGenerationItem = { id: string, status: string, revised_prompt?: string, result: string, saved_path?: 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 MacOsAutomationPermission = "none" | "all" | { "bundle_ids": Array<string> };
|
||||
@@ -1,7 +0,0 @@
|
||||
// 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 { MacOsAutomationValue } from "./MacOsAutomationValue";
|
||||
import type { MacOsPreferencesValue } from "./MacOsPreferencesValue";
|
||||
|
||||
export type MacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, };
|
||||
@@ -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 MacOsPreferencesPermission = "none" | "read_only" | "read_write";
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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 { MacOsAutomationPermission } from "./MacOsAutomationPermission";
|
||||
import type { MacOsPreferencesPermission } from "./MacOsPreferencesPermission";
|
||||
|
||||
export type MacOsSeatbeltProfileExtensions = { macos_preferences: MacOsPreferencesPermission, macos_automation: MacOsAutomationPermission, macos_accessibility: boolean, macos_calendar: boolean, };
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FileSystemPermissions } from "./FileSystemPermissions";
|
||||
import type { MacOsPermissions } from "./MacOsPermissions";
|
||||
import type { MacOsSeatbeltProfileExtensions } from "./MacOsSeatbeltProfileExtensions";
|
||||
import type { NetworkPermissions } from "./NetworkPermissions";
|
||||
|
||||
export type PermissionProfile = { network: NetworkPermissions | null, file_system: FileSystemPermissions | null, macos: MacOsPermissions | null, };
|
||||
export type PermissionProfile = { network: NetworkPermissions | null, file_system: FileSystemPermissions | null, macos: MacOsSeatbeltProfileExtensions | null, };
|
||||
|
||||
@@ -8,9 +8,10 @@ import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefre
|
||||
import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams";
|
||||
import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
|
||||
import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams";
|
||||
import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams";
|
||||
import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams";
|
||||
|
||||
/**
|
||||
* Request initiated from the server and sent to the client.
|
||||
*/
|
||||
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
|
||||
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
|
||||
|
||||
@@ -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 SessionNetworkProxyRuntime = { http_addr: string, socks_addr: string, admin_addr: string, };
|
||||
export type SessionNetworkProxyRuntime = { http_addr: string, socks_addr: string, };
|
||||
|
||||
@@ -48,6 +48,7 @@ export type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent";
|
||||
export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
|
||||
export type { DynamicToolCallRequest } from "./DynamicToolCallRequest";
|
||||
export type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent";
|
||||
export type { ElicitationRequest } from "./ElicitationRequest";
|
||||
export type { ElicitationRequestEvent } from "./ElicitationRequestEvent";
|
||||
export type { ErrorEvent } from "./ErrorEvent";
|
||||
export type { EventMsg } from "./EventMsg";
|
||||
@@ -99,9 +100,9 @@ export type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent";
|
||||
export type { LocalShellAction } from "./LocalShellAction";
|
||||
export type { LocalShellExecAction } from "./LocalShellExecAction";
|
||||
export type { LocalShellStatus } from "./LocalShellStatus";
|
||||
export type { MacOsAutomationValue } from "./MacOsAutomationValue";
|
||||
export type { MacOsPermissions } from "./MacOsPermissions";
|
||||
export type { MacOsPreferencesValue } from "./MacOsPreferencesValue";
|
||||
export type { MacOsAutomationPermission } from "./MacOsAutomationPermission";
|
||||
export type { MacOsPreferencesPermission } from "./MacOsPreferencesPermission";
|
||||
export type { MacOsSeatbeltProfileExtensions } from "./MacOsSeatbeltProfileExtensions";
|
||||
export type { McpAuthStatus } from "./McpAuthStatus";
|
||||
export type { McpInvocation } from "./McpInvocation";
|
||||
export type { McpListToolsResponseEvent } from "./McpListToolsResponseEvent";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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 { MacOsAutomationValue } from "../MacOsAutomationValue";
|
||||
import type { MacOsPreferencesValue } from "../MacOsPreferencesValue";
|
||||
import type { MacOsAutomationPermission } from "../MacOsAutomationPermission";
|
||||
import type { MacOsPreferencesPermission } from "../MacOsPreferencesPermission";
|
||||
|
||||
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, };
|
||||
export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesPermission, automations: MacOsAutomationPermission, accessibility: boolean, calendar: boolean, };
|
||||
|
||||
@@ -16,4 +16,4 @@ export type AppInfo = { id: string, name: string, description: string | null, lo
|
||||
* enabled = false
|
||||
* ```
|
||||
*/
|
||||
isEnabled: boolean, };
|
||||
isEnabled: boolean, pluginDisplayNames: Array<string>, };
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL - app metadata summary for plugin-install responses.
|
||||
*/
|
||||
export type AppSummary = { id: string, name: string, description: string | null, installUrl: string | null, };
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MacOsPreferencesValue = boolean | string;
|
||||
export type McpElicitationArrayType = "array";
|
||||
@@ -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 { McpElicitationBooleanType } from "./McpElicitationBooleanType";
|
||||
|
||||
export type McpElicitationBooleanSchema = { type: McpElicitationBooleanType, title?: string, description?: string, default?: boolean, };
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MacOsAutomationValue = boolean | Array<string>;
|
||||
export type McpElicitationBooleanType = "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 McpElicitationConstOption = { const: string, title: string, };
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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 { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema";
|
||||
import type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema";
|
||||
import type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema";
|
||||
|
||||
export type McpElicitationEnumSchema = McpElicitationSingleSelectEnumSchema | McpElicitationMultiSelectEnumSchema | McpElicitationLegacyTitledEnumSchema;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
|
||||
export type McpElicitationLegacyTitledEnumSchema = { type: McpElicitationStringType, title?: string, description?: string, enum: Array<string>, enumNames?: Array<string>, default?: string, };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema";
|
||||
import type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema";
|
||||
|
||||
export type McpElicitationMultiSelectEnumSchema = McpElicitationUntitledMultiSelectEnumSchema | McpElicitationTitledMultiSelectEnumSchema;
|
||||
@@ -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 { McpElicitationNumberType } from "./McpElicitationNumberType";
|
||||
|
||||
export type McpElicitationNumberSchema = { type: McpElicitationNumberType, title?: string, description?: string, minimum?: number, maximum?: number, default?: number, };
|
||||
@@ -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 McpElicitationNumberType = "number" | "integer";
|
||||
@@ -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 McpElicitationObjectType = "object";
|
||||
@@ -0,0 +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 { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema";
|
||||
import type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema";
|
||||
import type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema";
|
||||
import type { McpElicitationStringSchema } from "./McpElicitationStringSchema";
|
||||
|
||||
export type McpElicitationPrimitiveSchema = McpElicitationEnumSchema | McpElicitationStringSchema | McpElicitationNumberSchema | McpElicitationBooleanSchema;
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 { McpElicitationObjectType } from "./McpElicitationObjectType";
|
||||
import type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema";
|
||||
|
||||
/**
|
||||
* Typed form schema for MCP `elicitation/create` requests.
|
||||
*
|
||||
* This matches the `requestedSchema` shape from the MCP 2025-11-25
|
||||
* `ElicitRequestFormParams` schema.
|
||||
*/
|
||||
export type McpElicitationSchema = { $schema?: string, type: McpElicitationObjectType, properties: { [key in string]?: McpElicitationPrimitiveSchema }, required?: Array<string>, };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema";
|
||||
import type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema";
|
||||
|
||||
export type McpElicitationSingleSelectEnumSchema = McpElicitationUntitledSingleSelectEnumSchema | McpElicitationTitledSingleSelectEnumSchema;
|
||||
@@ -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 McpElicitationStringFormat = "email" | "uri" | "date" | "date-time";
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationStringFormat } from "./McpElicitationStringFormat";
|
||||
import type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
|
||||
export type McpElicitationStringSchema = { type: McpElicitationStringType, title?: string, description?: string, minLength?: number, maxLength?: number, format?: McpElicitationStringFormat, default?: 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 McpElicitationStringType = "string";
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpElicitationConstOption } from "./McpElicitationConstOption";
|
||||
|
||||
export type McpElicitationTitledEnumItems = { anyOf: Array<McpElicitationConstOption>, };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationArrayType } from "./McpElicitationArrayType";
|
||||
import type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems";
|
||||
|
||||
export type McpElicitationTitledMultiSelectEnumSchema = { type: McpElicitationArrayType, title?: string, description?: string, minItems?: bigint, maxItems?: bigint, items: McpElicitationTitledEnumItems, default?: Array<string>, };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationConstOption } from "./McpElicitationConstOption";
|
||||
import type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
|
||||
export type McpElicitationTitledSingleSelectEnumSchema = { type: McpElicitationStringType, title?: string, description?: string, oneOf: Array<McpElicitationConstOption>, default?: string, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
|
||||
export type McpElicitationUntitledEnumItems = { type: McpElicitationStringType, enum: Array<string>, };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// 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 { McpElicitationArrayType } from "./McpElicitationArrayType";
|
||||
import type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems";
|
||||
|
||||
export type McpElicitationUntitledMultiSelectEnumSchema = { type: McpElicitationArrayType, title?: string, description?: string, minItems?: bigint, maxItems?: bigint, items: McpElicitationUntitledEnumItems, default?: Array<string>, };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
|
||||
export type McpElicitationUntitledSingleSelectEnumSchema = { type: McpElicitationStringType, title?: string, description?: string, enum: Array<string>, default?: 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 McpServerElicitationAction = "accept" | "decline" | "cancel";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// 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 { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { McpElicitationSchema } from "./McpElicitationSchema";
|
||||
|
||||
export type McpServerElicitationRequestParams = { threadId: string,
|
||||
/**
|
||||
* Active Codex turn when this elicitation was observed, if app-server could correlate one.
|
||||
*
|
||||
* This is nullable because MCP models elicitation as a standalone server-to-client request
|
||||
* identified by the MCP server request id. It may be triggered during a turn, but turn
|
||||
* context is app-server correlation rather than part of the protocol identity of the
|
||||
* elicitation itself.
|
||||
*/
|
||||
turnId: string | null, serverName: string, } & ({ "mode": "form", _meta: JsonValue | null, message: string, requestedSchema: McpElicitationSchema, } | { "mode": "url", _meta: JsonValue | null, message: string, url: string, elicitationId: string, });
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// 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 { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { McpServerElicitationAction } from "./McpServerElicitationAction";
|
||||
|
||||
export type McpServerElicitationRequestResponse = { action: McpServerElicitationAction,
|
||||
/**
|
||||
* Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.
|
||||
*
|
||||
* This is nullable because decline/cancel responses have no content.
|
||||
*/
|
||||
content: JsonValue | null,
|
||||
/**
|
||||
* Optional client metadata for form-mode action handling.
|
||||
*/
|
||||
_meta: JsonValue | null, };
|
||||
@@ -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 NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowAllUnixSockets: boolean | null, allowedDomains: Array<string> | null, deniedDomains: Array<string> | null, allowUnixSockets: Array<string> | null, allowLocalBinding: boolean | null, };
|
||||
|
||||
@@ -1,5 +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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PluginInstallParams = { marketplaceName: string, pluginName: string, cwd?: string | null, };
|
||||
export type PluginInstallParams = { marketplacePath: AbsolutePathBuf, pluginName: string, };
|
||||
|
||||
@@ -1,5 +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 { AppSummary } from "./AppSummary";
|
||||
|
||||
export type PluginInstallResponse = Record<string, never>;
|
||||
export type PluginInstallResponse = { appsNeedingAuth: Array<AppSummary>, };
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PluginListParams = {
|
||||
/**
|
||||
* Optional working directories used to discover repo marketplaces. When omitted,
|
||||
* only home-scoped marketplaces are considered.
|
||||
*/
|
||||
cwds?: Array<AbsolutePathBuf> | 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 { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
|
||||
export type PluginListResponse = { marketplaces: Array<PluginMarketplaceEntry>, };
|
||||
@@ -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 { PluginSummary } from "./PluginSummary";
|
||||
|
||||
export type PluginMarketplaceEntry = { name: string, path: string, plugins: Array<PluginSummary>, };
|
||||
@@ -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 PluginSource = { "type": "local", path: string, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginSource } from "./PluginSource";
|
||||
|
||||
export type PluginSummary = { name: string, source: PluginSource, enabled: boolean, };
|
||||
@@ -16,6 +16,7 @@ export type { AppListUpdatedNotification } from "./AppListUpdatedNotification";
|
||||
export type { AppMetadata } from "./AppMetadata";
|
||||
export type { AppReview } from "./AppReview";
|
||||
export type { AppScreenshot } from "./AppScreenshot";
|
||||
export type { AppSummary } from "./AppSummary";
|
||||
export type { AppToolApproval } from "./AppToolApproval";
|
||||
export type { AppToolsConfig } from "./AppToolsConfig";
|
||||
export type { AppsConfig } from "./AppsConfig";
|
||||
@@ -97,6 +98,31 @@ export type { LoginAccountParams } from "./LoginAccountParams";
|
||||
export type { LoginAccountResponse } from "./LoginAccountResponse";
|
||||
export type { LogoutAccountResponse } from "./LogoutAccountResponse";
|
||||
export type { McpAuthStatus } from "./McpAuthStatus";
|
||||
export type { McpElicitationArrayType } from "./McpElicitationArrayType";
|
||||
export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema";
|
||||
export type { McpElicitationBooleanType } from "./McpElicitationBooleanType";
|
||||
export type { McpElicitationConstOption } from "./McpElicitationConstOption";
|
||||
export type { McpElicitationEnumSchema } from "./McpElicitationEnumSchema";
|
||||
export type { McpElicitationLegacyTitledEnumSchema } from "./McpElicitationLegacyTitledEnumSchema";
|
||||
export type { McpElicitationMultiSelectEnumSchema } from "./McpElicitationMultiSelectEnumSchema";
|
||||
export type { McpElicitationNumberSchema } from "./McpElicitationNumberSchema";
|
||||
export type { McpElicitationNumberType } from "./McpElicitationNumberType";
|
||||
export type { McpElicitationObjectType } from "./McpElicitationObjectType";
|
||||
export type { McpElicitationPrimitiveSchema } from "./McpElicitationPrimitiveSchema";
|
||||
export type { McpElicitationSchema } from "./McpElicitationSchema";
|
||||
export type { McpElicitationSingleSelectEnumSchema } from "./McpElicitationSingleSelectEnumSchema";
|
||||
export type { McpElicitationStringFormat } from "./McpElicitationStringFormat";
|
||||
export type { McpElicitationStringSchema } from "./McpElicitationStringSchema";
|
||||
export type { McpElicitationStringType } from "./McpElicitationStringType";
|
||||
export type { McpElicitationTitledEnumItems } from "./McpElicitationTitledEnumItems";
|
||||
export type { McpElicitationTitledMultiSelectEnumSchema } from "./McpElicitationTitledMultiSelectEnumSchema";
|
||||
export type { McpElicitationTitledSingleSelectEnumSchema } from "./McpElicitationTitledSingleSelectEnumSchema";
|
||||
export type { McpElicitationUntitledEnumItems } from "./McpElicitationUntitledEnumItems";
|
||||
export type { McpElicitationUntitledMultiSelectEnumSchema } from "./McpElicitationUntitledMultiSelectEnumSchema";
|
||||
export type { McpElicitationUntitledSingleSelectEnumSchema } from "./McpElicitationUntitledSingleSelectEnumSchema";
|
||||
export type { McpServerElicitationAction } from "./McpServerElicitationAction";
|
||||
export type { McpServerElicitationRequestParams } from "./McpServerElicitationRequestParams";
|
||||
export type { McpServerElicitationRequestResponse } from "./McpServerElicitationRequestResponse";
|
||||
export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification";
|
||||
export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams";
|
||||
export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse";
|
||||
@@ -126,6 +152,11 @@ export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
export type { PluginInstallParams } from "./PluginInstallParams";
|
||||
export type { PluginInstallResponse } from "./PluginInstallResponse";
|
||||
export type { PluginListParams } from "./PluginListParams";
|
||||
export type { PluginListResponse } from "./PluginListResponse";
|
||||
export type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry";
|
||||
export type { PluginSource } from "./PluginSource";
|
||||
export type { PluginSummary } from "./PluginSummary";
|
||||
export type { ProductSurface } from "./ProductSurface";
|
||||
export type { ProfileV2 } from "./ProfileV2";
|
||||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||||
|
||||
@@ -248,6 +248,10 @@ client_request_definitions! {
|
||||
params: v2::SkillsListParams,
|
||||
response: v2::SkillsListResponse,
|
||||
},
|
||||
PluginList => "plugin/list" {
|
||||
params: v2::PluginListParams,
|
||||
response: v2::PluginListResponse,
|
||||
},
|
||||
SkillsRemoteList => "skills/remote/list" {
|
||||
params: v2::SkillsRemoteReadParams,
|
||||
response: v2::SkillsRemoteReadResponse,
|
||||
@@ -650,6 +654,12 @@ server_request_definitions! {
|
||||
response: v2::ToolRequestUserInputResponse,
|
||||
},
|
||||
|
||||
/// Request input for an MCP server elicitation.
|
||||
McpServerElicitationRequest => "mcpServer/elicitation/request" {
|
||||
params: v2::McpServerElicitationRequestParams,
|
||||
response: v2::McpServerElicitationRequestResponse,
|
||||
},
|
||||
|
||||
/// Execute a dynamic tool call on the client.
|
||||
DynamicToolCall => "item/tool/call" {
|
||||
params: v2::DynamicToolCallParams,
|
||||
@@ -1046,6 +1056,63 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_mcp_server_elicitation_request() -> Result<()> {
|
||||
let requested_schema: v2::McpElicitationSchema = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["confirmed"]
|
||||
}))?;
|
||||
let params = v2::McpServerElicitationRequestParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
turn_id: Some("turn_123".to_string()),
|
||||
server_name: "codex_apps".to_string(),
|
||||
request: v2::McpServerElicitationRequest::Form {
|
||||
meta: None,
|
||||
message: "Allow this request?".to_string(),
|
||||
requested_schema,
|
||||
},
|
||||
};
|
||||
let request = ServerRequest::McpServerElicitationRequest {
|
||||
request_id: RequestId::Integer(9),
|
||||
params: params.clone(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "mcpServer/elicitation/request",
|
||||
"id": 9,
|
||||
"params": {
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_123",
|
||||
"serverName": "codex_apps",
|
||||
"mode": "form",
|
||||
"_meta": null,
|
||||
"message": "Allow this request?",
|
||||
"requestedSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["confirmed"]
|
||||
}
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
|
||||
let payload = ServerRequestPayload::McpServerElicitationRequest(params);
|
||||
assert_eq!(request.id(), &RequestId::Integer(9));
|
||||
assert_eq!(payload.request_with_id(RequestId::Integer(9)), request);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_get_account_rate_limits() -> Result<()> {
|
||||
let request = ClientRequest::GetAccountRateLimits {
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::RequestId;
|
||||
use crate::protocol::common::AuthMode;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
|
||||
@@ -27,9 +28,9 @@ use codex_protocol::mcp::Resource as McpResource;
|
||||
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
use codex_protocol::mcp::Tool as McpTool;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::MacOsAutomationValue as CoreMacOsAutomationValue;
|
||||
use codex_protocol::models::MacOsPermissions as CoreMacOsPermissions;
|
||||
use codex_protocol::models::MacOsPreferencesValue as CoreMacOsPreferencesValue;
|
||||
use codex_protocol::models::MacOsAutomationPermission as CoreMacOsAutomationPermission;
|
||||
use codex_protocol::models::MacOsPreferencesPermission as CoreMacOsPreferencesPermission;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions as CoreMacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
|
||||
@@ -628,7 +629,6 @@ pub struct NetworkRequirements {
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
@@ -637,7 +637,7 @@ pub struct NetworkRequirements {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ResidencyRequirement {
|
||||
Us,
|
||||
@@ -836,19 +836,19 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AdditionalMacOsPermissions {
|
||||
pub preferences: Option<CoreMacOsPreferencesValue>,
|
||||
pub automations: Option<CoreMacOsAutomationValue>,
|
||||
pub accessibility: Option<bool>,
|
||||
pub calendar: Option<bool>,
|
||||
pub preferences: CoreMacOsPreferencesPermission,
|
||||
pub automations: CoreMacOsAutomationPermission,
|
||||
pub accessibility: bool,
|
||||
pub calendar: bool,
|
||||
}
|
||||
|
||||
impl From<CoreMacOsPermissions> for AdditionalMacOsPermissions {
|
||||
fn from(value: CoreMacOsPermissions) -> Self {
|
||||
impl From<CoreMacOsSeatbeltProfileExtensions> for AdditionalMacOsPermissions {
|
||||
fn from(value: CoreMacOsSeatbeltProfileExtensions) -> Self {
|
||||
Self {
|
||||
preferences: value.preferences,
|
||||
automations: value.automations,
|
||||
accessibility: value.accessibility,
|
||||
calendar: value.calendar,
|
||||
preferences: value.macos_preferences,
|
||||
automations: value.macos_automation,
|
||||
accessibility: value.macos_accessibility,
|
||||
calendar: value.macos_calendar,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1708,6 +1708,30 @@ pub struct AppInfo {
|
||||
/// ```
|
||||
#[serde(default = "default_enabled")]
|
||||
pub is_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub plugin_display_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL - app metadata summary for plugin-install responses.
|
||||
pub struct AppSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub install_url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<AppInfo> for AppSummary {
|
||||
fn from(value: AppInfo) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
install_url: value.install_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -2368,6 +2392,23 @@ pub struct SkillsListResponse {
|
||||
pub data: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginListParams {
|
||||
/// Optional working directories used to discover repo marketplaces. When omitted,
|
||||
/// only home-scoped marketplaces are considered.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwds: Option<Vec<AbsolutePathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginListResponse {
|
||||
pub marketplaces: Vec<PluginMarketplaceEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2393,8 +2434,8 @@ pub enum HazelnutScope {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ProductSurface {
|
||||
Chatgpt,
|
||||
@@ -2531,6 +2572,34 @@ pub struct SkillsListEntry {
|
||||
pub errors: Vec<SkillErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginMarketplaceEntry {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub plugins: Vec<PluginSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginSummary {
|
||||
pub name: String,
|
||||
pub source: PluginSource,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PluginSource {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Local { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2550,16 +2619,16 @@ pub struct SkillsConfigWriteResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginInstallParams {
|
||||
pub marketplace_name: String,
|
||||
pub marketplace_path: AbsolutePathBuf,
|
||||
pub plugin_name: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginInstallResponse {}
|
||||
pub struct PluginInstallResponse {
|
||||
pub apps_needing_auth: Vec<AppSummary>,
|
||||
}
|
||||
|
||||
impl From<CoreSkillMetadata> for SkillMetadata {
|
||||
fn from(value: CoreSkillMetadata) -> Self {
|
||||
@@ -4081,6 +4150,472 @@ pub struct FileChangeRequestApprovalResponse {
|
||||
pub decision: FileChangeApprovalDecision,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpServerElicitationAction {
|
||||
Accept,
|
||||
Decline,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl McpServerElicitationAction {
|
||||
pub fn to_core(self) -> codex_protocol::approvals::ElicitationAction {
|
||||
match self {
|
||||
Self::Accept => codex_protocol::approvals::ElicitationAction::Accept,
|
||||
Self::Decline => codex_protocol::approvals::ElicitationAction::Decline,
|
||||
Self::Cancel => codex_protocol::approvals::ElicitationAction::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<McpServerElicitationAction> for rmcp::model::ElicitationAction {
|
||||
fn from(value: McpServerElicitationAction) -> Self {
|
||||
match value {
|
||||
McpServerElicitationAction::Accept => Self::Accept,
|
||||
McpServerElicitationAction::Decline => Self::Decline,
|
||||
McpServerElicitationAction::Cancel => Self::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rmcp::model::ElicitationAction> for McpServerElicitationAction {
|
||||
fn from(value: rmcp::model::ElicitationAction) -> Self {
|
||||
match value {
|
||||
rmcp::model::ElicitationAction::Accept => Self::Accept,
|
||||
rmcp::model::ElicitationAction::Decline => Self::Decline,
|
||||
rmcp::model::ElicitationAction::Cancel => Self::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpServerElicitationRequestParams {
|
||||
pub thread_id: String,
|
||||
/// Active Codex turn when this elicitation was observed, if app-server could correlate one.
|
||||
///
|
||||
/// This is nullable because MCP models elicitation as a standalone server-to-client request
|
||||
/// identified by the MCP server request id. It may be triggered during a turn, but turn
|
||||
/// context is app-server correlation rather than part of the protocol identity of the
|
||||
/// elicitation itself.
|
||||
pub turn_id: Option<String>,
|
||||
pub server_name: String,
|
||||
#[serde(flatten)]
|
||||
pub request: McpServerElicitationRequest,
|
||||
// TODO: When core can correlate an elicitation with an MCP tool call, expose the associated
|
||||
// McpToolCall item id here as an optional field. The current core event does not carry that
|
||||
// association.
|
||||
}
|
||||
|
||||
/// Typed form schema for MCP `elicitation/create` requests.
|
||||
///
|
||||
/// This matches the `requestedSchema` shape from the MCP 2025-11-25
|
||||
/// `ElicitRequestFormParams` schema.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationSchema {
|
||||
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, rename = "$schema")]
|
||||
pub schema_uri: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationObjectType,
|
||||
pub properties: BTreeMap<String, McpElicitationPrimitiveSchema>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub required: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationObjectType {
|
||||
Object,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationPrimitiveSchema {
|
||||
Enum(McpElicitationEnumSchema),
|
||||
String(McpElicitationStringSchema),
|
||||
Number(McpElicitationNumberSchema),
|
||||
Boolean(McpElicitationBooleanSchema),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationStringSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationStringType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub min_length: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub max_length: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub format: Option<McpElicitationStringFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationStringType {
|
||||
String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(rename_all = "kebab-case", export_to = "v2/")]
|
||||
pub enum McpElicitationStringFormat {
|
||||
Email,
|
||||
Uri,
|
||||
Date,
|
||||
DateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationNumberSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationNumberType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub minimum: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub maximum: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationNumberType {
|
||||
Number,
|
||||
Integer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationBooleanSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationBooleanType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationBooleanType {
|
||||
Boolean,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationEnumSchema {
|
||||
SingleSelect(McpElicitationSingleSelectEnumSchema),
|
||||
MultiSelect(McpElicitationMultiSelectEnumSchema),
|
||||
Legacy(McpElicitationLegacyTitledEnumSchema),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationLegacyTitledEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationStringType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "enum")]
|
||||
#[ts(rename = "enum")]
|
||||
pub enum_: Vec<String>,
|
||||
#[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional, rename = "enumNames")]
|
||||
pub enum_names: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationSingleSelectEnumSchema {
|
||||
Untitled(McpElicitationUntitledSingleSelectEnumSchema),
|
||||
Titled(McpElicitationTitledSingleSelectEnumSchema),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationUntitledSingleSelectEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationStringType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "enum")]
|
||||
#[ts(rename = "enum")]
|
||||
pub enum_: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationTitledSingleSelectEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationStringType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "oneOf")]
|
||||
#[ts(rename = "oneOf")]
|
||||
pub one_of: Vec<McpElicitationConstOption>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationMultiSelectEnumSchema {
|
||||
Untitled(McpElicitationUntitledMultiSelectEnumSchema),
|
||||
Titled(McpElicitationTitledMultiSelectEnumSchema),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationUntitledMultiSelectEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationArrayType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub min_items: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub max_items: Option<u64>,
|
||||
pub items: McpElicitationUntitledEnumItems,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationTitledMultiSelectEnumSchema {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationArrayType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub min_items: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub max_items: Option<u64>,
|
||||
pub items: McpElicitationTitledEnumItems,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub default: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpElicitationArrayType {
|
||||
Array,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationUntitledEnumItems {
|
||||
#[serde(rename = "type")]
|
||||
#[ts(rename = "type")]
|
||||
pub type_: McpElicitationStringType,
|
||||
#[serde(rename = "enum")]
|
||||
#[ts(rename = "enum")]
|
||||
pub enum_: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationTitledEnumItems {
|
||||
#[serde(rename = "anyOf", alias = "oneOf")]
|
||||
#[ts(rename = "anyOf")]
|
||||
pub any_of: Vec<McpElicitationConstOption>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpElicitationConstOption {
|
||||
#[serde(rename = "const")]
|
||||
#[ts(rename = "const")]
|
||||
pub const_: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "mode", rename_all = "camelCase")]
|
||||
#[ts(tag = "mode")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum McpServerElicitationRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Form {
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta")]
|
||||
meta: Option<JsonValue>,
|
||||
message: String,
|
||||
requested_schema: McpElicitationSchema,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Url {
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta")]
|
||||
meta: Option<JsonValue>,
|
||||
message: String,
|
||||
url: String,
|
||||
elicitation_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<CoreElicitationRequest> for McpServerElicitationRequest {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: CoreElicitationRequest) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CoreElicitationRequest::Form {
|
||||
meta,
|
||||
message,
|
||||
requested_schema,
|
||||
} => Ok(Self::Form {
|
||||
meta,
|
||||
message,
|
||||
requested_schema: serde_json::from_value(requested_schema)?,
|
||||
}),
|
||||
CoreElicitationRequest::Url {
|
||||
meta,
|
||||
message,
|
||||
url,
|
||||
elicitation_id,
|
||||
} => Ok(Self::Url {
|
||||
meta,
|
||||
message,
|
||||
url,
|
||||
elicitation_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpServerElicitationRequestResponse {
|
||||
pub action: McpServerElicitationAction,
|
||||
/// Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.
|
||||
///
|
||||
/// This is nullable because decline/cancel responses have no content.
|
||||
pub content: Option<JsonValue>,
|
||||
/// Optional client metadata for form-mode action handling.
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta")]
|
||||
pub meta: Option<JsonValue>,
|
||||
}
|
||||
|
||||
impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitationResult {
|
||||
fn from(value: McpServerElicitationRequestResponse) -> Self {
|
||||
Self {
|
||||
action: value.action.into(),
|
||||
content: value.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rmcp::model::CreateElicitationResult> for McpServerElicitationRequestResponse {
|
||||
fn from(value: rmcp::model::CreateElicitationResult) -> Self {
|
||||
Self {
|
||||
action: value.action.into(),
|
||||
content: value.content,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4372,6 +4907,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_request_approval_accepts_macos_automation_bundle_ids_object() {
|
||||
let params = serde_json::from_value::<CommandExecutionRequestApprovalParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"turnId": "turn_123",
|
||||
"itemId": "call_123",
|
||||
"command": "cat file",
|
||||
"cwd": "/tmp",
|
||||
"commandActions": null,
|
||||
"reason": null,
|
||||
"networkApprovalContext": null,
|
||||
"additionalPermissions": {
|
||||
"network": null,
|
||||
"fileSystem": null,
|
||||
"macos": {
|
||||
"preferences": "read_only",
|
||||
"automations": {
|
||||
"bundle_ids": ["com.apple.Notes"]
|
||||
},
|
||||
"accessibility": false,
|
||||
"calendar": false
|
||||
}
|
||||
},
|
||||
"proposedExecpolicyAmendment": null,
|
||||
"proposedNetworkPolicyAmendments": null,
|
||||
"availableDecisions": null
|
||||
}))
|
||||
.expect("bundle_ids object should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params
|
||||
.additional_permissions
|
||||
.and_then(|permissions| permissions.macos)
|
||||
.map(|macos| macos.automations),
|
||||
Some(CoreMacOsAutomationPermission::BundleIds(vec![
|
||||
"com.apple.Notes".to_string(),
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_external_sandbox_network_access() {
|
||||
let v2_policy = SandboxPolicy::ExternalSandbox {
|
||||
@@ -4417,6 +4992,237 @@ mod tests {
|
||||
assert_eq!(back_to_v2, v2_policy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_response_round_trips_rmcp_result() {
|
||||
let rmcp_result = rmcp::model::CreateElicitationResult {
|
||||
action: rmcp::model::ElicitationAction::Accept,
|
||||
content: Some(json!({
|
||||
"confirmed": true,
|
||||
})),
|
||||
};
|
||||
|
||||
let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone());
|
||||
assert_eq!(
|
||||
v2_response,
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Accept,
|
||||
content: Some(json!({
|
||||
"confirmed": true,
|
||||
})),
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
rmcp::model::CreateElicitationResult::from(v2_response),
|
||||
rmcp_result
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_request_from_core_url_request() {
|
||||
let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Url {
|
||||
meta: None,
|
||||
message: "Finish sign-in".to_string(),
|
||||
url: "https://example.com/complete".to_string(),
|
||||
elicitation_id: "elicitation-123".to_string(),
|
||||
})
|
||||
.expect("URL request should convert");
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
McpServerElicitationRequest::Url {
|
||||
meta: None,
|
||||
message: "Finish sign-in".to_string(),
|
||||
url: "https://example.com/complete".to_string(),
|
||||
elicitation_id: "elicitation-123".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_request_from_core_form_request() {
|
||||
let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form {
|
||||
meta: None,
|
||||
message: "Allow this request?".to_string(),
|
||||
requested_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmed": {
|
||||
"type": "boolean",
|
||||
}
|
||||
},
|
||||
"required": ["confirmed"],
|
||||
}),
|
||||
})
|
||||
.expect("form request should convert");
|
||||
|
||||
let expected_schema: McpElicitationSchema = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmed": {
|
||||
"type": "boolean",
|
||||
}
|
||||
},
|
||||
"required": ["confirmed"],
|
||||
}))
|
||||
.expect("expected schema should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
McpServerElicitationRequest::Form {
|
||||
meta: None,
|
||||
message: "Allow this request?".to_string(),
|
||||
requested_schema: expected_schema,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_elicitation_schema_matches_mcp_2025_11_25_primitives() {
|
||||
let schema: McpElicitationSchema = serde_json::from_value(json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"title": "Email",
|
||||
"description": "Work email address",
|
||||
"format": "email",
|
||||
"default": "dev@example.com",
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"title": "Count",
|
||||
"description": "How many items to create",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"default": 3,
|
||||
},
|
||||
"confirmed": {
|
||||
"type": "boolean",
|
||||
"title": "Confirm",
|
||||
"description": "Approve the pending action",
|
||||
"default": true,
|
||||
},
|
||||
"legacyChoice": {
|
||||
"type": "string",
|
||||
"title": "Action",
|
||||
"description": "Legacy titled enum form",
|
||||
"enum": ["allow", "deny"],
|
||||
"enumNames": ["Allow", "Deny"],
|
||||
"default": "allow",
|
||||
},
|
||||
},
|
||||
"required": ["email", "confirmed"],
|
||||
}))
|
||||
.expect("schema should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
McpElicitationSchema {
|
||||
schema_uri: Some("https://json-schema.org/draft/2020-12/schema".to_string()),
|
||||
type_: McpElicitationObjectType::Object,
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"confirmed".to_string(),
|
||||
McpElicitationPrimitiveSchema::Boolean(McpElicitationBooleanSchema {
|
||||
type_: McpElicitationBooleanType::Boolean,
|
||||
title: Some("Confirm".to_string()),
|
||||
description: Some("Approve the pending action".to_string()),
|
||||
default: Some(true),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"count".to_string(),
|
||||
McpElicitationPrimitiveSchema::Number(McpElicitationNumberSchema {
|
||||
type_: McpElicitationNumberType::Integer,
|
||||
title: Some("Count".to_string()),
|
||||
description: Some("How many items to create".to_string()),
|
||||
minimum: Some(1.0),
|
||||
maximum: Some(5.0),
|
||||
default: Some(3.0),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"email".to_string(),
|
||||
McpElicitationPrimitiveSchema::String(McpElicitationStringSchema {
|
||||
type_: McpElicitationStringType::String,
|
||||
title: Some("Email".to_string()),
|
||||
description: Some("Work email address".to_string()),
|
||||
min_length: None,
|
||||
max_length: None,
|
||||
format: Some(McpElicitationStringFormat::Email),
|
||||
default: Some("dev@example.com".to_string()),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"legacyChoice".to_string(),
|
||||
McpElicitationPrimitiveSchema::Enum(McpElicitationEnumSchema::Legacy(
|
||||
McpElicitationLegacyTitledEnumSchema {
|
||||
type_: McpElicitationStringType::String,
|
||||
title: Some("Action".to_string()),
|
||||
description: Some("Legacy titled enum form".to_string()),
|
||||
enum_: vec!["allow".to_string(), "deny".to_string()],
|
||||
enum_names: Some(vec!["Allow".to_string(), "Deny".to_string(),]),
|
||||
default: Some("allow".to_string()),
|
||||
},
|
||||
)),
|
||||
),
|
||||
]),
|
||||
required: Some(vec!["email".to_string(), "confirmed".to_string()]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_request_rejects_null_core_form_schema() {
|
||||
let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form {
|
||||
meta: Some(json!({
|
||||
"persist": "session",
|
||||
})),
|
||||
message: "Allow this request?".to_string(),
|
||||
requested_schema: JsonValue::Null,
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_request_rejects_invalid_core_form_schema() {
|
||||
let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form {
|
||||
meta: None,
|
||||
message: "Allow this request?".to_string(),
|
||||
requested_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirmed": {
|
||||
"type": "object",
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_response_serializes_nullable_content() {
|
||||
let response = McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(response).expect("response should serialize"),
|
||||
json!({
|
||||
"action": "decline",
|
||||
"content": null,
|
||||
"_meta": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_workspace_write_read_only_access() {
|
||||
let readable_root = test_absolute_path();
|
||||
|
||||
@@ -69,6 +69,7 @@ core_test_support = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"elicitation",
|
||||
"server",
|
||||
"transport-streamable-http-server",
|
||||
] }
|
||||
|
||||
@@ -148,12 +148,13 @@ 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 marketplaces reachable from optional `cwds` (unioned into a single list). When `cwds` is omitted, only home-scoped marketplaces are considered. Includes each plugin's current `enabled` state from config (**under development; do not call from production clients yet**).
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**).
|
||||
- `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**).
|
||||
- `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 by `pluginName` and `marketplaceName` (**under development; do not call from production clients yet**).
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry by `pluginName` and `marketplacePath`; on success it returns `appsNeedingAuth` for any plugin-declared apps that still are not accessible in the current ChatGPT auth context (**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).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
@@ -764,6 +765,20 @@ UI guidance for IDEs: surface an approval dialog as soon as the request arrives.
|
||||
|
||||
When the client responds to `item/tool/requestUserInput`, the server emits `serverRequest/resolved` with `{ threadId, requestId }`. If the pending request is cleared by turn start, turn completion, or turn interruption before the client answers, the server emits the same notification for that cleanup.
|
||||
|
||||
### MCP server elicitations
|
||||
|
||||
MCP servers can interrupt a turn and ask the client for structured input via `mcpServer/elicitation/request`.
|
||||
|
||||
Order of messages:
|
||||
|
||||
1. `mcpServer/elicitation/request` (request) — includes `threadId`, nullable `turnId`, `serverName`, and either:
|
||||
- a form request: `{ "mode": "form", "message": "...", "requestedSchema": { ... } }`
|
||||
- a URL request: `{ "mode": "url", "message": "...", "url": "...", "elicitationId": "..." }`
|
||||
2. Client response — `{ "action": "accept", "content": ... }`, `{ "action": "decline", "content": null }`, or `{ "action": "cancel", "content": null }`.
|
||||
3. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt.
|
||||
|
||||
`turnId` is best-effort. When the elicitation is correlated with an active turn, the request includes that turn id; otherwise it is `null`.
|
||||
|
||||
### Dynamic tool calls (experimental)
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
@@ -45,6 +45,9 @@ use codex_app_server_protocol::InterruptConversationResponse;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
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::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallResult;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
@@ -609,6 +612,66 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::ElicitationRequest(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let permission_guard = thread_watch_manager
|
||||
.note_permission_requested(&conversation_id.to_string())
|
||||
.await;
|
||||
let turn_id = match request.turn_id.clone() {
|
||||
Some(turn_id) => Some(turn_id),
|
||||
None => {
|
||||
let state = thread_state.lock().await;
|
||||
state.active_turn_snapshot().map(|turn| turn.id)
|
||||
}
|
||||
};
|
||||
let server_name = request.server_name.clone();
|
||||
let request_body = match request.request.try_into() {
|
||||
Ok(request_body) => request_body,
|
||||
Err(err) => {
|
||||
error!(
|
||||
error = %err,
|
||||
server_name,
|
||||
request_id = ?request.id,
|
||||
"failed to parse typed MCP elicitation schema"
|
||||
);
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::ResolveElicitation {
|
||||
server_name: request.server_name,
|
||||
request_id: request.id,
|
||||
decision: codex_protocol::approvals::ElicitationAction::Cancel,
|
||||
content: None,
|
||||
meta: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit ResolveElicitation: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let params = McpServerElicitationRequestParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
server_name: request.server_name.clone(),
|
||||
request: request_body,
|
||||
};
|
||||
let (pending_request_id, rx) = outgoing
|
||||
.send_request(ServerRequestPayload::McpServerElicitationRequest(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_mcp_server_elicitation_response(
|
||||
request.server_name,
|
||||
request.id,
|
||||
pending_request_id,
|
||||
rx,
|
||||
conversation,
|
||||
thread_state,
|
||||
permission_guard,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let call_id = request.call_id;
|
||||
@@ -1989,6 +2052,73 @@ async fn on_request_user_input_response(
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_mcp_server_elicitation_response(
|
||||
server_name: String,
|
||||
request_id: codex_protocol::mcp::RequestId,
|
||||
pending_request_id: RequestId,
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
conversation: Arc<CodexThread>,
|
||||
thread_state: Arc<Mutex<ThreadState>>,
|
||||
permission_guard: ThreadWatchActiveGuard,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
drop(permission_guard);
|
||||
let response = mcp_server_elicitation_response_from_client_result(response);
|
||||
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::ResolveElicitation {
|
||||
server_name,
|
||||
request_id,
|
||||
decision: response.action.to_core(),
|
||||
content: response.content,
|
||||
meta: response.meta,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit ResolveElicitation: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_server_elicitation_response_from_client_result(
|
||||
response: std::result::Result<ClientRequestResult, oneshot::error::RecvError>,
|
||||
) -> McpServerElicitationRequestResponse {
|
||||
match response {
|
||||
Ok(Ok(value)) => serde_json::from_value::<McpServerElicitationRequestResponse>(value)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("failed to deserialize McpServerElicitationRequestResponse: {err}");
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
}),
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => {
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Cancel,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
@@ -2334,6 +2464,7 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::plan_tool::PlanItemArg;
|
||||
@@ -2378,6 +2509,26 @@ mod tests {
|
||||
assert_eq!(completion_status, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_turn_transition_error_maps_to_cancel() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: -1,
|
||||
message: "client request resolved because the turn state was changed".to_string(),
|
||||
data: Some(serde_json::json!({ "reason": "turnTransition" })),
|
||||
};
|
||||
|
||||
let response = mcp_server_elicitation_response_from_client_result(Ok(Err(error)));
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Cancel,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_begin_maps_to_item_started_resume_agent() {
|
||||
let event = CollabResumeBeginEvent {
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::AppListUpdatedNotification;
|
||||
use codex_app_server_protocol::AppSummary;
|
||||
use codex_app_server_protocol::AppsListParams;
|
||||
use codex_app_server_protocol::AppsListResponse;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
@@ -79,6 +80,11 @@ use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::ProductSurface as ApiProductSurface;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
@@ -182,6 +188,8 @@ use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::types::McpServerTransportConfig;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::connectors::filter_disallowed_connectors;
|
||||
use codex_core::connectors::merge_plugin_apps;
|
||||
use codex_core::default_client::set_default_client_residency_requirement;
|
||||
use codex_core::error::CodexErr;
|
||||
use codex_core::exec::ExecParams;
|
||||
@@ -198,8 +206,12 @@ use codex_core::mcp::collect_mcp_snapshot;
|
||||
use codex_core::mcp::group_tools_by_server;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_core::parse_cursor;
|
||||
use codex_core::plugins::AppConnectorId;
|
||||
use codex_core::plugins::MarketplaceError;
|
||||
use codex_core::plugins::MarketplacePluginSourceSummary;
|
||||
use codex_core::plugins::PluginInstallError as CorePluginInstallError;
|
||||
use codex_core::plugins::PluginInstallRequest;
|
||||
use codex_core::plugins::load_plugin_apps;
|
||||
use codex_core::read_head_for_summary;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::rollout_date_parts;
|
||||
@@ -461,10 +473,14 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
|
||||
async fn load_latest_config(
|
||||
&self,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
) -> Result<Config, JSONRPCErrorError> {
|
||||
let cloud_requirements = self.current_cloud_requirements();
|
||||
let mut config = codex_core::config::ConfigBuilder::default()
|
||||
.cli_overrides(self.cli_overrides.clone())
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_requirements(cloud_requirements)
|
||||
.build()
|
||||
.await
|
||||
@@ -646,6 +662,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PluginList { request_id, params } => {
|
||||
self.plugin_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsRemoteList { request_id, params } => {
|
||||
self.skills_remote_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -3902,7 +3922,7 @@ impl CodexMessageProcessor {
|
||||
params: ExperimentalFeatureListParams,
|
||||
) {
|
||||
let ExperimentalFeatureListParams { cursor, limit } = params;
|
||||
let config = match self.load_latest_config().await {
|
||||
let config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -4017,7 +4037,7 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
|
||||
async fn mcp_server_refresh(&self, request_id: ConnectionRequestId, _params: Option<()>) {
|
||||
let config = match self.load_latest_config().await {
|
||||
let config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -4076,7 +4096,7 @@ impl CodexMessageProcessor {
|
||||
request_id: ConnectionRequestId,
|
||||
params: McpServerOauthLoginParams,
|
||||
) {
|
||||
let config = match self.load_latest_config().await {
|
||||
let config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -4182,7 +4202,7 @@ impl CodexMessageProcessor {
|
||||
let request = request_id.clone();
|
||||
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let config = match self.load_latest_config().await {
|
||||
let config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request, error).await;
|
||||
@@ -4318,6 +4338,30 @@ impl CodexMessageProcessor {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
|
||||
async fn send_marketplace_error(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
err: MarketplaceError,
|
||||
action: &str,
|
||||
) {
|
||||
match err {
|
||||
MarketplaceError::MarketplaceNotFound { .. } => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
}
|
||||
MarketplaceError::Io { .. } => {
|
||||
self.send_internal_error(request_id, format!("failed to {action}: {err}"))
|
||||
.await;
|
||||
}
|
||||
MarketplaceError::InvalidMarketplaceFile { .. }
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::InvalidPlugin(_) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_thread_shutdown(thread: &Arc<CodexThread>) -> ThreadShutdownResult {
|
||||
match thread.submit(Op::Shutdown).await {
|
||||
Ok(_) => {
|
||||
@@ -4581,7 +4625,7 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
|
||||
async fn apps_list(&self, request_id: ConnectionRequestId, params: AppsListParams) {
|
||||
let mut config = match self.load_latest_config().await {
|
||||
let mut config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
@@ -4812,6 +4856,36 @@ impl CodexMessageProcessor {
|
||||
connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded)
|
||||
}
|
||||
|
||||
fn plugin_apps_needing_auth(
|
||||
all_connectors: &[AppInfo],
|
||||
accessible_connectors: &[AppInfo],
|
||||
plugin_apps: &[AppConnectorId],
|
||||
codex_apps_ready: bool,
|
||||
) -> Vec<AppSummary> {
|
||||
if !codex_apps_ready {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let accessible_ids = accessible_connectors
|
||||
.iter()
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let plugin_app_ids = plugin_apps
|
||||
.iter()
|
||||
.map(|connector_id| connector_id.0.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
all_connectors
|
||||
.iter()
|
||||
.filter(|connector| {
|
||||
plugin_app_ids.contains(connector.id.as_str())
|
||||
&& !accessible_ids.contains(connector.id.as_str())
|
||||
})
|
||||
.cloned()
|
||||
.map(AppSummary::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn should_send_app_list_updated_notification(
|
||||
connectors: &[AppInfo],
|
||||
accessible_loaded: bool,
|
||||
@@ -4924,6 +4998,66 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let roots = params.cwds.unwrap_or_default();
|
||||
|
||||
let config = match self.load_latest_config(None).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let data = match tokio::task::spawn_blocking(move || {
|
||||
let marketplaces = plugins_manager.list_marketplaces_for_config(&config, &roots)?;
|
||||
Ok::<Vec<PluginMarketplaceEntry>, MarketplaceError>(
|
||||
marketplaces
|
||||
.into_iter()
|
||||
.map(|marketplace| PluginMarketplaceEntry {
|
||||
name: marketplace.name,
|
||||
path: marketplace.path,
|
||||
plugins: marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| PluginSummary {
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
source: match plugin.source {
|
||||
MarketplacePluginSourceSummary::Local { path } => {
|
||||
PluginSource::Local { path }
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(data)) => data,
|
||||
Ok(Err(err)) => {
|
||||
self.send_marketplace_error(request_id, err, "list marketplace plugins")
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to list marketplace plugins: {err}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, PluginListResponse { marketplaces: data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn skills_remote_list(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -5034,24 +5168,96 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn plugin_install(&self, request_id: ConnectionRequestId, params: PluginInstallParams) {
|
||||
let PluginInstallParams {
|
||||
marketplace_name,
|
||||
marketplace_path,
|
||||
plugin_name,
|
||||
cwd,
|
||||
} = params;
|
||||
let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf);
|
||||
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let request = PluginInstallRequest {
|
||||
plugin_name,
|
||||
marketplace_name,
|
||||
cwd: cwd.unwrap_or_else(|| self.config.cwd.clone()),
|
||||
marketplace_path,
|
||||
};
|
||||
|
||||
match plugins_manager.install_plugin(request).await {
|
||||
Ok(_) => {
|
||||
Ok(result) => {
|
||||
let config = match self.load_latest_config(config_cwd).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to reload config after plugin install, using current config: {err:?}"
|
||||
);
|
||||
self.config.as_ref().clone()
|
||||
}
|
||||
};
|
||||
let plugin_apps = load_plugin_apps(&result.installed_path);
|
||||
let apps_needing_auth = if plugin_apps.is_empty()
|
||||
|| !config.features.enabled(Feature::Apps)
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
|
||||
connectors::list_all_connectors_with_options(&config, true),
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
&config, true
|
||||
),
|
||||
);
|
||||
|
||||
let all_connectors = match all_connectors_result {
|
||||
Ok(connectors) => filter_disallowed_connectors(merge_plugin_apps(
|
||||
connectors,
|
||||
plugin_apps.clone(),
|
||||
)),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"failed to load app metadata after plugin install: {err:#}"
|
||||
);
|
||||
filter_disallowed_connectors(merge_plugin_apps(
|
||||
connectors::list_cached_all_connectors(&config)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
plugin_apps.clone(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let (accessible_connectors, codex_apps_ready) =
|
||||
match accessible_connectors_result {
|
||||
Ok(status) => (status.connectors, status.codex_apps_ready),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"failed to load accessible apps after plugin install: {err:#}"
|
||||
);
|
||||
(
|
||||
connectors::list_cached_accessible_connectors_from_mcp_tools(
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
if !codex_apps_ready {
|
||||
warn!(
|
||||
plugin = result.plugin_id.as_key(),
|
||||
"codex_apps MCP not ready after plugin install; skipping appsNeedingAuth check"
|
||||
);
|
||||
}
|
||||
|
||||
Self::plugin_apps_needing_auth(
|
||||
&all_connectors,
|
||||
&accessible_connectors,
|
||||
&plugin_apps,
|
||||
codex_apps_ready,
|
||||
)
|
||||
};
|
||||
|
||||
plugins_manager.clear_cache();
|
||||
self.thread_manager.skills_manager().clear_cache();
|
||||
self.outgoing
|
||||
.send_response(request_id, PluginInstallResponse {})
|
||||
.send_response(request_id, PluginInstallResponse { apps_needing_auth })
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -5062,6 +5268,10 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
|
||||
match err {
|
||||
CorePluginInstallError::Marketplace(err) => {
|
||||
self.send_marketplace_error(request_id, err, "install plugin")
|
||||
.await;
|
||||
}
|
||||
CorePluginInstallError::Config(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
@@ -5076,7 +5286,13 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
CorePluginInstallError::Marketplace(_) | CorePluginInstallError::Store(_) => {}
|
||||
CorePluginInstallError::Store(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to install plugin: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7267,6 +7483,35 @@ mod tests {
|
||||
validate_dynamic_tools(&tools).expect("valid schema");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_apps_needing_auth_returns_empty_when_codex_apps_is_not_ready() {
|
||||
let all_connectors = vec![AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
CodexMessageProcessor::plugin_apps_needing_auth(
|
||||
&all_connectors,
|
||||
&[],
|
||||
&[AppConnectorId("alpha".to_string())],
|
||||
false,
|
||||
),
|
||||
Vec::<AppSummary>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_resume_override_mismatches_includes_service_tier() {
|
||||
let request = ThreadResumeParams {
|
||||
|
||||
@@ -163,7 +163,6 @@ fn map_network_requirements_to_api(
|
||||
socks_port: network.socks_port,
|
||||
allow_upstream_proxy: network.allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
|
||||
allowed_domains: network.allowed_domains,
|
||||
denied_domains: network.denied_domains,
|
||||
@@ -230,7 +229,6 @@ mod tests {
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
@@ -275,7 +273,6 @@ mod tests {
|
||||
socks_port: Some(1080),
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_admin: Some(false),
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
allowed_domains: Some(vec!["api.openai.com".to_string()]),
|
||||
denied_domains: Some(vec!["example.com".to_string()]),
|
||||
|
||||
@@ -124,6 +124,25 @@ enum ShutdownAction {
|
||||
Finish,
|
||||
}
|
||||
|
||||
async fn shutdown_signal() -> IoResult<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tokio::signal::unix::signal;
|
||||
|
||||
let mut term = signal(SignalKind::terminate())?;
|
||||
tokio::select! {
|
||||
ctrl_c_result = tokio::signal::ctrl_c() => ctrl_c_result,
|
||||
_ = term.recv() => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::signal::ctrl_c().await
|
||||
}
|
||||
}
|
||||
|
||||
impl ShutdownState {
|
||||
fn requested(&self) -> bool {
|
||||
self.requested
|
||||
@@ -133,7 +152,7 @@ impl ShutdownState {
|
||||
self.forced
|
||||
}
|
||||
|
||||
fn on_ctrl_c(&mut self, connection_count: usize, running_turn_count: usize) {
|
||||
fn on_signal(&mut self, connection_count: usize, running_turn_count: usize) {
|
||||
if self.requested {
|
||||
self.forced = true;
|
||||
return;
|
||||
@@ -142,7 +161,7 @@ impl ShutdownState {
|
||||
self.requested = true;
|
||||
self.last_logged_running_turn_count = None;
|
||||
info!(
|
||||
"received Ctrl-C; entering graceful restart drain (connections={}, runningAssistantTurns={}, requests still accepted until no assistant turns are running)",
|
||||
"received shutdown signal; entering graceful restart drain (connections={}, runningAssistantTurns={}, requests still accepted until no assistant turns are running)",
|
||||
connection_count, running_turn_count,
|
||||
);
|
||||
}
|
||||
@@ -155,11 +174,11 @@ impl ShutdownState {
|
||||
if self.forced || running_turn_count == 0 {
|
||||
if self.forced {
|
||||
info!(
|
||||
"received second Ctrl-C; forcing restart with {running_turn_count} running assistant turn(s) and {connection_count} connection(s)"
|
||||
"received second shutdown signal; forcing restart with {running_turn_count} running assistant turn(s) and {connection_count} connection(s)"
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Ctrl-C restart: no assistant turns running; stopping acceptor and disconnecting {connection_count} connection(s)"
|
||||
"shutdown signal restart: no assistant turns running; stopping acceptor and disconnecting {connection_count} connection(s)"
|
||||
);
|
||||
}
|
||||
return ShutdownAction::Finish;
|
||||
@@ -167,7 +186,7 @@ impl ShutdownState {
|
||||
|
||||
if self.last_logged_running_turn_count != Some(running_turn_count) {
|
||||
info!(
|
||||
"Ctrl-C restart: waiting for {running_turn_count} running assistant turn(s) to finish"
|
||||
"shutdown signal restart: waiting for {running_turn_count} running assistant turn(s) to finish"
|
||||
);
|
||||
self.last_logged_running_turn_count = Some(running_turn_count);
|
||||
}
|
||||
@@ -359,8 +378,7 @@ pub async fn run_main_with_transport(
|
||||
};
|
||||
let single_client_mode = matches!(&transport_runtime, TransportRuntime::Stdio);
|
||||
let shutdown_when_no_connections = single_client_mode;
|
||||
let graceful_ctrl_c_restart_enabled = !single_client_mode;
|
||||
|
||||
let graceful_signal_restart_enabled = !single_client_mode;
|
||||
// Parse CLI overrides once and derive the base Config eagerly so later
|
||||
// components do not need to work with raw TOML values.
|
||||
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
|
||||
@@ -614,14 +632,14 @@ pub async fn run_main_with_transport(
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
ctrl_c_result = tokio::signal::ctrl_c(), if graceful_ctrl_c_restart_enabled && !shutdown_state.forced() => {
|
||||
if let Err(err) = ctrl_c_result {
|
||||
warn!("failed to listen for Ctrl-C during graceful restart drain: {err}");
|
||||
shutdown_signal_result = shutdown_signal(), if graceful_signal_restart_enabled && !shutdown_state.forced() => {
|
||||
if let Err(err) = shutdown_signal_result {
|
||||
warn!("failed to listen for shutdown signal during graceful restart drain: {err}");
|
||||
}
|
||||
let running_turn_count = *running_turn_count_rx.borrow();
|
||||
shutdown_state.on_ctrl_c(connections.len(), running_turn_count);
|
||||
shutdown_state.on_signal(connections.len(), running_turn_count);
|
||||
}
|
||||
changed = running_turn_count_rx.changed(), if graceful_ctrl_c_restart_enabled && shutdown_state.requested() => {
|
||||
changed = running_turn_count_rx.changed(), if graceful_signal_restart_enabled && shutdown_state.requested() => {
|
||||
if changed.is_err() {
|
||||
warn!("running-turn watcher closed during graceful restart drain");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
@@ -439,6 +441,32 @@ impl McpProcess {
|
||||
self.send_request("skills/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/install` JSON-RPC request.
|
||||
pub async fn send_plugin_install_request(
|
||||
&mut self,
|
||||
params: PluginInstallParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/install", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/list` JSON-RPC request.
|
||||
pub async fn send_plugin_list_request(
|
||||
&mut self,
|
||||
params: PluginListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("plugin/list", params).await
|
||||
}
|
||||
|
||||
/// Send a JSON-RPC request with raw params for protocol-level validation tests.
|
||||
pub async fn send_raw_request(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<i64> {
|
||||
self.send_request(method, params).await
|
||||
}
|
||||
/// Send a `collaborationMode/list` JSON-RPC request.
|
||||
pub async fn send_list_collaboration_modes_request(
|
||||
&mut self,
|
||||
|
||||
@@ -97,6 +97,7 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu
|
||||
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) =
|
||||
@@ -199,6 +200,7 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> {
|
||||
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) =
|
||||
@@ -308,6 +310,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
@@ -322,6 +325,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -370,6 +374,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
let first_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
@@ -389,6 +394,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
@@ -403,6 +409,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -443,6 +450,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
@@ -457,6 +465,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -516,6 +525,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
@@ -530,6 +540,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -564,6 +575,7 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
let (server_url, server_handle) = start_apps_server_with_delays(
|
||||
connectors.clone(),
|
||||
@@ -619,6 +631,7 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
let update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
@@ -653,6 +666,7 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
@@ -667,6 +681,7 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -724,6 +739,7 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(first_page, expected_first);
|
||||
@@ -767,6 +783,7 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(second_page, expected_second);
|
||||
@@ -791,6 +808,7 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result
|
||||
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) =
|
||||
@@ -895,6 +913,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
@@ -909,6 +928,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
];
|
||||
let initial_tools = vec![connector_tool("beta", "Beta App")?];
|
||||
@@ -958,6 +978,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}]
|
||||
);
|
||||
|
||||
@@ -978,6 +999,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
@@ -992,6 +1014,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -1021,6 +1044,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}]);
|
||||
server_control.set_tools(Vec::new());
|
||||
|
||||
@@ -1050,6 +1074,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
@@ -1064,6 +1089,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -1091,6 +1117,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
let second_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(second_update.data, expected_final);
|
||||
|
||||
@@ -83,6 +83,57 @@ async fn websocket_transport_second_ctrl_c_forces_exit_while_turn_running() -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_sigterm_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigterm(&process)?;
|
||||
assert_process_does_not_exit_within(&mut process, Duration::from_millis(300)).await?;
|
||||
|
||||
let status = wait_for_process_exit_within(
|
||||
&mut process,
|
||||
Duration::from_secs(10),
|
||||
"timed out waiting for graceful SIGTERM restart shutdown",
|
||||
)
|
||||
.await?;
|
||||
assert!(status.success(), "expected graceful exit, got {status}");
|
||||
|
||||
expect_websocket_disconnect(&mut ws).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_second_sigterm_forces_exit_while_turn_running() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigterm(&process)?;
|
||||
assert_process_does_not_exit_within(&mut process, Duration::from_millis(300)).await?;
|
||||
|
||||
send_sigterm(&process)?;
|
||||
let status = wait_for_process_exit_within(
|
||||
&mut process,
|
||||
Duration::from_secs(2),
|
||||
"timed out waiting for forced SIGTERM restart shutdown",
|
||||
)
|
||||
.await?;
|
||||
assert!(status.success(), "expected graceful exit, got {status}");
|
||||
|
||||
expect_websocket_disconnect(&mut ws).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct GracefulCtrlCFixture {
|
||||
_codex_home: TempDir,
|
||||
_server: wiremock::MockServer,
|
||||
@@ -180,16 +231,24 @@ async fn wait_for_responses_post(server: &wiremock::MockServer, wait_for: Durati
|
||||
}
|
||||
|
||||
fn send_sigint(process: &Child) -> Result<()> {
|
||||
send_signal(process, "-INT")
|
||||
}
|
||||
|
||||
fn send_sigterm(process: &Child) -> Result<()> {
|
||||
send_signal(process, "-TERM")
|
||||
}
|
||||
|
||||
fn send_signal(process: &Child, signal: &str) -> Result<()> {
|
||||
let pid = process
|
||||
.id()
|
||||
.context("websocket app-server process has no pid")?;
|
||||
let status = StdCommand::new("kill")
|
||||
.arg("-INT")
|
||||
.arg(signal)
|
||||
.arg(pid.to_string())
|
||||
.status()
|
||||
.context("failed to invoke kill -INT")?;
|
||||
.with_context(|| format!("failed to invoke kill {signal}"))?;
|
||||
if !status.success() {
|
||||
bail!("kill -INT exited with {status}");
|
||||
bail!("kill {signal} exited with {status}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::McpElicitationSchema;
|
||||
use codex_app_server_protocol::McpServerElicitationAction;
|
||||
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerRequestResolvedNotification;
|
||||
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::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::BooleanSchema;
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::model::Content;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationSchema;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::ListToolsResult;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::PrimitiveSchema;
|
||||
use rmcp::model::ServerCapabilities;
|
||||
use rmcp::model::ServerInfo;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use rmcp::service::RequestContext;
|
||||
use rmcp::service::RoleServer;
|
||||
use rmcp::transport::StreamableHttpServerConfig;
|
||||
use rmcp::transport::StreamableHttpService;
|
||||
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const CONNECTOR_ID: &str = "calendar";
|
||||
const CONNECTOR_NAME: &str = "Calendar";
|
||||
const TOOL_NAME: &str = "calendar_confirm_action";
|
||||
const QUALIFIED_TOOL_NAME: &str = "mcp__codex_apps__calendar_confirm_action";
|
||||
const TOOL_CALL_ID: &str = "call-calendar-confirm";
|
||||
const ELICITATION_MESSAGE: &str = "Allow this request?";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn mcp_server_elicitation_round_trip() -> Result<()> {
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let tool_call_arguments = serde_json::to_string(&json!({}))?;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
&responses_server,
|
||||
vec![
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-0"),
|
||||
responses::ev_assistant_message("msg-0", "Warmup"),
|
||||
responses::ev_completed("resp-0"),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call(
|
||||
TOOL_CALL_ID,
|
||||
QUALIFIED_TOOL_NAME,
|
||||
&tool_call_arguments,
|
||||
),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-2"),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (apps_server_url, apps_server_handle) = start_apps_server().await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config_toml(codex_home.path(), &responses_server.uri(), &apps_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 mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;
|
||||
|
||||
let warmup_turn_start_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Warm up connectors.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let warmup_turn_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(warmup_turn_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response(warmup_turn_start_resp)?;
|
||||
|
||||
let warmup_completed = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let warmup_completed: TurnCompletedNotification = serde_json::from_value(
|
||||
warmup_completed
|
||||
.params
|
||||
.clone()
|
||||
.expect("warmup turn/completed params"),
|
||||
)?;
|
||||
assert_eq!(warmup_completed.thread_id, thread.id);
|
||||
assert_eq!(warmup_completed.turn.status, TurnStatus::Completed);
|
||||
|
||||
let turn_start_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec to run the calendar tool.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response(turn_start_resp)?;
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::McpServerElicitationRequest { request_id, params } = server_req else {
|
||||
panic!("expected McpServerElicitationRequest request, got: {server_req:?}");
|
||||
};
|
||||
let requested_schema: McpElicitationSchema = serde_json::from_value(serde_json::to_value(
|
||||
ElicitationSchema::builder()
|
||||
.required_property("confirmed", PrimitiveSchema::Boolean(BooleanSchema::new()))
|
||||
.build()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
)?)?;
|
||||
|
||||
assert_eq!(
|
||||
params,
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_id: Some(turn.id.clone()),
|
||||
server_name: "codex_apps".to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: None,
|
||||
message: ELICITATION_MESSAGE.to_string(),
|
||||
requested_schema,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let resolved_request_id = request_id.clone();
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(McpServerElicitationRequestResponse {
|
||||
action: McpServerElicitationAction::Accept,
|
||||
content: Some(json!({
|
||||
"confirmed": true,
|
||||
})),
|
||||
meta: None,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut saw_resolved = false;
|
||||
loop {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match notification.method.as_str() {
|
||||
"serverRequest/resolved" => {
|
||||
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("serverRequest/resolved params"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved,
|
||||
ServerRequestResolvedNotification {
|
||||
thread_id: thread.id.clone(),
|
||||
request_id: resolved_request_id.clone(),
|
||||
}
|
||||
);
|
||||
saw_resolved = true;
|
||||
}
|
||||
"turn/completed" => {
|
||||
let completed: TurnCompletedNotification = serde_json::from_value(
|
||||
notification.params.clone().expect("turn/completed params"),
|
||||
)?;
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.id, turn.id);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let function_call_output = requests[2].function_call_output(TOOL_CALL_ID);
|
||||
assert_eq!(
|
||||
function_call_output.get("type"),
|
||||
Some(&Value::String("function_call_output".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
function_call_output.get("call_id"),
|
||||
Some(&Value::String(TOOL_CALL_ID.to_string()))
|
||||
);
|
||||
let output = function_call_output
|
||||
.get("output")
|
||||
.and_then(Value::as_str)
|
||||
.expect("function_call_output output should be a JSON string");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(output)?,
|
||||
json!([{
|
||||
"type": "text",
|
||||
"text": "accepted"
|
||||
}])
|
||||
);
|
||||
|
||||
apps_server_handle.abort();
|
||||
let _ = apps_server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppsServerState {
|
||||
expected_bearer: String,
|
||||
expected_account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ElicitationAppsMcpServer;
|
||||
|
||||
impl ServerHandler for ElicitationAppsMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: rmcp::model::ProtocolVersion::V_2025_06_18,
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..ServerInfo::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<ListToolsResult, rmcp::ErrorData> {
|
||||
let input_schema: JsonObject = serde_json::from_value(json!({
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}))
|
||||
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?;
|
||||
|
||||
let mut tool = Tool::new(
|
||||
Cow::Borrowed(TOOL_NAME),
|
||||
Cow::Borrowed("Confirm a calendar action."),
|
||||
Arc::new(input_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(ListToolsResult {
|
||||
tools: vec![tool],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
_request: CallToolRequestParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
let requested_schema = ElicitationSchema::builder()
|
||||
.required_property("confirmed", PrimitiveSchema::Boolean(BooleanSchema::new()))
|
||||
.build()
|
||||
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?;
|
||||
|
||||
let result = context
|
||||
.peer
|
||||
.create_elicitation(CreateElicitationRequestParams::FormElicitationParams {
|
||||
meta: None,
|
||||
message: ELICITATION_MESSAGE.to_string(),
|
||||
requested_schema,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?;
|
||||
|
||||
let output = match result.action {
|
||||
ElicitationAction::Accept => {
|
||||
assert_eq!(
|
||||
result.content,
|
||||
Some(json!({
|
||||
"confirmed": true,
|
||||
}))
|
||||
);
|
||||
"accepted"
|
||||
}
|
||||
ElicitationAction::Decline => "declined",
|
||||
ElicitationAction::Cancel => "cancelled",
|
||||
};
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(output)]))
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_apps_server() -> Result<(String, JoinHandle<()>)> {
|
||||
let state = Arc::new(AppsServerState {
|
||||
expected_bearer: "Bearer chatgpt-token".to_string(),
|
||||
expected_account_id: "account-123".to_string(),
|
||||
});
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
let mcp_service = StreamableHttpService::new(
|
||||
move || Ok(ElicitationAppsMcpServer),
|
||||
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<Json<serde_json::Value>, StatusCode> {
|
||||
let bearer_ok = headers
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == state.expected_bearer);
|
||||
let account_ok = headers
|
||||
.get("chatgpt-account-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value == state.expected_account_id);
|
||||
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 {
|
||||
Ok(Json(json!({
|
||||
"apps": [{
|
||||
"id": CONNECTOR_ID,
|
||||
"name": CONNECTOR_NAME,
|
||||
"description": "Calendar connector",
|
||||
"logo_url": null,
|
||||
"logo_url_dark": null,
|
||||
"distribution_channel": null,
|
||||
"branding": null,
|
||||
"app_metadata": null,
|
||||
"labels": null,
|
||||
"install_url": null,
|
||||
"is_accessible": false,
|
||||
"is_enabled": true
|
||||
}],
|
||||
"next_token": null
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_config_toml(
|
||||
codex_home: &std::path::Path,
|
||||
responses_server_uri: &str,
|
||||
apps_server_url: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "untrusted"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
chatgpt_base_url = "{apps_server_url}"
|
||||
mcp_oauth_credentials_store = "file"
|
||||
|
||||
[features]
|
||||
apps = true
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{responses_server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -11,9 +11,12 @@ mod dynamic_tools;
|
||||
mod experimental_api;
|
||||
mod experimental_feature_list;
|
||||
mod initialize;
|
||||
mod mcp_server_elicitation;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod plan_item;
|
||||
mod plugin_install;
|
||||
mod plugin_list;
|
||||
mod rate_limits;
|
||||
mod realtime_conversation;
|
||||
mod request_user_input;
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
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::AppSummary;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
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);
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/install",
|
||||
Some(serde_json::json!({
|
||||
"marketplacePath": "relative-marketplace.json",
|
||||
"pluginName": "missing-plugin",
|
||||
})),
|
||||
)
|
||||
.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("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
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: AbsolutePathBuf::try_from(
|
||||
codex_home.path().join("missing-marketplace.json"),
|
||||
)?,
|
||||
plugin_name: "missing-plugin".to_string(),
|
||||
})
|
||||
.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("marketplace file"));
|
||||
assert!(err.error.message.contains("does not exist"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_apps_needing_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_install_request(PluginInstallParams {
|
||||
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: PluginInstallResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
PluginInstallResponse {
|
||||
apps_needing_auth: vec![AppSummary {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_filters_disallowed_apps_needing_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(),
|
||||
}];
|
||||
let (server_url, server_handle) = start_apps_server(connectors, Vec::new()).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", "asdk_app_6938a94a61d881918ef32cb999ff937c"],
|
||||
)?;
|
||||
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(),
|
||||
})
|
||||
.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,
|
||||
PluginInstallResponse {
|
||||
apps_needing_auth: vec![AppSummary {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppsServerState {
|
||||
response: Arc<StdMutex<serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PluginInstallMcpServer {
|
||||
tools: Arc<StdMutex<Vec<Tool>>>,
|
||||
}
|
||||
|
||||
impl ServerHandler for PluginInstallMcpServer {
|
||||
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(PluginInstallMcpServer {
|
||||
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]
|
||||
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(".agents/plugins").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(())
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_returns_invalid_request_for_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"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
"{not json",
|
||||
)?;
|
||||
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.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("invalid marketplace file"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_rejects_relative_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"plugin/list",
|
||||
Some(serde_json::json!({
|
||||
"cwds": ["relative-root"],
|
||||
})),
|
||||
)
|
||||
.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("Invalid request"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "home-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./home-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams { cwds: None })
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: PluginListResponse = to_response(response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_includes_enabled_state_from_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
repo_root.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "enabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./enabled-plugin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disabled-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./disabled-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."enabled-plugin@codex-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."disabled-plugin@codex-curated"]
|
||||
enabled = false
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let marketplace = response
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.find(|marketplace| {
|
||||
marketplace.path == repo_root.path().join(".agents/plugins/marketplace.json")
|
||||
})
|
||||
.expect("expected repo marketplace entry");
|
||||
|
||||
assert_eq!(marketplace.name, "codex-curated");
|
||||
assert_eq!(marketplace.plugins.len(), 2);
|
||||
assert_eq!(marketplace.plugins[0].name, "enabled-plugin");
|
||||
assert_eq!(marketplace.plugins[0].enabled, true);
|
||||
assert_eq!(marketplace.plugins[1].name, "disabled-plugin");
|
||||
assert_eq!(marketplace.plugins[1].enabled, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_uses_home_config_for_enabled_state() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::create_dir_all(codex_home.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./shared-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."shared-plugin@codex-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let workspace_enabled = TempDir::new()?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".git"))?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".agents/plugins"))?;
|
||||
std::fs::write(
|
||||
workspace_enabled
|
||||
.path()
|
||||
.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "shared-plugin",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./shared-plugin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(workspace_enabled.path().join(".codex"))?;
|
||||
std::fs::write(
|
||||
workspace_enabled.path().join(".codex/config.toml"),
|
||||
r#"[plugins."shared-plugin@codex-curated"]
|
||||
enabled = false
|
||||
"#,
|
||||
)?;
|
||||
set_project_trust_level(
|
||||
codex_home.path(),
|
||||
workspace_enabled.path(),
|
||||
TrustLevel::Trusted,
|
||||
)?;
|
||||
|
||||
let workspace_default = TempDir::new()?;
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: Some(vec![
|
||||
AbsolutePathBuf::try_from(workspace_enabled.path())?,
|
||||
AbsolutePathBuf::try_from(workspace_default.path())?,
|
||||
]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let shared_plugin = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.flat_map(|marketplace| marketplace.plugins.iter())
|
||||
.find(|plugin| plugin.name == "shared-plugin")
|
||||
.expect("expected shared-plugin entry");
|
||||
assert_eq!(shared_plugin.enabled, true);
|
||||
Ok(())
|
||||
}
|
||||
@@ -36,6 +36,7 @@ use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const STARTUP_CONTEXT_HEADER: &str = "Startup context from Codex.";
|
||||
|
||||
#[tokio::test]
|
||||
async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
@@ -114,6 +115,18 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
assert_eq!(started.thread_id, thread_start.thread.id);
|
||||
assert!(started.session_id.is_some());
|
||||
|
||||
let startup_context_request = realtime_server.wait_for_request(0, 0).await;
|
||||
assert_eq!(
|
||||
startup_context_request.body_json()["type"].as_str(),
|
||||
Some("session.update")
|
||||
);
|
||||
assert!(
|
||||
startup_context_request.body_json()["session"]["instructions"]
|
||||
.as_str()
|
||||
.context("expected startup context instructions")?
|
||||
.contains(STARTUP_CONTEXT_HEADER)
|
||||
);
|
||||
|
||||
let audio_append_request_id = mcp
|
||||
.send_thread_realtime_append_audio_request(ThreadRealtimeAppendAudioParams {
|
||||
thread_id: started.thread_id.clone(),
|
||||
@@ -183,6 +196,12 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
connection[0].body_json()["type"].as_str(),
|
||||
Some("session.update")
|
||||
);
|
||||
assert!(
|
||||
connection[0].body_json()["session"]["instructions"]
|
||||
.as_str()
|
||||
.context("expected startup context instructions")?
|
||||
.contains(STARTUP_CONTEXT_HEADER)
|
||||
);
|
||||
let mut request_types = [
|
||||
connection[1].body_json()["type"]
|
||||
.as_str()
|
||||
|
||||
@@ -446,6 +446,7 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo {
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +484,7 @@ mod tests {
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,6 +542,7 @@ mod tests {
|
||||
install_url: Some(connector_install_url(id, id)),
|
||||
is_accessible,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ impl RealtimeWebsocketWriter {
|
||||
},
|
||||
},
|
||||
output: SessionAudioOutput {
|
||||
voice: "mundo".to_string(),
|
||||
voice: "fathom".to_string(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -793,7 +793,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
first_json["session"]["audio"]["output"]["voice"],
|
||||
Value::String("mundo".to_string())
|
||||
Value::String("fathom".to_string())
|
||||
);
|
||||
|
||||
ws.send(Message::Text(
|
||||
|
||||
@@ -136,7 +136,6 @@ pub struct NetworkRequirementsToml {
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
@@ -152,7 +151,6 @@ pub struct NetworkConstraints {
|
||||
pub socks_port: Option<u16>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
@@ -168,7 +166,6 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
@@ -181,7 +178,6 @@ impl From<NetworkRequirementsToml> for NetworkConstraints {
|
||||
socks_port,
|
||||
allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_non_loopback_admin,
|
||||
dangerously_allow_all_unix_sockets,
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
|
||||
@@ -443,6 +443,9 @@
|
||||
"steer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_call_mcp_elicitation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"undo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -794,9 +797,6 @@
|
||||
"NetworkToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"admin_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"allow_local_binding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -818,9 +818,6 @@
|
||||
"dangerously_allow_all_unix_sockets": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_non_loopback_admin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_non_loopback_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1834,6 +1831,9 @@
|
||||
"steer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_call_mcp_elicitation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"undo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+146
-48
File diff suppressed because one or more lines are too long
@@ -167,7 +167,7 @@ pub(crate) mod tools {
|
||||
#[serde(rename = "local_shell")]
|
||||
LocalShell {},
|
||||
#[serde(rename = "image_generation")]
|
||||
ImageGeneration {},
|
||||
ImageGeneration { output_format: String },
|
||||
// TODO: Understand why we get an error on web_search although the API docs say it's supported.
|
||||
// https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C
|
||||
// The `external_web_access` field determines whether the web search is over cached or live content.
|
||||
@@ -188,7 +188,7 @@ pub(crate) mod tools {
|
||||
match self {
|
||||
ToolSpec::Function(tool) => tool.name.as_str(),
|
||||
ToolSpec::LocalShell {} => "local_shell",
|
||||
ToolSpec::ImageGeneration {} => "image_generation",
|
||||
ToolSpec::ImageGeneration { .. } => "image_generation",
|
||||
ToolSpec::WebSearch { .. } => "web_search",
|
||||
ToolSpec::Freeform(tool) => tool.name.as_str(),
|
||||
}
|
||||
|
||||
+630
-108
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -78,6 +79,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
let previous_model = "previous-rollout-model";
|
||||
let mut previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -739,6 +741,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -810,6 +813,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
.expect("serialize seeded reference context item"),
|
||||
serde_json::to_value(Some(TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -838,6 +842,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -943,6 +948,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
let unmatched_abort_turn_id = "other-turn".to_string();
|
||||
let current_context_item = TurnContextItem {
|
||||
turn_id: Some(current_turn_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1044,6 +1050,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1187,6 +1194,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
let previous_model = "previous-rollout-model";
|
||||
let previous_context_item = TurnContextItem {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
|
||||
@@ -219,10 +219,6 @@ pub struct Config {
|
||||
/// using backend-specific headers or URLs to enforce this.
|
||||
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,
|
||||
|
||||
/// True if the user passed in an override or set a value in config.toml
|
||||
/// for either of approval_policy or sandbox_mode.
|
||||
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
|
||||
|
||||
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
||||
/// suppressed from the frontend output. This can reduce visual noise when
|
||||
/// users are only interested in the final agent responses.
|
||||
@@ -1789,9 +1785,6 @@ impl Config {
|
||||
let active_project = cfg
|
||||
.get_active_project(&resolved_cwd)
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
let sandbox_mode_was_explicit = sandbox_mode.is_some()
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
|
||||
let windows_sandbox_level = match windows_sandbox_mode {
|
||||
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
@@ -1821,9 +1814,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
let mut approval_policy = approval_policy_override
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
@@ -1836,9 +1826,7 @@ impl Config {
|
||||
AskForApproval::default()
|
||||
}
|
||||
});
|
||||
if !approval_policy_was_explicit
|
||||
&& let Err(err) = constrained_approval_policy.can_set(&approval_policy)
|
||||
{
|
||||
if let Err(err) = constrained_approval_policy.can_set(&approval_policy) {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default approval policy is disallowed by requirements; falling back to required default"
|
||||
@@ -1847,10 +1835,6 @@ impl Config {
|
||||
}
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
// TODO(dylan): We should be able to leverage ConfigLayerStack so that
|
||||
// we can reliably check this at every config level.
|
||||
let did_user_set_custom_approval_policy_or_sandbox_mode =
|
||||
approval_policy_was_explicit || sandbox_mode_was_explicit;
|
||||
|
||||
let mut model_providers = built_in_model_providers();
|
||||
// Merge user-defined providers into the built-in list.
|
||||
@@ -2155,7 +2139,6 @@ impl Config {
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
enforce_residency: enforce_residency.value,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode,
|
||||
notify: cfg.notify,
|
||||
user_instructions,
|
||||
base_instructions,
|
||||
@@ -2727,13 +2710,11 @@ allowed_domains = ["openai.com"]
|
||||
NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
admin_url: None,
|
||||
enable_socks5: Some(false),
|
||||
socks_url: None,
|
||||
enable_socks5_udp: None,
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_non_loopback_admin: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
mode: None,
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
@@ -3489,7 +3470,6 @@ profile = "project"
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::DangerFullAccess
|
||||
));
|
||||
assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5195,7 +5175,6 @@ model_verbosity = "high"
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
@@ -5325,7 +5304,6 @@ model_verbosity = "high"
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
@@ -5453,7 +5431,6 @@ model_verbosity = "high"
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
@@ -5567,7 +5544,6 @@ model_verbosity = "high"
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
},
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
cwd: fixture.cwd(),
|
||||
@@ -5649,24 +5625,6 @@ model_verbosity = "high"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_did_user_set_custom_approval_policy_or_sandbox_mode_defaults_no() -> anyhow::Result<()>
|
||||
{
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
fixture.cfg.clone(),
|
||||
ConfigOverrides {
|
||||
..Default::default()
|
||||
},
|
||||
fixture.codex_home(),
|
||||
)?;
|
||||
|
||||
assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()>
|
||||
{
|
||||
|
||||
@@ -183,14 +183,6 @@ impl NetworkProxySpec {
|
||||
constraints.dangerously_allow_non_loopback_proxy =
|
||||
Some(dangerously_allow_non_loopback_proxy);
|
||||
}
|
||||
if let Some(dangerously_allow_non_loopback_admin) =
|
||||
requirements.dangerously_allow_non_loopback_admin
|
||||
{
|
||||
config.network.dangerously_allow_non_loopback_admin =
|
||||
dangerously_allow_non_loopback_admin;
|
||||
constraints.dangerously_allow_non_loopback_admin =
|
||||
Some(dangerously_allow_non_loopback_admin);
|
||||
}
|
||||
if let Some(dangerously_allow_all_unix_sockets) =
|
||||
requirements.dangerously_allow_all_unix_sockets
|
||||
{
|
||||
|
||||
@@ -17,13 +17,11 @@ pub struct PermissionsToml {
|
||||
pub struct NetworkToml {
|
||||
pub enabled: Option<bool>,
|
||||
pub proxy_url: Option<String>,
|
||||
pub admin_url: Option<String>,
|
||||
pub enable_socks5: Option<bool>,
|
||||
pub socks_url: Option<String>,
|
||||
pub enable_socks5_udp: Option<bool>,
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[schemars(with = "Option<NetworkModeSchema>")]
|
||||
pub mode: Option<NetworkMode>,
|
||||
@@ -48,9 +46,6 @@ impl NetworkToml {
|
||||
if let Some(proxy_url) = self.proxy_url.as_ref() {
|
||||
config.network.proxy_url = proxy_url.clone();
|
||||
}
|
||||
if let Some(admin_url) = self.admin_url.as_ref() {
|
||||
config.network.admin_url = admin_url.clone();
|
||||
}
|
||||
if let Some(enable_socks5) = self.enable_socks5 {
|
||||
config.network.enable_socks5 = enable_socks5;
|
||||
}
|
||||
@@ -69,12 +64,6 @@ impl NetworkToml {
|
||||
config.network.dangerously_allow_non_loopback_proxy =
|
||||
dangerously_allow_non_loopback_proxy;
|
||||
}
|
||||
if let Some(dangerously_allow_non_loopback_admin) =
|
||||
self.dangerously_allow_non_loopback_admin
|
||||
{
|
||||
config.network.dangerously_allow_non_loopback_admin =
|
||||
dangerously_allow_non_loopback_admin;
|
||||
}
|
||||
if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets {
|
||||
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
|
||||
}
|
||||
|
||||
+204
-25
@@ -1,7 +1,9 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
@@ -26,11 +28,14 @@ use crate::default_client::is_first_party_chat_originator;
|
||||
use crate::default_client::originator;
|
||||
use crate::features::Feature;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::with_codex_apps_mcp;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::plugins::AppConnectorId;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::token_data::TokenData;
|
||||
|
||||
pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600);
|
||||
@@ -123,9 +128,12 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
let auth_manager = auth_manager_from_config(config);
|
||||
let auth = auth_manager.auth().await;
|
||||
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config);
|
||||
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
|
||||
{
|
||||
let cached_connectors = filter_disallowed_connectors(cached_connectors);
|
||||
let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance);
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: cached_connectors,
|
||||
codex_apps_ready: true,
|
||||
@@ -162,6 +170,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
ToolPluginProvenance::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -210,6 +219,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
if codex_apps_ready || !accessible_connectors.is_empty() {
|
||||
write_cached_accessible_connectors(cache_key, &accessible_connectors);
|
||||
}
|
||||
let accessible_connectors =
|
||||
with_app_plugin_sources(accessible_connectors, &tool_plugin_provenance);
|
||||
Ok(AccessibleConnectorsStatus {
|
||||
connectors: accessible_connectors,
|
||||
codex_apps_ready,
|
||||
@@ -291,13 +302,19 @@ pub fn connector_mention_slug(connector: &AppInfo) -> String {
|
||||
pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
mcp_tools: &HashMap<String, crate::mcp_connection_manager::ToolInfo>,
|
||||
) -> Vec<AppInfo> {
|
||||
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
||||
// can be derived here instead of requiring a separate enrichment pass.
|
||||
let tools = mcp_tools.values().filter_map(|tool| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
let connector_id = tool.connector_id.as_deref()?;
|
||||
let connector_name = normalize_connector_value(tool.connector_name.as_deref());
|
||||
Some((connector_id.to_string(), connector_name))
|
||||
Some((
|
||||
connector_id.to_string(),
|
||||
connector_name,
|
||||
tool.plugin_display_names.clone(),
|
||||
))
|
||||
});
|
||||
collect_accessible_connectors(tools)
|
||||
}
|
||||
@@ -334,6 +351,9 @@ pub fn merge_connectors(
|
||||
if existing.distribution_channel.is_none() && connector.distribution_channel.is_some() {
|
||||
existing.distribution_channel = connector.distribution_channel;
|
||||
}
|
||||
existing
|
||||
.plugin_display_names
|
||||
.extend(connector.plugin_display_names);
|
||||
} else {
|
||||
merged.insert(connector_id, connector);
|
||||
}
|
||||
@@ -344,6 +364,8 @@ pub fn merge_connectors(
|
||||
if connector.install_url.is_none() {
|
||||
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
|
||||
}
|
||||
connector.plugin_display_names.sort_unstable();
|
||||
connector.plugin_display_names.dedup();
|
||||
}
|
||||
merged.sort_by(|left, right| {
|
||||
right
|
||||
@@ -407,6 +429,18 @@ pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) ->
|
||||
connectors
|
||||
}
|
||||
|
||||
pub fn with_app_plugin_sources(
|
||||
mut connectors: Vec<AppInfo>,
|
||||
tool_plugin_provenance: &ToolPluginProvenance,
|
||||
) -> Vec<AppInfo> {
|
||||
for connector in &mut connectors {
|
||||
connector.plugin_display_names = tool_plugin_provenance
|
||||
.plugin_display_names_for_connector_id(connector.id.as_str())
|
||||
.to_vec();
|
||||
}
|
||||
connectors
|
||||
}
|
||||
|
||||
pub(crate) fn app_tool_policy(
|
||||
config: &Config,
|
||||
connector_id: Option<&str>,
|
||||
@@ -579,35 +613,49 @@ fn app_tool_policy_from_apps_config(
|
||||
|
||||
fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = (String, Option<String>)>,
|
||||
I: IntoIterator<Item = (String, Option<String>, Vec<String>)>,
|
||||
{
|
||||
let mut connectors: HashMap<String, String> = HashMap::new();
|
||||
for (connector_id, connector_name) in tools {
|
||||
let mut connectors: HashMap<String, (String, BTreeSet<String>)> = HashMap::new();
|
||||
for (connector_id, connector_name, plugin_display_names) in tools {
|
||||
let connector_name = connector_name.unwrap_or_else(|| connector_id.clone());
|
||||
if let Some(existing_name) = connectors.get_mut(&connector_id) {
|
||||
if let Some((existing_name, existing_plugin_display_names)) =
|
||||
connectors.get_mut(&connector_id)
|
||||
{
|
||||
if existing_name == &connector_id && connector_name != connector_id {
|
||||
*existing_name = connector_name;
|
||||
}
|
||||
existing_plugin_display_names.extend(plugin_display_names);
|
||||
} else {
|
||||
connectors.insert(connector_id, connector_name);
|
||||
connectors.insert(
|
||||
connector_id,
|
||||
(
|
||||
connector_name,
|
||||
plugin_display_names
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<String>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut accessible: Vec<AppInfo> = connectors
|
||||
.into_iter()
|
||||
.map(|(connector_id, connector_name)| AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: connector_name.clone(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(connector_install_url(&connector_name, &connector_id)),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
})
|
||||
.map(
|
||||
|(connector_id, (connector_name, plugin_display_names))| AppInfo {
|
||||
id: connector_id.clone(),
|
||||
name: connector_name.clone(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(connector_install_url(&connector_name, &connector_id)),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: plugin_display_names.into_iter().collect(),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
accessible.sort_by(|left, right| {
|
||||
right
|
||||
@@ -638,6 +686,7 @@ fn plugin_app_to_app_info(connector_id: AppConnectorId) -> AppInfo {
|
||||
install_url: Some(connector_install_url(&name, &connector_id)),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,7 +730,11 @@ mod tests {
|
||||
use crate::config::types::AppToolConfig;
|
||||
use crate::config::types::AppToolsConfig;
|
||||
use crate::config::types::AppsDefaultConfig;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Tool;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn annotations(
|
||||
destructive_hint: Option<bool>,
|
||||
@@ -710,13 +763,30 @@ mod tests {
|
||||
labels: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
let plugin = plugin_app_to_app_info(AppConnectorId("calendar".to_string()));
|
||||
let accessible = AppInfo {
|
||||
fn plugin_names(names: &[&str]) -> Vec<String> {
|
||||
names.iter().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
fn test_tool_definition(tool_name: &str) -> Tool {
|
||||
Tool {
|
||||
name: tool_name.to_string().into(),
|
||||
title: None,
|
||||
description: None,
|
||||
input_schema: Arc::new(JsonObject::default()),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInfo {
|
||||
AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: Some("Plan events".to_string()),
|
||||
@@ -729,7 +799,30 @@ mod tests {
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
};
|
||||
plugin_display_names: plugin_names(plugin_display_names),
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_app_tool(
|
||||
tool_name: &str,
|
||||
connector_id: &str,
|
||||
connector_name: Option<&str>,
|
||||
plugin_display_names: &[&str],
|
||||
) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool: test_tool_definition(tool_name),
|
||||
connector_id: Some(connector_id.to_string()),
|
||||
connector_name: connector_name.map(ToOwned::to_owned),
|
||||
plugin_display_names: plugin_names(plugin_display_names),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
|
||||
let plugin = plugin_app_to_app_info(AppConnectorId("calendar".to_string()));
|
||||
let accessible = google_calendar_accessible_connector(&[]);
|
||||
|
||||
let merged = merge_connectors(vec![plugin], vec![accessible]);
|
||||
|
||||
@@ -748,11 +841,97 @@ mod tests {
|
||||
install_url: Some(connector_install_url("calendar", "calendar")),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(connector_mention_slug(&merged[0]), "google-calendar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
let tools = HashMap::from([
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
"calendar",
|
||||
None,
|
||||
&["sample", "sample"],
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
codex_app_tool(
|
||||
"calendar_create_event",
|
||||
"calendar",
|
||||
Some("Google Calendar"),
|
||||
&["beta", "sample"],
|
||||
),
|
||||
),
|
||||
(
|
||||
"mcp__sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
tool_name: "echo".to_string(),
|
||||
tool: test_tool_definition("echo"),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: plugin_names(&["ignored"]),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
let connectors = accessible_connectors_from_mcp_tools(&tools);
|
||||
|
||||
assert_eq!(
|
||||
connectors,
|
||||
vec![AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some(connector_install_url("Google Calendar", "calendar")),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: plugin_names(&["beta", "sample"]),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_connectors_unions_and_dedupes_plugin_display_names() {
|
||||
let mut plugin = plugin_app_to_app_info(AppConnectorId("calendar".to_string()));
|
||||
plugin.plugin_display_names = plugin_names(&["sample", "alpha", "sample"]);
|
||||
|
||||
let accessible = google_calendar_accessible_connector(&["beta", "alpha"]);
|
||||
|
||||
let merged = merge_connectors(vec![plugin], vec![accessible]);
|
||||
|
||||
assert_eq!(
|
||||
merged,
|
||||
vec![AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: Some("Plan events".to_string()),
|
||||
logo_url: Some("https://example.com/logo.png".to_string()),
|
||||
logo_url_dark: Some("https://example.com/logo-dark.png".to_string()),
|
||||
distribution_channel: Some("workspace".to_string()),
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(connector_install_url("calendar", "calendar")),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: plugin_names(&["alpha", "beta", "sample"]),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_global_defaults_for_destructive_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
|
||||
@@ -153,6 +153,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
status: status.clone(),
|
||||
revised_prompt: revised_prompt.clone(),
|
||||
result: result.clone(),
|
||||
saved_path: None,
|
||||
},
|
||||
)),
|
||||
_ => None,
|
||||
|
||||
@@ -147,6 +147,8 @@ pub enum Feature {
|
||||
/// Enable collaboration modes (Plan, Default).
|
||||
/// Kept for config backward compatibility; behavior is always collaboration-modes-enabled.
|
||||
CollaborationModes,
|
||||
/// Route MCP tool approval prompts through the MCP elicitation request path.
|
||||
ToolCallMcpElicitation,
|
||||
/// Enable personality selection in the TUI.
|
||||
Personality,
|
||||
/// Enable native artifact tools.
|
||||
@@ -693,6 +695,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Removed,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ToolCallMcpElicitation,
|
||||
key: "tool_call_mcp_elicitation",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Personality,
|
||||
key: "personality",
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod auth;
|
||||
mod client;
|
||||
mod client_common;
|
||||
pub mod codex;
|
||||
mod realtime_context;
|
||||
mod realtime_conversation;
|
||||
pub use codex::SteerInputError;
|
||||
mod codex_thread;
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::SandboxState;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use crate::plugins::PluginsManager;
|
||||
|
||||
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
|
||||
@@ -35,6 +36,64 @@ const CODEX_CONNECTORS_TOKEN_ENV_VAR: &str = "CODEX_CONNECTORS_TOKEN";
|
||||
const OPENAI_CONNECTORS_MCP_BASE_URL: &str = "https://api.openai.com";
|
||||
const OPENAI_CONNECTORS_MCP_PATH: &str = "/v1/connectors/gateways/flat/mcp";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ToolPluginProvenance {
|
||||
plugin_display_names_by_connector_id: HashMap<String, Vec<String>>,
|
||||
plugin_display_names_by_mcp_server_name: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl ToolPluginProvenance {
|
||||
pub fn plugin_display_names_for_connector_id(&self, connector_id: &str) -> &[String] {
|
||||
self.plugin_display_names_by_connector_id
|
||||
.get(connector_id)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
pub fn plugin_display_names_for_mcp_server_name(&self, server_name: &str) -> &[String] {
|
||||
self.plugin_display_names_by_mcp_server_name
|
||||
.get(server_name)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn from_capability_summaries(capability_summaries: &[PluginCapabilitySummary]) -> Self {
|
||||
let mut tool_plugin_provenance = Self::default();
|
||||
for plugin in capability_summaries {
|
||||
for connector_id in &plugin.app_connector_ids {
|
||||
tool_plugin_provenance
|
||||
.plugin_display_names_by_connector_id
|
||||
.entry(connector_id.0.clone())
|
||||
.or_default()
|
||||
.push(plugin.display_name.clone());
|
||||
}
|
||||
|
||||
for server_name in &plugin.mcp_server_names {
|
||||
tool_plugin_provenance
|
||||
.plugin_display_names_by_mcp_server_name
|
||||
.entry(server_name.clone())
|
||||
.or_default()
|
||||
.push(plugin.display_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for plugin_names in tool_plugin_provenance
|
||||
.plugin_display_names_by_connector_id
|
||||
.values_mut()
|
||||
.chain(
|
||||
tool_plugin_provenance
|
||||
.plugin_display_names_by_mcp_server_name
|
||||
.values_mut(),
|
||||
)
|
||||
{
|
||||
plugin_names.sort_unstable();
|
||||
plugin_names.dedup();
|
||||
}
|
||||
|
||||
tool_plugin_provenance
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy vs new MCP gateway
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CodexAppsMcpGateway {
|
||||
@@ -182,6 +241,11 @@ impl McpManager {
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
effective_mcp_servers(config, auth, self.plugins_manager.as_ref())
|
||||
}
|
||||
|
||||
pub fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance {
|
||||
let loaded_plugins = self.plugins_manager.plugins_for_config(config);
|
||||
ToolPluginProvenance::from_capability_summaries(loaded_plugins.capability_summaries())
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_mcp_servers(
|
||||
@@ -219,6 +283,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
|
||||
let auth = auth_manager.auth().await;
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let mcp_servers = mcp_manager.effective_servers(config, auth.as_ref());
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpListToolsResponseEvent {
|
||||
tools: HashMap::new(),
|
||||
@@ -251,6 +316,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent
|
||||
sandbox_state,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
tool_plugin_provenance,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -407,6 +473,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::plugins::AppConnectorId;
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -485,6 +553,47 @@ mod tests {
|
||||
assert_eq!(group_tools_by_server(&tools), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
||||
let provenance = ToolPluginProvenance::from_capability_summaries(&[
|
||||
PluginCapabilitySummary {
|
||||
display_name: "alpha-plugin".to_string(),
|
||||
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
|
||||
mcp_server_names: vec!["alpha".to_string()],
|
||||
..PluginCapabilitySummary::default()
|
||||
},
|
||||
PluginCapabilitySummary {
|
||||
display_name: "beta-plugin".to_string(),
|
||||
app_connector_ids: vec![
|
||||
AppConnectorId("connector_example".to_string()),
|
||||
AppConnectorId("connector_gmail".to_string()),
|
||||
],
|
||||
mcp_server_names: vec!["beta".to_string()],
|
||||
..PluginCapabilitySummary::default()
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
provenance,
|
||||
ToolPluginProvenance {
|
||||
plugin_display_names_by_connector_id: HashMap::from([
|
||||
(
|
||||
"connector_example".to_string(),
|
||||
vec!["alpha-plugin".to_string(), "beta-plugin".to_string()],
|
||||
),
|
||||
(
|
||||
"connector_gmail".to_string(),
|
||||
vec!["beta-plugin".to_string()],
|
||||
),
|
||||
]),
|
||||
plugin_display_names_by_mcp_server_name: HashMap::from([
|
||||
("alpha".to_string(), vec!["alpha-plugin".to_string()]),
|
||||
("beta".to_string(), vec!["beta-plugin".to_string()]),
|
||||
]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_mcp_url_for_default_gateway_keeps_existing_paths() {
|
||||
assert_eq!(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user