diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 68a10ea8d4..e6f91f1661 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -442,6 +442,7 @@ fn sample_turn_completed_notification( items: vec![], status, error: codex_error_info.map(|codex_error_info| AppServerTurnError { + misalignment: None, message: "turn failed".to_string(), codex_error_info: Some(codex_error_info), additional_details: None, @@ -547,6 +548,7 @@ fn non_steerable_review_error() -> JSONRPCErrorError { message: "cannot steer a review turn".to_string(), data: Some( serde_json::to_value(AppServerTurnError { + misalignment: None, message: "cannot steer a review turn".to_string(), codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind::Review, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 6b5503ed1a..49218efb65 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2784,6 +2784,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -5956,6 +5997,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 9dc9e6a80f..6cc4bd3f98 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14654,6 +14654,47 @@ }, "type": "object" }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/v2/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -24043,6 +24084,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/v2/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a3dd5c3d38..d619ffd275 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10787,6 +10787,47 @@ }, "type": "object" }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -21778,6 +21819,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json index 92dd60548c..69187dd539 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ErrorNotification.json @@ -141,6 +141,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -169,6 +210,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index bd78ecf099..b461409650 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -634,6 +634,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -1636,6 +1677,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 6abfafc209..2f89c32bf8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -738,6 +738,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -2328,6 +2369,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 88c5f4f3cb..737c5731d2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index c2e0726459..6588235744 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 4e9b4b83b4..37b5b93809 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 2eb73c83d0..f267aeaf36 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -738,6 +738,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -2328,6 +2369,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index c876ba22e0..1ff75a0c51 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index d56f80ebd7..e898ed8f3c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 8ca47fd81c..99cba383f8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -738,6 +738,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "MultiAgentMode": { "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", "oneOf": [ @@ -2328,6 +2369,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 329167a240..c07fcd44fd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json index 51ed4e037c..5a71e6eea8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json @@ -634,6 +634,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -1636,6 +1677,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index af5b9161fd..46fbe29c71 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -660,6 +660,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -2119,6 +2160,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 3293e05bd5..4ed3a6bc8c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -634,6 +634,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -1636,6 +1677,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 3da13ac0c4..0931d2cef8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -634,6 +634,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -1636,6 +1677,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 771c7821da..7355dc3fed 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -634,6 +634,47 @@ } ] }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", @@ -1636,6 +1677,18 @@ }, "message": { "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 77ebebfd7e..5612ef3887 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index e859566915..3680793d49 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentErrorDetails.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentErrorDetails.ts new file mode 100644 index 0000000000..3f6e449dc5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentErrorDetails.ts @@ -0,0 +1,18 @@ +// 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 { MisalignmentSteer } from "./MisalignmentSteer"; + +export type MisalignmentErrorDetails = { +/** + * Open-ended classification; clients must accept categories added by Responses. + */ +errorType: string | null, +/** + * A substantive localized explanation is required before offering continuation. + */ +detailedExplanation: string | null, +/** + * Instruction to submit as the next turn's user input if continuation is confirmed. + */ +steer: MisalignmentSteer | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentSteer.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentSteer.ts new file mode 100644 index 0000000000..f0c4304752 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MisalignmentSteer.ts @@ -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 MisalignmentSteer = { message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnError.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnError.ts index 765a8e050b..2893b58fe6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnError.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnError.ts @@ -2,5 +2,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CodexErrorInfo } from "./CodexErrorInfo"; +import type { MisalignmentErrorDetails } from "./MisalignmentErrorDetails"; -export type TurnError = { message: string, codexErrorInfo: CodexErrorInfo | null, additionalDetails: string | null, }; +export type TurnError = { message: string, codexErrorInfo: CodexErrorInfo | null, additionalDetails: string | null, +/** + * Optional public explanation and continuation instruction for a misalignment block. + */ +misalignment: MisalignmentErrorDetails | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index dbfa4aa4c9..6023b44fd1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -297,6 +297,8 @@ export type { MemoryCitation } from "./MemoryCitation"; export type { MemoryCitationEntry } from "./MemoryCitationEntry"; export type { MergeStrategy } from "./MergeStrategy"; export type { MigrationDetails } from "./MigrationDetails"; +export type { MisalignmentErrorDetails } from "./MisalignmentErrorDetails"; +export type { MisalignmentSteer } from "./MisalignmentSteer"; export type { Model } from "./Model"; export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; export type { ModelListParams } from "./ModelListParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index fa82981ada..8e17ed70d3 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1192,6 +1192,7 @@ impl ThreadHistoryBuilder { let changed_turn = if let Some(turn) = self.current_turn.as_mut() { turn.status = TurnStatus::Failed; turn.error = Some(V2TurnError { + misalignment: payload.misalignment.clone().map(Into::into), message: payload.message.clone(), codex_error_info: payload.codex_error_info.clone().map(Into::into), additional_details: None, @@ -1250,6 +1251,7 @@ impl ThreadHistoryBuilder { fn handle_turn_complete(&mut self, payload: &TurnCompleteEvent) { let terminal_error = payload.error.as_ref().map(|error| V2TurnError { + misalignment: error.misalignment.clone().map(Into::into), message: error.message.clone(), codex_error_info: error.codex_error_info.clone().map(Into::into), additional_details: None, @@ -3821,6 +3823,7 @@ mod tests { started_at: Some(10), last_agent_message: None, error: Some(ErrorEvent { + misalignment: None, message: "Selected model is at capacity. Please try a different model.".into(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), }), @@ -3851,6 +3854,7 @@ mod tests { }], status: TurnStatus::Failed, error: Some(TurnError { + misalignment: None, message: "Selected model is at capacity. Please try a different model." .into(), codex_error_info: Some( @@ -4203,6 +4207,7 @@ mod tests { delivery: None, }), EventMsg::Error(ErrorEvent { + misalignment: None, message: "rollback failed".into(), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -4246,6 +4251,7 @@ mod tests { time_to_first_token_ms: None, }), EventMsg::Error(ErrorEvent { + misalignment: None, message: "request-level failure".into(), codex_error_info: Some(CodexErrorInfo::BadRequest), }), @@ -4298,6 +4304,7 @@ mod tests { ..Default::default() }), EventMsg::Error(ErrorEvent { + misalignment: None, message: "stream failure".into(), codex_error_info: Some(CodexErrorInfo::ResponseStreamDisconnected { http_status_code: Some(502), @@ -4325,6 +4332,7 @@ mod tests { assert_eq!( turns[0].error, Some(TurnError { + misalignment: None, message: "stream failure".into(), codex_error_info: Some( crate::protocol::v2::CodexErrorInfo::ResponseStreamDisconnected { @@ -4359,6 +4367,7 @@ mod tests { started_at: Some(10), last_agent_message: None, error: Some(ErrorEvent { + misalignment: None, message: "Selected model is at capacity. Please try a different model.".into(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), }), @@ -4388,6 +4397,7 @@ mod tests { }], status: TurnStatus::Failed, error: Some(TurnError { + misalignment: None, message: "Selected model is at capacity. Please try a different model.".into(), codex_error_info: Some(crate::protocol::v2::CodexErrorInfo::ServerOverloaded), additional_details: None, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs index 38a7c7e2f6..6bb1138dc0 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection.rs @@ -40,6 +40,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet { TurnStatus::Completed }, error: event.error.as_ref().map(|error| TurnError { + misalignment: error.misalignment.clone().map(Into::into), message: error.message.clone(), codex_error_info: error.codex_error_info.clone().map(Into::into), additional_details: None, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs index 6cd4744d6a..1c1cda3129 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs @@ -68,6 +68,7 @@ fn projects_turn_lifecycle_without_prior_builder_state() { #[test] fn projects_failed_turn_completion_as_snapshot() { let error = ErrorEvent { + misalignment: None, message: "request failed".to_string(), codex_error_info: None, }; @@ -91,6 +92,7 @@ fn projects_failed_turn_completion_as_snapshot() { turn_id: "turn-1".to_string(), status: TurnStatus::Failed, error: Some(TurnError { + misalignment: None, message: "request failed".to_string(), codex_error_info: None, additional_details: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index c68ed7f979..131d42347c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -5,6 +5,8 @@ use super::TurnStatus; use crate::JsonSchema; use crate::TS; use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::protocol::MisalignmentErrorDetails as CoreMisalignmentErrorDetails; +use codex_protocol::protocol::MisalignmentSteer as CoreMisalignmentSteer; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; use codex_protocol::protocol::ThreadHistoryMode as CoreThreadHistoryMode; @@ -16,6 +18,7 @@ use schemars::r#gen::SchemaGenerator; use schemars::schema::Schema; use serde::Deserialize; use serde::Serialize; +use std::fmt; use std::path::PathBuf; use thiserror::Error; @@ -393,4 +396,67 @@ pub struct TurnError { pub codex_error_info: Option, #[serde(default)] pub additional_details: Option, + /// Optional public explanation and continuation instruction for a misalignment block. + #[serde(default)] + pub misalignment: Option, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MisalignmentErrorDetails { + /// Open-ended classification; clients must accept categories added by Responses. + pub error_type: Option, + /// A substantive localized explanation is required before offering continuation. + pub detailed_explanation: Option, + /// Instruction to submit as the next turn's user input if continuation is confirmed. + pub steer: Option, +} + +impl fmt::Debug for MisalignmentErrorDetails { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MisalignmentErrorDetails") + .field("error_type", &self.error_type) + .field( + "has_detailed_explanation", + &self.detailed_explanation.is_some(), + ) + .field("has_steer", &self.steer.is_some()) + .finish() + } +} + +impl From for MisalignmentErrorDetails { + fn from(value: CoreMisalignmentErrorDetails) -> Self { + Self { + error_type: value.error_type, + detailed_explanation: value.detailed_explanation, + steer: value.steer.map(Into::into), + } + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MisalignmentSteer { + pub message: String, +} + +impl fmt::Debug for MisalignmentSteer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MisalignmentSteer") + .field("message", &"[REDACTED]") + .finish() + } +} + +impl From for MisalignmentSteer { + fn from(value: CoreMisalignmentSteer) -> Self { + Self { + message: value.message, + } + } } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3f6c6b79e3..bc08470444 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1680,7 +1680,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`. - `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`. -- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`. +- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; successful turns include their final agent message when available, and failures carry `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }`. - `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items. - `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`. - `rawResponse/completed` — internal-only; when `thread/start.experimentalRawEvents` is enabled, emits `{ threadId, turnId, responseId, usage }` once for each upstream Responses API completion. `usage` is the exact upstream usage payload mapped to the app-server token breakdown shape and is `null` when the upstream completion omitted usage. Unlike `thread/tokenUsage/updated`, this notification is not accumulated, estimated, persisted, or replayed. @@ -1760,7 +1760,7 @@ There are additional item-specific events: Ownership rejections for parent-owned Multi-Agent V2 subagents return JSON-RPC error code `-32600` with message `direct app-server input is not allowed for multi-agent v2 sub-agents`. -`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails? } }` payload as `turn.status: "failed"` and may precede that terminal notification. +`error` event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same `{ error: { message, codexErrorInfo?, additionalDetails?, misalignment? } }` payload as `turn.status: "failed"` and may precede that terminal notification. `codexErrorInfo` maps to the `CodexErrorInfo` enum. Common values: @@ -1783,6 +1783,16 @@ Ownership rejections for parent-owned Multi-Agent V2 subagents return JSON-RPC e When an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. +For `misalignmentPolicyViolation`, optional `misalignment` details contain `errorType`, +`detailedExplanation`, and `steer: { message }`. Error categories are open-ended. A category alone +remains a terminal block; clients may offer continuation only when both a substantive explanation +and a steering message are present. To continue after user confirmation, submit the steering +message with the existing `turn/start` method and include +`responsesapiClientMetadata: { misalignment_override: JSON.stringify({ timestamp, feedback }) }`, +where `timestamp` is the confirmation time in Unix milliseconds and `feedback` is the user's +explanation. Misalignment explanation and steering details are delivered live but excluded from +persisted rollout errors, so unavailable details after a restart remain a terminal block. + ## Approvals Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 7d5c067b68..f14d4792ed 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -965,6 +965,7 @@ pub(crate) async fn apply_bespoke_event_handling( } let turn_error = TurnError { + misalignment: ev.misalignment.map(Into::into), message: ev.message, codex_error_info: ev.codex_error_info.map(V2CodexErrorInfo::from), additional_details: None, @@ -982,6 +983,7 @@ pub(crate) async fn apply_bespoke_event_handling( // We don't need to update the turn summary store for stream errors as they are intermediate error states for retries, // but we notify the client. let turn_error = TurnError { + misalignment: None, message: ev.message, codex_error_info: ev.codex_error_info.map(V2CodexErrorInfo::from), additional_details: ev.additional_details, @@ -1831,6 +1833,7 @@ async fn on_request_permissions_response( conversation_id, &turn_id, TurnError { + misalignment: None, message, codex_error_info: None, additional_details: None, @@ -3303,6 +3306,7 @@ mod tests { handle_error( conversation_id, TurnError { + misalignment: None, message: "boom".to_string(), codex_error_info: Some(V2CodexErrorInfo::InternalServerError), additional_details: None, @@ -3315,6 +3319,7 @@ mod tests { assert_eq!( turn_summary.last_error, Some(TurnError { + misalignment: None, message: "boom".to_string(), codex_error_info: Some(V2CodexErrorInfo::InternalServerError), additional_details: None, @@ -3703,6 +3708,7 @@ mod tests { handle_error( conversation_id, TurnError { + misalignment: None, message: "oops".to_string(), codex_error_info: None, additional_details: None, @@ -3753,6 +3759,7 @@ mod tests { handle_error( conversation_id, TurnError { + misalignment: None, message: "bad".to_string(), codex_error_info: Some(V2CodexErrorInfo::Other), additional_details: None, @@ -3788,6 +3795,7 @@ mod tests { assert_eq!( n.turn.error, Some(TurnError { + misalignment: None, message: "bad".to_string(), codex_error_info: Some(V2CodexErrorInfo::Other), additional_details: None, @@ -4000,6 +4008,7 @@ mod tests { handle_error( conversation_a, TurnError { + misalignment: None, message: "a1".to_string(), codex_error_info: Some(V2CodexErrorInfo::BadRequest), additional_details: None, @@ -4021,6 +4030,7 @@ mod tests { handle_error( conversation_b, TurnError { + misalignment: None, message: "b1".to_string(), codex_error_info: None, additional_details: None, @@ -4057,6 +4067,7 @@ mod tests { assert_eq!( n.turn.error, Some(TurnError { + misalignment: None, message: "a1".to_string(), codex_error_info: Some(V2CodexErrorInfo::BadRequest), additional_details: None, @@ -4075,6 +4086,7 @@ mod tests { assert_eq!( n.turn.error, Some(TurnError { + misalignment: None, message: "b1".to_string(), codex_error_info: None, additional_details: None, diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index c2b2a8fa55..0803fb1445 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -5669,6 +5669,7 @@ fn stored_turn_to_api_turn( StoredTurnStatus::InProgress => TurnStatus::InProgress, }; let error = turn.error.map(|error| TurnError { + misalignment: None, message: error.message, codex_error_info: error.codex_error_info, additional_details: error.additional_details, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 948e2b3f68..a209055d75 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1028,6 +1028,7 @@ impl TurnRequestProcessor { ), }; let error = TurnError { + misalignment: None, message: message.clone(), codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { turn_kind: turn_kind.into(), diff --git a/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs b/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs index 375b49dd07..131911b0c1 100644 --- a/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs +++ b/codex-rs/app-server/tests/suite/v2/misalignment_policy.rs @@ -4,6 +4,8 @@ use app_test_support::TestAppServer; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CodexErrorInfo; use codex_app_server_protocol::ErrorNotification; +use codex_app_server_protocol::MisalignmentErrorDetails; +use codex_app_server_protocol::MisalignmentSteer; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnCompletedNotification; @@ -20,6 +22,8 @@ use tokio::time::timeout; use wiremock::ResponseTemplate; const MESSAGE: &str = "This request violated the misalignment policy."; +const EXPLANATION: &str = "The agent attempted to transfer your files externally."; +const STEER: &str = "Continue without transferring the user's files externally."; #[tokio::test] async fn streamed_policy_violation_completes_turn_with_typed_terminal_error() -> Result<()> { @@ -41,7 +45,10 @@ async fn streamed_policy_violation_completes_turn_with_typed_terminal_error() -> }), ])); - assert_policy_violation_completes_turn_with_typed_terminal_error(response).await + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, /*expected_misalignment*/ None, + ) + .await } #[tokio::test] @@ -56,7 +63,10 @@ async fn http_400_policy_violation_completes_turn_with_typed_terminal_error() -> } })); - assert_policy_violation_completes_turn_with_typed_terminal_error(response).await + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, /*expected_misalignment*/ None, + ) + .await } #[tokio::test] @@ -71,11 +81,100 @@ async fn http_403_policy_violation_completes_turn_with_typed_terminal_error() -> } })); - assert_policy_violation_completes_turn_with_typed_terminal_error(response).await + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, /*expected_misalignment*/ None, + ) + .await +} + +#[tokio::test] +async fn streamed_policy_violation_exposes_resumable_misalignment_details() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = responses::sse_response(responses::sse(vec![ + responses::ev_response_created("resp-misalignment"), + serde_json::json!({ + "type": "response.failed", + "response": { + "id": "resp-misalignment", + "status": "failed", + "error": { + "code": "misalignment_policy_violation", + "message": MESSAGE, + "misalignment": { + "error_type": "unauthorized_data_transfer", + "detailed_explanation": EXPLANATION, + "steer": { "message": STEER } + } + } + } + }), + ])); + + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, + Some(resumable_misalignment()), + ) + .await +} + +#[tokio::test] +async fn http_403_policy_violation_exposes_resumable_misalignment_details() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = ResponseTemplate::new(403).set_body_json(serde_json::json!({ + "error": { + "code": "misalignment_policy_violation", + "message": MESSAGE, + "misalignment": { + "error_type": "unauthorized_data_transfer", + "detailed_explanation": EXPLANATION, + "steer": { "message": STEER } + } + } + })); + + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, + Some(resumable_misalignment()), + ) + .await +} + +#[tokio::test] +async fn classification_only_policy_violation_preserves_the_terminal_error() -> Result<()> { + skip_if_no_network!(Ok(())); + + let response = responses::sse_response(responses::sse(vec![ + responses::ev_response_created("resp-misalignment"), + serde_json::json!({ + "type": "response.failed", + "response": { + "id": "resp-misalignment", + "status": "failed", + "error": { + "code": "misalignment_policy_violation", + "message": MESSAGE, + "misalignment": { "error_type": "unsafe_activity" } + } + } + }), + ])); + + assert_policy_violation_completes_turn_with_typed_terminal_error( + response, + Some(MisalignmentErrorDetails { + error_type: Some("unsafe_activity".to_string()), + detailed_explanation: None, + steer: None, + }), + ) + .await } async fn assert_policy_violation_completes_turn_with_typed_terminal_error( response: ResponseTemplate, + expected_misalignment: Option, ) -> Result<()> { let server = responses::start_mock_server().await; let response_mock = responses::mount_response_once(&server, response).await; @@ -117,6 +216,7 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error( error, ErrorNotification { error: TurnError { + misalignment: expected_misalignment.clone(), message: MESSAGE.to_string(), codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), additional_details: None, @@ -138,6 +238,7 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error( assert_eq!( completed.turn.error, Some(TurnError { + misalignment: expected_misalignment, message: MESSAGE.to_string(), codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), additional_details: None, @@ -145,5 +246,25 @@ async fn assert_policy_violation_completes_turn_with_typed_terminal_error( ); response_mock.single_request(); + let rollout = tokio::fs::read_to_string( + thread + .path + .as_ref() + .expect("non-ephemeral thread should have a rollout path"), + ) + .await?; + assert!(!rollout.contains(EXPLANATION)); + assert!(!rollout.contains(STEER)); + Ok(()) } + +fn resumable_misalignment() -> MisalignmentErrorDetails { + MisalignmentErrorDetails { + error_type: Some("unauthorized_data_transfer".to_string()), + detailed_explanation: Some(EXPLANATION.to_string()), + steer: Some(MisalignmentSteer { + message: STEER.to_string(), + }), + } +} diff --git a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs index 8a0194e96e..53a608cbdd 100644 --- a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs @@ -137,6 +137,7 @@ async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()> error, ErrorNotification { error: codex_app_server_protocol::TurnError { + misalignment: None, message: CYBER_POLICY_MESSAGE.to_string(), codex_error_info: Some(CodexErrorInfo::CyberPolicy), additional_details: None, diff --git a/codex-rs/codex-api/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs index acb40613c7..e8d7c4743c 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -13,6 +13,7 @@ use codex_protocol::error::ConnectionFailedError; use codex_protocol::error::RetryLimitReachedError; use codex_protocol::error::UnexpectedResponseError; use codex_protocol::error::UsageLimitReachedError; +use codex_protocol::protocol::MisalignmentErrorDetails; use http::HeaderMap; use serde::Deserialize; use serde_json::Value; @@ -55,9 +56,13 @@ pub fn map_api_error(err: ApiError) -> CodexErr { ApiError::CyberPolicy { message } => { CodexErr::new(CodexErrorDetails::CyberPolicy { message }) } - ApiError::MisalignmentPolicyViolation { message } => { - CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { message }) - } + ApiError::MisalignmentPolicyViolation { + message, + misalignment, + } => CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { + message, + misalignment, + }), ApiError::Transport(transport) => match transport { TransportError::Http { status, @@ -97,6 +102,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr { }); return CodexErr::new(CodexErrorDetails::MisalignmentPolicyViolation { message, + misalignment: error.get("misalignment").cloned().and_then(|details| { + serde_json::from_value::(details).ok() + }), }); } diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index 06f31c8682..666dea8864 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -199,10 +199,104 @@ fn assert_misalignment_policy_violation_from_http_body(status: http::StatusCode) body: Some(body), })); - let CodexErrorDetails::MisalignmentPolicyViolation { message } = err.details() else { + let CodexErrorDetails::MisalignmentPolicyViolation { + message, + misalignment, + } = err.details() + else { panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); }; assert_eq!(message, "This request violated the misalignment policy."); + assert_eq!(misalignment, &None); + assert!(!err.is_retryable()); +} + +#[test] +fn map_api_error_preserves_misalignment_details_from_403_body() { + let body = serde_json::json!({ + "error": { + "message": "This request violated the misalignment policy.", + "code": "misalignment_policy_violation", + "misalignment": { + "error_type": "unauthorized_data_transfer", + "detailed_explanation": "The agent attempted an external transfer.", + "steer": { "message": "Do not transfer the user's files." } + } + } + }) + .to_string(); + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::FORBIDDEN, + url: Some("http://example.com/v1/responses".to_string()), + headers: None, + body: Some(body), + })); + + let CodexErrorDetails::MisalignmentPolicyViolation { + message, + misalignment, + } = err.details() + else { + panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); + }; + assert_eq!(message, "This request violated the misalignment policy."); + assert_eq!( + misalignment, + &Some(MisalignmentErrorDetails { + error_type: Some("unauthorized_data_transfer".to_string()), + detailed_explanation: Some("The agent attempted an external transfer.".to_string()), + steer: Some(codex_protocol::protocol::MisalignmentSteer { + message: "Do not transfer the user's files.".to_string(), + }), + }) + ); + assert!(!err.is_retryable()); +} + +#[test] +fn map_api_error_preserves_misalignment_details_from_wrapped_websocket_error() { + let body = serde_json::json!({ + "type": "error", + "status": 403, + "error": { + "message": "This websocket request violated the misalignment policy.", + "code": "misalignment_policy_violation", + "misalignment": { + "error_type": "future_safety_category", + "detailed_explanation": "The agent attempted an external transfer.", + "steer": { "message": "Do not transfer the user's files." } + } + } + }) + .to_string(); + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::FORBIDDEN, + url: Some("ws://example.com/v1/responses".to_string()), + headers: None, + body: Some(body), + })); + + let CodexErrorDetails::MisalignmentPolicyViolation { + message, + misalignment, + } = err.details() + else { + panic!("expected CodexErrorDetails::MisalignmentPolicyViolation, got {err:?}"); + }; + assert_eq!( + message, + "This websocket request violated the misalignment policy." + ); + assert_eq!( + misalignment, + &Some(MisalignmentErrorDetails { + error_type: Some("future_safety_category".to_string()), + detailed_explanation: Some("The agent attempted an external transfer.".to_string()), + steer: Some(codex_protocol::protocol::MisalignmentSteer { + message: "Do not transfer the user's files.".to_string(), + }), + }) + ); assert!(!err.is_retryable()); } diff --git a/codex-rs/codex-api/src/error.rs b/codex-rs/codex-api/src/error.rs index 5ac83ca1d4..d1d83cf401 100644 --- a/codex-rs/codex-api/src/error.rs +++ b/codex-rs/codex-api/src/error.rs @@ -1,5 +1,6 @@ use crate::rate_limits::RateLimitError; use codex_client::TransportError; +use codex_protocol::protocol::MisalignmentErrorDetails; use http::StatusCode; use std::time::Duration; use thiserror::Error; @@ -35,7 +36,10 @@ pub enum ApiError { #[error("cyber policy: {message}")] CyberPolicy { message: String }, #[error("misalignment policy violation: {message}")] - MisalignmentPolicyViolation { message: String }, + MisalignmentPolicyViolation { + message: String, + misalignment: Option, + }, #[error("server overloaded")] ServerOverloaded, } diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index b4cc3fa17d..9789bdb088 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -9,6 +9,7 @@ use crate::telemetry::SseTelemetry; use codex_client::ByteStream; use codex_client::StreamResponse; use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::MisalignmentErrorDetails; use codex_protocol::protocol::ModelVerification; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TurnModerationMetadataEvent; @@ -107,6 +108,8 @@ struct Error { message: Option, plan_type: Option, resets_at: Option, + #[serde(default)] + misalignment: Option, } #[derive(Debug, Deserialize)] @@ -428,7 +431,12 @@ pub fn process_responses_event( "This request was blocked due to a misalignment policy violation." .to_string() }); - response_error = ApiError::MisalignmentPolicyViolation { message }; + response_error = ApiError::MisalignmentPolicyViolation { + message, + misalignment: error.misalignment.and_then(|details| { + serde_json::from_value::(details).ok() + }), + }; } else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy")) { let message = error @@ -1212,8 +1220,12 @@ mod tests { assert_eq!(events.len(), 1); match &events[0] { - Err(ApiError::MisalignmentPolicyViolation { message }) => { + Err(ApiError::MisalignmentPolicyViolation { + message, + misalignment, + }) => { assert_eq!(message, "This request violated the misalignment policy."); + assert_eq!(misalignment, &None); } other => panic!("unexpected event: {other:?}"), } @@ -1239,7 +1251,7 @@ mod tests { assert_eq!(events.len(), 1); match &events[0] { - Err(ApiError::MisalignmentPolicyViolation { message }) => assert_eq!( + Err(ApiError::MisalignmentPolicyViolation { message, .. }) => assert_eq!( message, "This request was blocked due to a misalignment policy violation." ), @@ -1248,6 +1260,78 @@ mod tests { } } + #[tokio::test] + async fn misalignment_policy_violation_preserves_public_continuation_details() { + let raw_error = json!({ + "type": "response.failed", + "response": { + "id": "resp_fatal_misalignment", + "status": "failed", + "error": { + "code": "misalignment_policy_violation", + "message": "This request violated the misalignment policy.", + "misalignment": { + "error_type": "future_safety_category", + "detailed_explanation": "The agent attempted an external transfer.", + "steer": { "message": "Do not transfer the user's files." } + } + } + } + }); + let sse = format!("event: response.failed\ndata: {raw_error}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + + assert_eq!(events.len(), 1); + match &events[0] { + Err(ApiError::MisalignmentPolicyViolation { + message, + misalignment, + }) => { + assert_eq!(message, "This request violated the misalignment policy."); + assert_eq!( + misalignment, + &Some(MisalignmentErrorDetails { + error_type: Some("future_safety_category".to_string()), + detailed_explanation: Some( + "The agent attempted an external transfer.".to_string() + ), + steer: Some(codex_protocol::protocol::MisalignmentSteer { + message: "Do not transfer the user's files.".to_string(), + }), + }) + ); + } + other => panic!("unexpected event: {other:?}"), + } + } + + #[tokio::test] + async fn malformed_misalignment_details_preserve_the_fatal_policy_error() { + let raw_error = json!({ + "type": "response.failed", + "response": { + "id": "resp_fatal_misalignment", + "status": "failed", + "error": { + "code": "misalignment_policy_violation", + "message": "This request violated the misalignment policy.", + "misalignment": { "steer": { "message": 42 } } + } + } + }); + let sse = format!("event: response.failed\ndata: {raw_error}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + + assert_eq!(events.len(), 1); + assert_matches!( + &events[0], + Err(ApiError::MisalignmentPolicyViolation { + misalignment: None, + .. + }) + ); + } + #[tokio::test] async fn content_policy_errors_without_type_are_invalid_requests() { for (code, expected_message) in [ @@ -1913,6 +1997,7 @@ mod tests { code: Some("rate_limit_exceeded".to_string()), plan_type: None, resets_at: None, + misalignment: None, }; let delay = try_parse_retry_after(&err); @@ -1927,6 +2012,7 @@ mod tests { code: Some("rate_limit_exceeded".to_string()), plan_type: None, resets_at: None, + misalignment: None, }; let delay = try_parse_retry_after(&err); assert_eq!(delay, Some(Duration::from_secs_f64(1.898))); @@ -1940,6 +2026,7 @@ mod tests { code: Some("rate_limit_exceeded".to_string()), plan_type: None, resets_at: None, + misalignment: None, }; let delay = try_parse_retry_after(&err); assert_eq!(delay, Some(Duration::from_secs(35))); diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 1ee3b009a1..0c0d2c0bb4 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -487,6 +487,7 @@ async fn on_event_updates_status_from_task_complete() { (None, AgentStatus::Completed(Some("done".to_string()))), ( Some(ErrorEvent { + misalignment: None, message: "denied".to_string(), codex_error_info: None, }), @@ -509,6 +510,7 @@ async fn on_event_updates_status_from_task_complete() { #[tokio::test] async fn on_event_updates_status_from_error() { let status = agent_status_from_event(&EventMsg::Error(ErrorEvent { + misalignment: None, message: "boom".to_string(), codex_error_info: None, })); diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 1c0cb706e5..9ce3976537 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -2310,6 +2310,7 @@ mod tests { .send(Event { id: "prior-turn".to_string(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "stale guardian error".to_string(), codex_error_info: None, }), @@ -2351,6 +2352,7 @@ mod tests { .send(Event { id: "current-turn".to_string(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "temporary failure".to_string(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), }), diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 3011fa04b8..8cbd1517cc 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -588,6 +588,7 @@ pub(crate) async fn run_legacy_after_agent_hook( return false; }; let event = EventMsg::Error(codex_protocol::protocol::ErrorEvent { + misalignment: None, message, codex_error_info: Some(CodexErrorInfo::Other), }); diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 879c2117d8..5d15f5ac45 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -2505,6 +2505,7 @@ async fn send_conversation_error( sess.send_event_raw(Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message, codex_error_info: Some(codex_error_info), }), diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 615ea7b8fb..dada3b6e6c 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -246,6 +246,7 @@ pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32 sess.send_event_raw(Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "num_turns must be >= 1".to_string(), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -259,6 +260,7 @@ pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32 sess.send_event_raw(Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "Cannot rollback while a turn is in progress.".to_string(), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -276,6 +278,7 @@ pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32 sess.send_event_raw(Event { id: turn_context.sub_id.clone(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "thread rollback requires persisted thread history".to_string(), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -288,6 +291,7 @@ pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32 sess.send_event_raw(Event { id: turn_context.sub_id.clone(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: format!("failed to flush thread persistence for rollback replay: {err}"), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -302,6 +306,7 @@ pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32 sess.send_event_raw(Event { id: turn_context.sub_id.clone(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: format!("failed to load thread history for rollback replay: {err}"), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }), @@ -381,6 +386,7 @@ pub async fn set_thread_memory_mode(sess: &Arc, sub_id: String, mode: T let event = Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: err.to_string(), codex_error_info: Some(CodexErrorInfo::Other), }), @@ -452,6 +458,7 @@ pub async fn shutdown(sess: &Arc, sub_id: String) -> bool { let event = Event { id: sub_id.clone(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: "Failed to shutdown thread persistence".to_string(), codex_error_info: Some(CodexErrorInfo::Other), }), @@ -500,6 +507,7 @@ pub async fn review( let event = Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: err.to_string(), codex_error_info: Some(CodexErrorInfo::Other), }), @@ -536,6 +544,7 @@ pub(super) async fn submission_loop( sess.send_event_raw(Event { id: sub.id.clone(), msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: err.to_string(), codex_error_info: Some(CodexErrorInfo::Other), }), diff --git a/codex-rs/core/src/session/thread_settings.rs b/codex-rs/core/src/session/thread_settings.rs index 82126153ce..42f74ca094 100644 --- a/codex-rs/core/src/session/thread_settings.rs +++ b/codex-rs/core/src/session/thread_settings.rs @@ -27,6 +27,7 @@ pub(super) async fn update( .send_event_raw(Event { id: submission_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: format!("invalid thread settings override: {error}"), codex_error_info: Some(CodexErrorInfo::BadRequest), }), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 27304344be..492bf27721 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -576,6 +576,7 @@ pub(crate) async fn run_turn( sess.emit_turn_error_lifecycle(turn_context.as_ref(), error.clone()) .await; let event = EventMsg::Error(ErrorEvent { + misalignment: None, message: "Invalid image in your last message. Please remove it and try again." .to_string(), codex_error_info: Some(error), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 9d175ec840..04d04a7bcb 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -834,6 +834,7 @@ impl Session { self.send_event_raw(Event { id: sub_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: message.clone(), codex_error_info: Some(CodexErrorInfo::BadRequest), }), diff --git a/codex-rs/core/src/session/turn_input.rs b/codex-rs/core/src/session/turn_input.rs index bbc37657d6..8c6eeb0ace 100644 --- a/codex-rs/core/src/session/turn_input.rs +++ b/codex-rs/core/src/session/turn_input.rs @@ -511,6 +511,7 @@ impl Session { self.send_event_raw(Event { id: submission_id, msg: EventMsg::Error(ErrorEvent { + misalignment: None, message: format!("failed to submit turn input: {reason:?}"), codex_error_info: Some(CodexErrorInfo::BadRequest), }), diff --git a/codex-rs/core/src/session/turn_input_tests.rs b/codex-rs/core/src/session/turn_input_tests.rs index fc31d04407..75b305ac24 100644 --- a/codex-rs/core/src/session/turn_input_tests.rs +++ b/codex-rs/core/src/session/turn_input_tests.rs @@ -652,6 +652,7 @@ async fn admission_revalidates_constraints_before_committing(kind: TurnStartKind assert_eq!( errors, vec![ErrorEvent { + misalignment: None, message: expected_message, codex_error_info: Some(CodexErrorInfo::BadRequest), }] diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 1b308b2c16..94fd078bdd 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -375,6 +375,7 @@ async fn send_user_shell_error(session: &Session, turn_context: &TurnContext, me .send_event( turn_context, EventMsg::Error(ErrorEvent { + misalignment: None, message: message.to_string(), codex_error_info: None, }), diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index 60ebd53260..096c3d8cf6 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -38,6 +38,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { items: Vec::new(), status: TurnStatus::Failed, error: Some(codex_app_server_protocol::TurnError { + misalignment: None, message: "turn failed".to_string(), additional_details: None, codex_error_info: None, diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 51e2538fc1..de6aac43b3 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -1561,6 +1561,7 @@ fn failed_turn_clears_stale_final_message() { items: Vec::new(), status: TurnStatus::Failed, error: Some(TurnError { + misalignment: None, message: "turn failed".to_string(), additional_details: None, codex_error_info: None, @@ -1617,6 +1618,7 @@ fn turn_failure_prefers_structured_error_message() { let error = processor.collect_thread_events(ServerNotification::Error(ErrorNotification { error: TurnError { + misalignment: None, message: "backend failed".to_string(), codex_error_info: None, additional_details: Some("request id abc".to_string()), diff --git a/codex-rs/protocol/src/error.rs b/codex-rs/protocol/src/error.rs index c4a314e4d0..1b4cf067fc 100644 --- a/codex-rs/protocol/src/error.rs +++ b/codex-rs/protocol/src/error.rs @@ -7,6 +7,7 @@ use crate::exec_output::ExecToolCallOutput; use crate::network_policy::NetworkPolicyDecisionPayload; use crate::protocol::CodexErrorInfo; use crate::protocol::ErrorEvent; +use crate::protocol::MisalignmentErrorDetails; use crate::protocol::RateLimitReachedType; use crate::protocol::RateLimitSnapshot; use crate::protocol::TruncationPolicy; @@ -136,7 +137,10 @@ pub enum CodexErrorDetails { #[error("{message}")] CyberPolicy { message: String }, #[error("{message}")] - MisalignmentPolicyViolation { message: String }, + MisalignmentPolicyViolation { + message: String, + misalignment: Option, + }, #[error("{0}")] ResponseStreamFailed(ResponseStreamFailed), #[error("{0}")] @@ -469,6 +473,12 @@ impl CodexErr { ErrorEvent { message, codex_error_info: Some(self.to_codex_protocol_error()), + misalignment: match &self.details { + CodexErrorDetails::MisalignmentPolicyViolation { misalignment, .. } => { + misalignment.clone() + } + _ => None, + }, } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index efabf5ff8b..3d09b2f548 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1983,11 +1983,59 @@ pub struct ExitedReviewModeEvent { // Individual event payload types matching each `EventMsg` variant. +/// Public, customer-facing details supplied by the Responses API for a misalignment block. +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct MisalignmentErrorDetails { + /// Open-ended classification; new values must not prevent the error from being surfaced. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error_type: Option, + /// A localized explanation is required before a client may offer continuation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub detailed_explanation: Option, + /// Model-visible instruction to submit if the user elects to continue. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub steer: Option, +} + +impl fmt::Debug for MisalignmentErrorDetails { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MisalignmentErrorDetails") + .field("error_type", &self.error_type) + .field( + "has_detailed_explanation", + &self.detailed_explanation.is_some(), + ) + .field("has_steer", &self.steer.is_some()) + .finish() + } +} + +/// Public steering instruction returned alongside a resumable misalignment block. +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct MisalignmentSteer { + pub message: String, +} + +impl fmt::Debug for MisalignmentSteer { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("MisalignmentSteer") + .field("message", &"[REDACTED]") + .finish() + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct ErrorEvent { pub message: String, #[serde(default)] pub codex_error_info: Option, + /// Sensitive explanation and steering are delivered live but never enter rollout storage. + #[serde(skip)] + #[schemars(skip)] + #[ts(skip)] + pub misalignment: Option, } impl ErrorEvent { @@ -5565,6 +5613,7 @@ mod tests { #[test] fn rollback_failed_error_does_not_affect_turn_status() { let event = ErrorEvent { + misalignment: None, message: "rollback failed".into(), codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), }; @@ -5574,6 +5623,7 @@ mod tests { #[test] fn active_turn_not_steerable_error_does_not_affect_turn_status() { let event = ErrorEvent { + misalignment: None, message: "cannot steer a review turn".into(), codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind::Review, @@ -5585,12 +5635,44 @@ mod tests { #[test] fn generic_error_affects_turn_status() { let event = ErrorEvent { + misalignment: None, message: "generic".into(), codex_error_info: Some(CodexErrorInfo::Other), }; assert!(event.affects_turn_status()); } + #[test] + fn misalignment_explanation_and_steer_are_never_serialized_into_error_events() { + let event = ErrorEvent { + message: "This request violated the misalignment policy.".to_string(), + codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), + misalignment: Some(MisalignmentErrorDetails { + error_type: Some("unauthorized_data_transfer".to_string()), + detailed_explanation: Some("Sensitive customer explanation".to_string()), + steer: Some(MisalignmentSteer { + message: "Sensitive customer steering".to_string(), + }), + }), + }; + + let serialized = serde_json::to_value(&event).expect("serialize error event"); + assert_eq!( + serialized, + json!({ + "message": "This request violated the misalignment policy.", + "codex_error_info": "misalignment_policy_violation" + }) + ); + let restored: ErrorEvent = + serde_json::from_value(serialized).expect("deserialize persisted error event"); + assert_eq!(restored.misalignment, None); + + let debug = format!("{event:?}"); + assert!(!debug.contains("Sensitive customer explanation")); + assert!(!debug.contains("Sensitive customer steering")); + } + #[test] fn realtime_conversation_started_event_uses_realtime_session_id() { let event = RealtimeConversationStartedEvent { diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 066df4e05e..a360285632 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -6497,6 +6497,7 @@ fn test_session_telemetry(config: &Config, model: &str) -> SessionTelemetry { #[test] fn active_turn_not_steerable_turn_error_extracts_structured_server_error() { let turn_error = AppServerTurnError { + misalignment: None, message: "cannot steer a review turn".to_string(), codex_error_info: Some(AppServerCodexErrorInfo::ActiveTurnNotSteerable { turn_kind: AppServerNonSteerableTurnKind::Review, diff --git a/codex-rs/tui/src/app/tests/rate_limits.rs b/codex-rs/tui/src/app/tests/rate_limits.rs index 419a37ac3e..13b4fe7617 100644 --- a/codex-rs/tui/src/app/tests/rate_limits.rs +++ b/codex-rs/tui/src/app/tests/rate_limits.rs @@ -88,6 +88,7 @@ fn deliver_usage_limit_error(app: &mut App) { app.chat_widget.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "Usage limit reached.".to_string(), codex_error_info: Some(CodexErrorInfo::UsageLimitExceeded), additional_details: None, diff --git a/codex-rs/tui/src/app/tests/turn_submission.rs b/codex-rs/tui/src/app/tests/turn_submission.rs index ff2265ef88..60342fd081 100644 --- a/codex-rs/tui/src/app/tests/turn_submission.rs +++ b/codex-rs/tui/src/app/tests/turn_submission.rs @@ -86,6 +86,7 @@ async fn misalignment_policy_blocks_queued_turns_and_goal_resumption() -> Result let queued_turn = next_user_turn_op(&mut op_rx); let failed_turn = Turn { error: Some(AppServerTurnError { + misalignment: None, message: "misalignment policy violation".to_string(), codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), additional_details: None, @@ -176,6 +177,7 @@ async fn misalignment_policy_in_parent_stops_active_side_conversation() -> Resul parent_thread_id, ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "misalignment policy violation".to_string(), codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), additional_details: None, diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 7b99cda901..2fb09c6124 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -1173,6 +1173,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() { chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "permission denied".to_string(), codex_error_info: None, additional_details: None, @@ -1196,6 +1197,7 @@ async fn live_app_server_failed_turn_does_not_duplicate_error_history() { items: Vec::new(), status: AppServerTurnStatus::Failed, error: Some(AppServerTurnError { + misalignment: None, message: "permission denied".to_string(), codex_error_info: None, additional_details: None, @@ -1330,6 +1332,7 @@ async fn live_app_server_stream_recovery_restores_previous_status_header() { chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "Reconnecting... 1/5".to_string(), codex_error_info: Some(CodexErrorInfo::Other), additional_details: None, @@ -1408,6 +1411,7 @@ async fn live_app_server_server_overloaded_error_renders_warning() { chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "server overloaded".to_string(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), additional_details: None, @@ -1450,6 +1454,7 @@ async fn live_app_server_cyber_policy_error_renders_dedicated_notice() { chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "server fallback message".to_string(), codex_error_info: Some(CodexErrorInfo::CyberPolicy), additional_details: None, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 309d67f2bc..73a33d83a7 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -415,6 +415,7 @@ pub(super) fn handle_error( chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: message.into(), codex_error_info, additional_details: None, @@ -448,6 +449,7 @@ pub(super) fn handle_stream_error_with_replay( chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: message.into(), codex_error_info: None, additional_details, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 2496a0f751..7307c252ac 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -95,6 +95,7 @@ async fn replayed_failed_turns_preserve_overload_warnings_between_retries() { /*duration_ms*/ None, /*error*/ Some(AppServerTurnError { + misalignment: None, message: error_message.to_string(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), additional_details: None, @@ -1024,6 +1025,7 @@ async fn replayed_retryable_app_server_error_keeps_turn_running() { chat.handle_server_notification( ServerNotification::Error(ErrorNotification { error: AppServerTurnError { + misalignment: None, message: "Reconnecting... 1/5".to_string(), codex_error_info: None, additional_details: Some("Idle timeout waiting for SSE".to_string()), diff --git a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs index a207872b28..396bdc76bc 100644 --- a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs @@ -76,6 +76,7 @@ async fn misalignment_policy_failure_stops_the_thread_and_renders_once() { AppServerTurnStatus::Failed, /*duration_ms*/ None, Some(AppServerTurnError { + misalignment: None, message: ERROR_MESSAGE.to_string(), codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation), additional_details: None, diff --git a/codex-rs/tui/src/chatwidget/thread_usage_tests.rs b/codex-rs/tui/src/chatwidget/thread_usage_tests.rs index d166325ffb..7fb80eb5a1 100644 --- a/codex-rs/tui/src/chatwidget/thread_usage_tests.rs +++ b/codex-rs/tui/src/chatwidget/thread_usage_tests.rs @@ -246,6 +246,7 @@ async fn replayed_turn_completions_do_not_start_live_billing_refreshes() { ( TurnStatus::Failed, Some(TurnError { + misalignment: None, message: "the replayed turn failed".to_string(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), additional_details: None, @@ -323,6 +324,7 @@ async fn replayed_errors_do_not_start_live_billing_refreshes() { let notification = ServerNotification::Error(ErrorNotification { error: TurnError { + misalignment: None, message: "the replayed turn failed".to_string(), codex_error_info: Some(CodexErrorInfo::ServerOverloaded), additional_details: None,