Merge a21b9e5173 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-04-22 22:36:12 -07:00
committed by GitHub
128 changed files with 2076 additions and 107 deletions
@@ -480,6 +480,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -2260,6 +2261,34 @@
],
"type": "object"
},
"ModelVerification": {
"enum": [
"trustedAccessForCyber"
],
"type": "string"
},
"ModelVerificationNotification": {
"properties": {
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
},
"verifications": {
"items": {
"$ref": "#/definitions/ModelVerification"
},
"type": "array"
}
},
"required": [
"threadId",
"turnId",
"verifications"
],
"type": "object"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
@@ -5339,6 +5368,26 @@
"title": "Model/reroutedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"model/verification"
],
"title": "Model/verificationNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ModelVerificationNotification"
}
},
"required": [
"method",
"params"
],
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -4424,6 +4424,26 @@
"title": "Model/reroutedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"model/verification"
],
"title": "Model/verificationNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ModelVerificationNotification"
}
},
"required": [
"method",
"params"
],
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -5985,6 +6005,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -10904,6 +10925,36 @@
],
"type": "object"
},
"ModelVerification": {
"enum": [
"trustedAccessForCyber"
],
"type": "string"
},
"ModelVerificationNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
},
"verifications": {
"items": {
"$ref": "#/definitions/v2/ModelVerification"
},
"type": "array"
}
},
"required": [
"threadId",
"turnId",
"verifications"
],
"title": "ModelVerificationNotification",
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
@@ -2565,6 +2565,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -7639,6 +7640,36 @@
],
"type": "object"
},
"ModelVerification": {
"enum": [
"trustedAccessForCyber"
],
"type": "string"
},
"ModelVerificationNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
},
"verifications": {
"items": {
"$ref": "#/definitions/ModelVerification"
},
"type": "array"
}
},
"required": [
"threadId",
"turnId",
"verifications"
],
"title": "ModelVerificationNotification",
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
@@ -10952,6 +10983,26 @@
"title": "Model/reroutedNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"model/verification"
],
"title": "Model/verificationNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ModelVerificationNotification"
}
},
"required": [
"method",
"params"
],
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -9,6 +9,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ModelVerification": {
"enum": [
"trustedAccessForCyber"
],
"type": "string"
}
},
"properties": {
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
},
"verifications": {
"items": {
"$ref": "#/definitions/ModelVerification"
},
"type": "array"
}
},
"required": [
"threadId",
"turnId",
"verifications"
],
"title": "ModelVerificationNotification",
"type": "object"
}
@@ -32,6 +32,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -94,6 +94,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -94,6 +94,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -94,6 +94,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -32,6 +32,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -35,6 +35,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -32,6 +32,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -32,6 +32,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -32,6 +32,7 @@
"contextWindowExceeded",
"usageLimitExceeded",
"serverOverloaded",
"cyberPolicy",
"internalServerError",
"unauthorized",
"badRequest",
@@ -29,6 +29,7 @@ import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOau
import type { McpServerStatusUpdatedNotification } from "./v2/McpServerStatusUpdatedNotification";
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
import type { ModelVerificationNotification } from "./v2/ModelVerificationNotification";
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification";
import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification";
@@ -63,4 +64,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification };
@@ -9,4 +9,4 @@ import type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
* 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.
*/
export type CodexErrorInfo = "contextWindowExceeded" | "usageLimitExceeded" | "serverOverloaded" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other";
export type CodexErrorInfo = "contextWindowExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other";
@@ -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 ModelVerification = "trustedAccessForCyber";
@@ -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 { ModelVerification } from "./ModelVerification";
export type ModelVerificationNotification = { threadId: string, turnId: string, verifications: Array<ModelVerification>, };
@@ -231,6 +231,8 @@ export type { ModelListResponse } from "./ModelListResponse";
export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
export type { ModelVerification } from "./ModelVerification";
export type { ModelVerificationNotification } from "./ModelVerificationNotification";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
@@ -1060,6 +1060,7 @@ server_notification_definitions! {
/// Deprecated: Use `ContextCompaction` item type instead.
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification),
ModelVerification => "model/verification" (v2::ModelVerificationNotification),
Warning => "warning" (v2::WarningNotification),
GuardianWarning => "guardianWarning" (v2::GuardianWarningNotification),
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
@@ -72,6 +72,7 @@ use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary;
use codex_protocol::protocol::HookScope as CoreHookScope;
use codex_protocol::protocol::HookSource as CoreHookSource;
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
use codex_protocol::protocol::ModelVerification as CoreModelVerification;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind;
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
@@ -163,6 +164,7 @@ pub enum CodexErrorInfo {
ContextWindowExceeded,
UsageLimitExceeded,
ServerOverloaded,
CyberPolicy,
HttpConnectionFailed {
#[serde(rename = "httpStatusCode")]
#[ts(rename = "httpStatusCode")]
@@ -207,6 +209,7 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
CoreCodexErrorInfo::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded,
CoreCodexErrorInfo::UsageLimitExceeded => CodexErrorInfo::UsageLimitExceeded,
CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded,
CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy,
CoreCodexErrorInfo::HttpConnectionFailed { http_status_code } => {
CodexErrorInfo::HttpConnectionFailed { http_status_code }
}
@@ -425,6 +428,12 @@ v2_enum_from_core!(
}
);
v2_enum_from_core!(
pub enum ModelVerification from CoreModelVerification {
TrustedAccessForCyber
}
);
v2_enum_from_core!(
pub enum HookEventName from CoreHookEventName {
PreToolUse, PermissionRequest, PostToolUse, SessionStart, UserPromptSubmit, Stop
@@ -7357,6 +7366,15 @@ pub struct ModelReroutedNotification {
pub reason: ModelRerouteReason,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ModelVerificationNotification {
pub thread_id: String,
pub turn_id: String,
pub verifications: Vec<ModelVerification>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -9968,6 +9986,14 @@ mod tests {
);
}
#[test]
fn codex_error_info_serializes_cyber_policy_in_camel_case() {
assert_eq!(
serde_json::to_value(CodexErrorInfo::CyberPolicy).unwrap(),
json!("cyberPolicy")
);
}
#[test]
fn codex_error_info_serializes_active_turn_not_steerable_turn_kind_in_camel_case() {
let value = CodexErrorInfo::ActiveTurnNotSteerable {
+1
View File
@@ -1034,6 +1034,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
- `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`.
- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
- `model/verification` — `{ threadId, turnId, verifications }` when the backend flags additional account verification, such as `trustedAccessForCyber`.
Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed.
@@ -61,6 +61,7 @@ use codex_app_server_protocol::McpToolCallError;
use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::McpToolCallStatus;
use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::ModelVerificationNotification;
use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext;
use codex_app_server_protocol::NetworkPolicyAmendment as V2NetworkPolicyAmendment;
use codex_app_server_protocol::NetworkPolicyRuleAction as V2NetworkPolicyRuleAction;
@@ -399,6 +400,18 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
}
EventMsg::ModelVerification(event) => {
if let ApiVersion::V2 = api_version {
let notification = ModelVerificationNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
verifications: event.verifications.into_iter().map(Into::into).collect(),
};
outgoing
.send_server_notification(ServerNotification::ModelVerification(notification))
.await;
}
}
EventMsg::RealtimeConversationStarted(event) => {
if let ApiVersion::V2 = api_version {
let notification = ThreadRealtimeStartedNotification {
@@ -660,6 +660,8 @@ mod tests {
use codex_app_server_protocol::GuardianWarningNotification;
use codex_app_server_protocol::ModelRerouteReason;
use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::ModelVerification;
use codex_app_server_protocol::ModelVerificationNotification;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_app_server_protocol::ToolRequestUserInputParams;
@@ -863,6 +865,30 @@ mod tests {
);
}
#[test]
fn verify_model_verification_notification_serialization() {
let notification = ServerNotification::ModelVerification(ModelVerificationNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
verifications: vec![ModelVerification::TrustedAccessForCyber],
});
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
assert_eq!(
json!({
"method": "model/verification",
"params": {
"threadId": "thread-1",
"turnId": "turn-1",
"verifications": ["trustedAccessForCyber"],
},
}),
serde_json::to_value(jsonrpc_notification)
.expect("ensure the notification serializes correctly"),
"ensure the notification serializes correctly"
);
}
#[tokio::test]
async fn send_response_routes_to_target_connection() {
let (tx, mut rx) = mpsc::channel::<OutgoingEnvelope>(4);
@@ -1,12 +1,16 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::ErrorNotification;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ModelRerouteReason;
use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::ModelVerification;
use codex_app_server_protocol::ModelVerificationNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadStartParams;
@@ -19,10 +23,14 @@ use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::ResponseTemplate;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const REQUESTED_MODEL: &str = "gpt-5.4";
const SERVER_MODEL: &str = "gpt-5.3-codex";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
const CYBER_POLICY_MESSAGE: &str =
"This request has been flagged for potentially high-risk cyber activity.";
#[tokio::test]
async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()> {
@@ -88,6 +96,75 @@ async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() ->
Ok(())
}
#[tokio::test]
async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = ResponseTemplate::new(400).set_body_json(serde_json::json!({
"error": {
"message": CYBER_POLICY_MESSAGE,
"type": "invalid_request",
"param": null,
"code": "cyber_policy"
}
}));
let _response_mock = responses::mount_response_once(&server, response).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some(REQUESTED_MODEL.to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "trigger cyber policy error".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let turn_start: TurnStartResponse = to_response(turn_resp)?;
let error = collect_cyber_policy_error_and_validate_no_reroute(&mut mcp).await?;
assert_eq!(
error,
ErrorNotification {
error: codex_app_server_protocol::TurnError {
message: CYBER_POLICY_MESSAGE.to_string(),
codex_error_info: Some(CodexErrorInfo::CyberPolicy),
additional_details: None,
},
will_retry: false,
thread_id: thread.id,
turn_id: turn_start.turn.id,
}
);
Ok(())
}
#[tokio::test]
async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested()
-> Result<()> {
@@ -161,6 +238,73 @@ async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when
Ok(())
}
#[tokio::test]
async fn model_verification_emits_typed_notification_and_warning_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let body = responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_model_verification_metadata(
"resp-1",
vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION],
),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
]);
let response = responses::sse_response(body);
let _response_mock = responses::mount_response_once(&server, response).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some(REQUESTED_MODEL.to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "trigger model verification".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let turn_start: TurnStartResponse = to_response(turn_resp)?;
let verification =
collect_model_verification_notifications_and_validate_no_warning_item(&mut mcp).await?;
assert_eq!(
verification,
ModelVerificationNotification {
thread_id: thread.id,
turn_id: turn_start.turn.id,
verifications: vec![ModelVerification::TrustedAccessForCyber],
}
);
Ok(())
}
async fn collect_turn_notifications_and_validate_no_warning_item(
mcp: &mut McpProcess,
) -> Result<ModelReroutedNotification> {
@@ -203,6 +347,90 @@ async fn collect_turn_notifications_and_validate_no_warning_item(
}
}
async fn collect_model_verification_notifications_and_validate_no_warning_item(
mcp: &mut McpProcess,
) -> Result<ModelVerificationNotification> {
let mut verification = None;
loop {
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
let JSONRPCMessage::Notification(notification) = message else {
continue;
};
match notification.method.as_str() {
"model/verification" => {
let params = notification.params.ok_or_else(|| {
anyhow::anyhow!("model/verification notifications must include params")
})?;
let payload: ModelVerificationNotification = serde_json::from_value(params)?;
verification = Some(payload);
}
"warning" => {
anyhow::bail!("verification-only response must not emit warning");
}
"model/rerouted" => {
anyhow::bail!("verification-only response must not emit model/rerouted");
}
"item/started" => {
let params = notification.params.ok_or_else(|| {
anyhow::anyhow!("item/started notifications must include params")
})?;
let payload: ItemStartedNotification = serde_json::from_value(params)?;
assert!(!is_warning_user_message_item(&payload.item));
}
"item/completed" => {
let params = notification.params.ok_or_else(|| {
anyhow::anyhow!("item/completed notifications must include params")
})?;
let payload: ItemCompletedNotification = serde_json::from_value(params)?;
assert!(!is_warning_user_message_item(&payload.item));
}
"turn/completed" => {
let verification = verification.ok_or_else(|| {
anyhow::anyhow!(
"expected model/verification notification before turn/completed"
)
})?;
return Ok(verification);
}
_ => {}
}
}
}
async fn collect_cyber_policy_error_and_validate_no_reroute(
mcp: &mut McpProcess,
) -> Result<ErrorNotification> {
let mut error = None;
loop {
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
let JSONRPCMessage::Notification(notification) = message else {
continue;
};
match notification.method.as_str() {
"error" => {
let params = notification
.params
.ok_or_else(|| anyhow::anyhow!("error notifications must include params"))?;
let payload: ErrorNotification = serde_json::from_value(params)?;
if payload.error.codex_error_info == Some(CodexErrorInfo::CyberPolicy) {
error = Some(payload);
}
}
"model/rerouted" => {
anyhow::bail!("cyber policy response must not emit model/rerouted");
}
"turn/completed" => {
return error.ok_or_else(|| {
anyhow::anyhow!("expected cyber policy error before turn/completed")
});
}
_ => {}
}
}
}
fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
let ThreadItem::UserMessage { content, .. } = item else {
return None;
+3
View File
@@ -69,6 +69,9 @@ fn response_event_to_json(event: codex_api::ResponseEvent) -> serde_json::Value
codex_api::ResponseEvent::ServerModel(model) => {
json!({ "type": "response.server_model", "model": model })
}
codex_api::ResponseEvent::ModelVerifications(verifications) => {
json!({ "type": "response.model_verifications", "verifications": verifications })
}
codex_api::ResponseEvent::ServerReasoningIncluded(included) => {
json!({ "type": "response.server_reasoning_included", "included": included })
}
+17 -1
View File
@@ -32,6 +32,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
identity_error_code: None,
}),
ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
ApiError::CyberPolicy { message } => CodexErr::CyberPolicy { message },
ApiError::Transport(transport) => match transport {
TransportError::Http {
status,
@@ -55,7 +56,19 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
}
if status == http::StatusCode::BAD_REQUEST {
if body_text
if let Ok(parsed) = serde_json::from_str::<Value>(&body_text)
&& let Some(error) = parsed.get("error")
&& error.get("code").and_then(Value::as_str)
== Some(CYBER_POLICY_ERROR_CODE)
{
let message = error
.get("message")
.and_then(Value::as_str)
.filter(|message| !message.trim().is_empty())
.map(str::to_string)
.unwrap_or_else(|| CYBER_POLICY_FALLBACK_MESSAGE.to_string());
CodexErr::CyberPolicy { message }
} else if body_text
.contains("The image data you provided does not represent a valid image")
{
CodexErr::InvalidImageRequest()
@@ -125,6 +138,9 @@ const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
const CF_RAY_HEADER: &str = "cf-ray";
const X_OPENAI_AUTHORIZATION_ERROR_HEADER: &str = "x-openai-authorization-error";
const X_ERROR_JSON_HEADER: &str = "x-error-json";
const CYBER_POLICY_ERROR_CODE: &str = "cyber_policy";
const CYBER_POLICY_FALLBACK_MESSAGE: &str =
"This request has been flagged for possible cybersecurity risk.";
#[cfg(test)]
#[path = "api_bridge_tests.rs"]
@@ -26,6 +26,104 @@ fn map_api_error_maps_server_overloaded_from_503_body() {
assert!(matches!(err, CodexErr::ServerOverloaded));
}
#[test]
fn map_api_error_maps_cyber_policy_from_400_body() {
let body = serde_json::json!({
"error": {
"message": "This request has been flagged for potentially high-risk cyber activity.",
"type": "invalid_request",
"param": null,
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(
message,
"This request has been flagged for potentially high-risk cyber activity."
);
}
#[test]
fn map_api_error_maps_wrapped_websocket_cyber_policy_from_400_body() {
let body = serde_json::json!({
"type": "error",
"status": 400,
"error": {
"message": "This websocket request was flagged.",
"type": "invalid_request",
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("ws://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(message, "This websocket request was flagged.");
}
#[test]
fn map_api_error_uses_cyber_policy_fallback_for_missing_message() {
let body = serde_json::json!({
"error": {
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(
message,
"This request has been flagged for possible cybersecurity risk."
);
}
#[test]
fn map_api_error_keeps_unknown_400_errors_generic() {
let body = serde_json::json!({
"error": {
"message": "Some other bad request.",
"code": "some_other_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body.clone()),
}));
let CodexErr::InvalidRequest(message) = err else {
panic!("expected CodexErr::InvalidRequest, got {err:?}");
};
assert_eq!(message, body);
}
#[test]
fn map_api_error_maps_usage_limit_limit_name_header() {
let mut headers = HeaderMap::new();
+3
View File
@@ -3,6 +3,7 @@ use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::Verbosity as VerbosityConfig;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::W3cTraceContext;
@@ -71,6 +72,8 @@ pub enum ResponseEvent {
/// Emitted when the server includes `OpenAI-Model` on the stream response.
/// This can differ from the requested model when backend safety routing applies.
ServerModel(String),
/// Emitted when the server recommends additional account verification.
ModelVerifications(Vec<ModelVerification>),
/// Emitted when `X-Reasoning-Included: true` is present on the response,
/// meaning the server already accounted for past reasoning tokens and the
/// client should not re-estimate them.
@@ -608,6 +608,7 @@ async fn run_websocket_response_stream(
continue;
}
};
let model_verifications = event.model_verifications();
if event.kind() == "codex.rate_limits" {
if let Some(snapshot) = parse_rate_limit_event(&text) {
let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;
@@ -622,6 +623,16 @@ async fn run_websocket_response_stream(
.await;
last_server_model = Some(model);
}
if let Some(verifications) = model_verifications
&& tx_event
.send(Ok(ResponseEvent::ModelVerifications(verifications)))
.await
.is_err()
{
return Err(ApiError::Stream(
"response event consumer dropped".to_string(),
));
}
match process_responses_event(event) {
Ok(Some(event)) => {
let is_completed = matches!(event, ResponseEvent::Completed { .. });
+2
View File
@@ -27,6 +27,8 @@ pub enum ApiError {
RateLimit(String),
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("cyber policy: {message}")]
CyberPolicy { message: String },
#[error("server overloaded")]
ServerOverloaded,
}
+231 -3
View File
@@ -7,6 +7,7 @@ use codex_client::ByteStream;
use codex_client::StreamResponse;
use codex_client::TransportError;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::TokenUsage;
use eventsource_stream::Eventsource;
use futures::StreamExt;
@@ -27,6 +28,7 @@ use tracing::trace;
const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";
const OPENAI_MODEL_HEADER: &str = "openai-model";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
/// Streams SSE events from an on-disk fixture for tests.
pub fn stream_from_fixture(
@@ -165,6 +167,7 @@ pub struct ResponsesStreamEvent {
#[serde(rename = "type")]
pub(crate) kind: String,
headers: Option<Value>,
metadata: Option<Value>,
response: Option<Value>,
item: Option<Value>,
item_id: Option<String>,
@@ -183,8 +186,7 @@ impl ResponsesStreamEvent {
///
/// Precedence:
/// 1. `response.headers` for standard Responses stream events.
/// 2. top-level `headers` for websocket metadata events (for example
/// `codex.response.metadata`).
/// 2. top-level `headers` for websocket metadata events.
pub fn response_model(&self) -> Option<String> {
let response_headers_model = self
.response
@@ -200,6 +202,17 @@ impl ResponsesStreamEvent {
.and_then(header_openai_model_value_from_json),
}
}
pub(crate) fn model_verifications(&self) -> Option<Vec<ModelVerification>> {
if self.kind() != "response.metadata" {
return None;
}
self.metadata
.as_ref()
.and_then(|metadata| metadata.get("openai_verification_recommendation"))
.and_then(model_verifications_from_json_value)
}
}
fn header_openai_model_value_from_json(value: &Value) -> Option<String> {
@@ -214,6 +227,38 @@ fn header_openai_model_value_from_json(value: &Value) -> Option<String> {
})
}
fn model_verifications_from_json_value(value: &Value) -> Option<Vec<ModelVerification>> {
let verifications = value
.as_array()
.map(|items| {
let mut verifications = Vec::new();
for verification in items
.iter()
.filter_map(Value::as_str)
.filter_map(parse_model_verification)
{
if !verifications.contains(&verification) {
verifications.push(verification);
}
}
verifications
})
.unwrap_or_default();
if verifications.is_empty() {
None
} else {
Some(verifications)
}
}
fn parse_model_verification(value: &str) -> Option<ModelVerification> {
match value {
TRUSTED_ACCESS_FOR_CYBER_VERIFICATION => Some(ModelVerification::TrustedAccessForCyber),
_ => None,
}
}
fn json_value_as_string(value: &Value) -> Option<String> {
match value {
Value::String(value) => Some(value.clone()),
@@ -296,6 +341,9 @@ pub fn process_responses_event(
response_error = ApiError::QuotaExceeded;
} else if is_usage_not_included(&error) {
response_error = ApiError::UsageNotIncluded;
} else if is_cyber_policy_error(&error) {
let message = cyber_policy_message(error.message);
response_error = ApiError::CyberPolicy { message };
} else if is_invalid_prompt_error(&error) {
let message = error
.message
@@ -414,6 +462,7 @@ pub async fn process_sse(
continue;
}
};
let model_verifications = event.model_verifications();
if let Some(model) = event.response_model()
&& last_server_model.as_deref() != Some(model.as_str())
@@ -427,6 +476,14 @@ pub async fn process_sse(
}
last_server_model = Some(model);
}
if let Some(verifications) = model_verifications
&& tx_event
.send(Ok(ResponseEvent::ModelVerifications(verifications)))
.await
.is_err()
{
return;
}
match process_responses_event(event) {
Ok(Some(event)) => {
@@ -488,11 +545,25 @@ fn is_invalid_prompt_error(error: &Error) -> bool {
error.code.as_deref() == Some("invalid_prompt")
}
fn is_cyber_policy_error(error: &Error) -> bool {
error.code.as_deref() == Some("cyber_policy")
}
fn is_server_overloaded_error(error: &Error) -> bool {
error.code.as_deref() == Some("server_is_overloaded")
|| error.code.as_deref() == Some("slow_down")
}
fn cyber_policy_fallback_message() -> String {
"This request has been flagged for possible cybersecurity risk.".to_string()
}
fn cyber_policy_message(message: Option<String>) -> String {
message
.filter(|message| !message.trim().is_empty())
.unwrap_or_else(cyber_policy_fallback_message)
}
fn rate_limit_regex() -> &'static regex_lite::Regex {
static RE: std::sync::OnceLock<regex_lite::Regex> = std::sync::OnceLock::new();
#[expect(clippy::unwrap_used)]
@@ -841,6 +912,45 @@ mod tests {
assert_matches!(events[0], Err(ApiError::QuotaExceeded));
}
#[tokio::test]
async fn cyber_policy_error_is_fatal() {
let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_fatal_cyber","object":"response","created_at":1759771626,"status":"failed","background":false,"error":{"code":"cyber_policy","message":"This request was flagged for cyber policy."},"incomplete_details":null}}"#;
let sse1 = format!("event: response.failed\ndata: {raw_error}\n\n");
let events = collect_events(&[sse1.as_bytes()]).await;
assert_eq!(events.len(), 1);
match &events[0] {
Err(ApiError::CyberPolicy { message }) => {
assert_eq!(message, "This request was flagged for cyber policy.");
}
other => panic!("unexpected event: {other:?}"),
}
}
#[tokio::test]
async fn cyber_policy_error_uses_fallback_for_empty_message() {
let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_fatal_cyber","object":"response","created_at":1759771626,"status":"failed","background":false,"error":{"code":"cyber_policy","message":" "},"incomplete_details":null}}"#;
let sse1 = format!("event: response.failed\ndata: {raw_error}\n\n");
let events = collect_events(&[sse1.as_bytes()]).await;
assert_eq!(events.len(), 1);
match &events[0] {
Err(ApiError::CyberPolicy { message }) => {
assert_eq!(
message,
"This request has been flagged for possible cybersecurity risk."
);
}
other => panic!("unexpected event: {other:?}"),
}
}
#[tokio::test]
async fn invalid_prompt_without_type_is_invalid_request() {
let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_invalid_prompt_no_type","object":"response","created_at":1759771628,"status":"failed","background":false,"error":{"code":"invalid_prompt","message":"Invalid prompt: we've limited access to this content for safety reasons."},"incomplete_details":null}}"#;
@@ -975,6 +1085,43 @@ mod tests {
}
}
#[tokio::test]
async fn spawn_response_stream_ignores_model_verification_header() {
let mut headers = HeaderMap::new();
headers.insert(
"openai-verification-recommendation",
HeaderValue::from_static(TRUSTED_ACCESS_FOR_CYBER_VERIFICATION),
);
let completed = json!({
"type": "response.completed",
"response": { "id": "resp-1" }
});
let sse = format!("event: response.completed\ndata: {completed}\n\n");
let bytes = stream::iter(vec![Ok(Bytes::from(sse))]);
let stream_response = StreamResponse {
status: StatusCode::OK,
headers,
bytes: Box::pin(bytes),
};
let mut stream = spawn_response_stream(
stream_response,
idle_timeout(),
/*telemetry*/ None,
/*turn_state*/ None,
);
let mut events = Vec::new();
while let Some(event) = stream.rx_event.recv().await {
events.push(event.expect("expected ok event"));
}
assert!(
!events
.iter()
.any(|event| matches!(event, ResponseEvent::ModelVerifications(_)))
);
}
#[tokio::test]
async fn process_sse_ignores_response_model_field_in_payload() {
let events = run_sse(vec![
@@ -1042,10 +1189,44 @@ mod tests {
);
}
#[tokio::test]
async fn process_sse_emits_model_verification_field() {
let events = run_sse(vec![
json!({
"type": "response.metadata",
"sequence_number": 1,
"response_id": "resp-1",
"metadata": {
"openai_verification_recommendation": [TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]
}
}),
json!({
"type": "response.completed",
"response": {
"id": "resp-1"
}
}),
])
.await;
assert_matches!(
&events[0],
ResponseEvent::ModelVerifications(verifications)
if verifications == &vec![ModelVerification::TrustedAccessForCyber]
);
assert_matches!(
&events[1],
ResponseEvent::Completed {
response_id,
token_usage: None
} if response_id == "resp-1"
);
}
#[test]
fn responses_stream_event_response_model_reads_top_level_headers() {
let ev: ResponsesStreamEvent = serde_json::from_value(json!({
"type": "codex.response.metadata",
"type": "response.metadata",
"headers": {
"openai-model": CYBER_RESTRICTED_MODEL_FOR_TESTS,
}
@@ -1080,6 +1261,53 @@ mod tests {
);
}
#[test]
fn responses_stream_event_model_verification_reads_metadata_field() {
let event = json!({
"type": "response.metadata",
"sequence_number": 1,
"response_id": "resp-1",
"metadata": {
"openai_verification_recommendation": [TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]
}
});
let event: ResponsesStreamEvent =
serde_json::from_value(event).expect("expected event to deserialize");
assert_eq!(
event.model_verifications(),
Some(vec![ModelVerification::TrustedAccessForCyber])
);
}
#[test]
fn responses_stream_event_model_verification_ignores_unknown_field() {
let event = json!({
"type": "response.metadata",
"metadata": {
"openai_verification_recommendation": ["unknown"]
}
});
let event: ResponsesStreamEvent =
serde_json::from_value(event).expect("expected event to deserialize");
assert_eq!(event.model_verifications(), None);
}
#[test]
fn responses_stream_event_model_verification_ignores_non_array_field() {
let event = json!({
"type": "response.metadata",
"metadata": {
"openai_verification_recommendation": TRUSTED_ACCESS_FOR_CYBER_VERIFICATION
}
});
let event: ResponsesStreamEvent =
serde_json::from_value(event).expect("expected event to deserialize");
assert_eq!(event.model_verifications(), None);
}
#[test]
fn test_try_parse_retry_after() {
let err = Error {
@@ -42,6 +42,7 @@ use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::RequestId as ProtocolRequestId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
@@ -646,6 +647,8 @@ pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_profile: Option<PermissionProfile>,
pub sandbox_policy: SandboxPolicy,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub sandbox_cwd: PathBuf,
+1
View File
@@ -23,6 +23,7 @@ use codex_protocol::protocol::AskForApproval;
#[schemars(deny_unknown_fields)]
pub struct ConfigProfile {
pub model: Option<String>,
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
pub service_tier: Option<ServiceTier>,
/// The key in the `model_providers` map identifying the
/// [`ModelProviderInfo`] to use.
+2
View File
@@ -615,6 +615,8 @@ pub struct Notice {
pub hide_full_access_warning: Option<bool>,
/// Tracks whether the user has acknowledged the Windows world-writable directories warning.
pub hide_world_writable_warning: Option<bool>,
/// Tracks whether the user opted out of Codex-managed fast defaults.
pub fast_default_opt_out: Option<bool>,
/// Tracks whether the user opted out of the rate limit model switch reminder.
pub hide_rate_limit_model_nudge: Option<bool>,
/// Tracks whether the user has seen the model migration prompt
+10 -1
View File
@@ -631,7 +631,12 @@
"$ref": "#/definitions/SandboxMode"
},
"service_tier": {
"$ref": "#/definitions/ServiceTier"
"allOf": [
{
"$ref": "#/definitions/ServiceTier"
}
],
"description": "Optional explicit service tier preference for new turns (`fast` or `flex`)."
},
"tools": {
"$ref": "#/definitions/ToolsToml"
@@ -1397,6 +1402,10 @@
},
"description": "Tracks scopes where external config migration prompts should be suppressed."
},
"fast_default_opt_out": {
"description": "Tracks whether the user opted out of Codex-managed fast defaults.",
"type": "boolean"
},
"hide_full_access_warning": {
"description": "Tracks whether the user has acknowledged the full access warning prompt.",
"type": "boolean"
+45
View File
@@ -39,6 +39,7 @@ use codex_config::types::McpServerTransportConfig;
use codex_config::types::MemoriesConfig;
use codex_config::types::MemoriesToml;
use codex_config::types::ModelAvailabilityNuxConfig;
use codex_config::types::Notice;
use codex_config::types::NotificationCondition;
use codex_config::types::NotificationMethod;
use codex_config::types::Notifications;
@@ -5298,6 +5299,50 @@ async fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result<
Ok(())
}
#[tokio::test]
async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
let config = Config::load_from_base_config_with_overrides(
fixture.cfg.clone(),
ConfigOverrides {
cwd: Some(fixture.cwd_path()),
service_tier: Some(None),
..Default::default()
},
fixture.codex_home(),
)
.await?;
assert_eq!(config.service_tier, None);
assert_eq!(config.notices.fast_default_opt_out, Some(true));
Ok(())
}
#[tokio::test]
async fn fast_default_opt_out_notice_config_is_respected() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
let mut cfg = fixture.cfg.clone();
cfg.notice = Some(Notice {
fast_default_opt_out: Some(true),
..Default::default()
});
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides {
cwd: Some(fixture.cwd_path()),
..Default::default()
},
fixture.codex_home(),
)
.await?;
assert_eq!(config.service_tier, None);
assert_eq!(config.notices.fast_default_opt_out, Some(true));
Ok(())
}
#[tokio::test]
async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
+13
View File
@@ -37,6 +37,8 @@ pub enum ConfigEdit {
SetNoticeHideFullAccessWarning(bool),
/// Toggle the Windows world-writable directories warning acknowledgement flag.
SetNoticeHideWorldWritableWarning(bool),
/// Toggle the opt-out marker for Codex-managed fast defaults.
SetNoticeFastDefaultOptOut(bool),
/// Toggle the rate limit model nudge acknowledgement flag.
SetNoticeHideRateLimitModelNudge(bool),
/// Toggle the Windows onboarding acknowledgement flag.
@@ -436,6 +438,11 @@ impl ConfigDocument {
&[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
value(*acknowledged),
)),
ConfigEdit::SetNoticeFastDefaultOptOut(opted_out) => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "fast_default_opt_out"],
value(*opted_out),
)),
ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
Scope::Global,
&[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
@@ -978,6 +985,12 @@ impl ConfigEditsBuilder {
self
}
pub fn set_fast_default_opt_out(mut self, opted_out: bool) -> Self {
self.edits
.push(ConfigEdit::SetNoticeFastDefaultOptOut(opted_out));
self
}
pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self {
self.edits
.push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged));
+14 -4
View File
@@ -2031,14 +2031,24 @@ impl Config {
let forced_login_method = cfg.forced_login_method;
let model = model.or(config_profile.model).or(cfg.model);
let service_tier = service_tier_override
.unwrap_or_else(|| config_profile.service_tier.or(cfg.service_tier));
let mut notices = cfg.notice.unwrap_or_default();
let service_tier = match service_tier_override {
Some(Some(service_tier)) => Some(service_tier),
Some(None) => {
// Preserve explicit standard/clear intent after the nested override
// collapses into `Config.service_tier = None`.
notices.fast_default_opt_out = Some(true);
None
}
None => config_profile.service_tier.or(cfg.service_tier),
};
let service_tier = match service_tier {
Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => {
Some(ServiceTier::Fast)
}
Some(ServiceTier::Fast) => None,
Some(ServiceTier::Flex) => Some(ServiceTier::Flex),
_ => None,
None => None,
};
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
@@ -2414,7 +2424,7 @@ impl Config {
active_profile: active_profile_name,
active_project,
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
notices: cfg.notice.unwrap_or_default(),
notices,
check_for_update_on_startup,
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
analytics_enabled: config_profile
@@ -691,6 +691,7 @@ async fn run_review_on_session(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: params.model.clone(),
effort: params.reasoning_effort,
summary: Some(params.reasoning_summary),
+1
View File
@@ -519,6 +519,7 @@ async fn augment_mcp_tool_request_meta_with_sandbox_state(
}
let sandbox_state = serde_json::to_value(SandboxState {
permission_profile: Some(turn_context.permission_profile()),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
sandbox_cwd: turn_context.cwd.to_path_buf(),
+2 -1
View File
@@ -135,6 +135,7 @@ pub(super) async fn user_input_or_turn_inner(
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
model,
effort,
summary,
@@ -162,7 +163,7 @@ pub(super) async fn user_input_or_turn_inner(
approval_policy: Some(approval_policy),
approvals_reviewer,
sandbox_policy: Some(sandbox_policy),
permission_profile: None,
permission_profile,
windows_sandbox_level: None,
collaboration_mode,
reasoning_summary: summary,
+46 -1
View File
@@ -77,6 +77,7 @@ use codex_otel::current_span_w3c_trace_context;
use codex_otel::set_parent_from_w3c_trace_context;
use codex_protocol::ThreadId;
use codex_protocol::ToolName;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
@@ -330,6 +331,8 @@ use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::McpServerRefreshConfig;
use codex_protocol::protocol::ModelRerouteEvent;
use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::ModelVerificationEvent;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::Op;
@@ -600,11 +603,20 @@ impl Codex {
developer_instructions: None,
},
};
let account_plan_type = auth_manager
.auth_cached()
.and_then(|auth| auth.account_plan_type());
let service_tier = get_service_tier(
config.service_tier,
config.notices.fast_default_opt_out.unwrap_or(false),
account_plan_type,
config.features.enabled(Feature::FastMode),
);
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
service_tier: config.service_tier,
service_tier,
developer_instructions: config.developer_instructions.clone(),
user_instructions,
personality: config.personality,
@@ -785,6 +797,27 @@ impl Codex {
}
}
fn get_service_tier(
configured_service_tier: Option<ServiceTier>,
fast_default_opt_out: bool,
account_plan_type: Option<AccountPlanType>,
fast_mode_enabled: bool,
) -> Option<ServiceTier> {
if configured_service_tier.is_some() || fast_default_opt_out || !fast_mode_enabled {
return configured_service_tier;
}
account_plan_type
.is_some_and(is_enterprise_default_service_tier_plan)
.then_some(ServiceTier::Fast)
}
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
plan_type == AccountPlanType::Enterprise
|| plan_type.is_business_like()
|| plan_type.is_team_like()
}
#[cfg(test)]
pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination {
futures::future::ready(()).boxed().shared()
@@ -2354,6 +2387,18 @@ impl Session {
true
}
pub(crate) async fn emit_model_verification(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
verifications: Vec<ModelVerification>,
) {
self.send_event(
turn_context,
EventMsg::ModelVerification(ModelVerificationEvent { verifications }),
)
.await;
}
pub(crate) async fn replace_history(
&self,
items: Vec<ResponseItem>,
+3
View File
@@ -1,5 +1,6 @@
use super::turn_context::image_generation_tool_auth_allowed;
use super::*;
use std::sync::atomic::AtomicBool;
/// Spawn a review thread using the given prompt.
pub(super) async fn spawn_review_thread(
@@ -140,6 +141,8 @@ pub(super) async fn spawn_review_thread(
turn_metadata_state,
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()),
turn_timing_state: Arc::new(TurnTimingState::default()),
server_model_warning_emitted: AtomicBool::new(false),
model_verification_emitted: AtomicBool::new(false),
};
// Seed the child task with the review prompt as the initial user message.
+101
View File
@@ -26,6 +26,8 @@ use codex_models_manager::bundled_models_response;
use codex_models_manager::model_info;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::FileSystemPermissions;
@@ -2643,6 +2645,104 @@ fn session_telemetry(
)
}
#[test]
fn get_service_tier_defaults_enterprise_accounts_to_fast() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::Enterprise),
/*fast_mode_enabled*/ true,
),
Some(ServiceTier::Fast)
);
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::EnterpriseCbpUsageBased),
/*fast_mode_enabled*/ true,
),
Some(ServiceTier::Fast)
);
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::Business),
/*fast_mode_enabled*/ true,
),
Some(ServiceTier::Fast)
);
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::Team),
/*fast_mode_enabled*/ true,
),
Some(ServiceTier::Fast)
);
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::SelfServeBusinessUsageBased),
/*fast_mode_enabled*/ true,
),
Some(ServiceTier::Fast)
);
}
#[test]
fn get_service_tier_respects_fast_default_opt_out() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ true,
Some(AccountPlanType::Enterprise),
/*fast_mode_enabled*/ true,
),
None
);
}
#[test]
fn get_service_tier_does_not_default_non_enterprise_or_disabled_fast_mode() {
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::Pro),
/*fast_mode_enabled*/ true,
),
None
);
assert_eq!(
get_service_tier(
/*configured_service_tier*/ None,
/*fast_default_opt_out*/ false,
Some(AccountPlanType::Enterprise),
/*fast_mode_enabled*/ false,
),
None
);
}
#[tokio::test]
async fn session_settings_null_service_tier_update_clears_service_tier() {
let session_configuration = make_session_configuration_for_tests().await;
let updated = session_configuration
.apply(&SessionSettingsUpdate {
service_tier: Some(None),
..Default::default()
})
.expect("null service tier update should apply");
assert_eq!(updated.service_tier, None);
}
pub(crate) async fn make_session_configuration_for_tests() -> SessionConfiguration {
let codex_home = tempfile::tempdir().expect("create temp dir");
let config = build_test_config(codex_home.path()).await;
@@ -3923,6 +4023,7 @@ async fn user_turn_updates_approvals_reviewer() {
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: turn_context.model_info.slug.clone(),
effort: config.model_reasoning_effort,
summary: config.model_reasoning_summary,
+17 -7
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::SkillInjections;
use crate::SkillLoadOutcome;
@@ -364,7 +365,6 @@ pub(crate) async fn run_turn(
// Although from the perspective of codex.rs, TurnDiffTracker has the lifecycle of a Task which contains
// many turns, from the perspective of the user, it is a single turn.
let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let mut server_model_warning_emitted_for_turn = false;
// `ModelClientSession` is turn-scoped and caches WebSocket + sticky routing state, so we reuse
// one instance across retries within this turn.
@@ -455,7 +455,6 @@ pub(crate) async fn run_turn(
sampling_request_input,
&explicitly_enabled_connectors,
skills_outcome,
&mut server_model_warning_emitted_for_turn,
cancellation_token.child_token(),
)
.await
@@ -1022,7 +1021,6 @@ async fn run_sampling_request(
input: Vec<ResponseItem>,
explicitly_enabled_connectors: &HashSet<String>,
skills_outcome: Option<&SkillLoadOutcome>,
server_model_warning_emitted_for_turn: &mut bool,
cancellation_token: CancellationToken,
) -> CodexResult<SamplingRequestResult> {
let router = built_tools(
@@ -1076,7 +1074,6 @@ async fn run_sampling_request(
client_session,
turn_metadata_header,
Arc::clone(&turn_diff_tracker),
server_model_warning_emitted_for_turn,
&prompt,
cancellation_token.child_token(),
)
@@ -1490,6 +1487,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
| EventMsg::RealtimeConversationRealtime(_)
| EventMsg::RealtimeConversationClosed(_)
| EventMsg::ModelReroute(_)
| EventMsg::ModelVerification(_)
| EventMsg::ContextCompacted(_)
| EventMsg::ThreadRolledBack(_)
| EventMsg::TurnStarted(_)
@@ -1866,7 +1864,6 @@ async fn try_run_sampling_request(
client_session: &mut ModelClientSession,
turn_metadata_header: Option<&str>,
turn_diff_tracker: SharedTurnDiffTracker,
server_model_warning_emitted_for_turn: &mut bool,
prompt: &Prompt,
cancellation_token: CancellationToken,
) -> CodexResult<SamplingRequestResult> {
@@ -2099,12 +2096,25 @@ async fn try_run_sampling_request(
}
}
ResponseEvent::ServerModel(server_model) => {
if !*server_model_warning_emitted_for_turn
if !turn_context
.server_model_warning_emitted
.load(Ordering::Relaxed)
&& sess
.maybe_warn_on_server_model_mismatch(&turn_context, server_model)
.await
{
*server_model_warning_emitted_for_turn = true;
turn_context
.server_model_warning_emitted
.store(true, Ordering::Relaxed);
}
}
ResponseEvent::ModelVerifications(verifications) => {
if !turn_context
.model_verification_emitted
.swap(true, Ordering::Relaxed)
{
sess.emit_model_verification(&turn_context, verifications)
.await;
}
}
ResponseEvent::ServerReasoningIncluded(included) => {
+12
View File
@@ -3,6 +3,8 @@ use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool {
matches!(
@@ -82,6 +84,8 @@ pub(crate) struct TurnContext {
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
pub(crate) turn_skills: TurnSkillsContext,
pub(crate) turn_timing_state: Arc<TurnTimingState>,
pub(crate) server_model_warning_emitted: AtomicBool,
pub(crate) model_verification_emitted: AtomicBool,
}
impl TurnContext {
pub(crate) fn permission_profile(&self) -> PermissionProfile {
@@ -216,6 +220,12 @@ impl TurnContext {
turn_metadata_state: self.turn_metadata_state.clone(),
turn_skills: self.turn_skills.clone(),
turn_timing_state: Arc::clone(&self.turn_timing_state),
server_model_warning_emitted: AtomicBool::new(
self.server_model_warning_emitted.load(Ordering::Relaxed),
),
model_verification_emitted: AtomicBool::new(
self.model_verification_emitted.load(Ordering::Relaxed),
),
}
}
@@ -469,6 +479,8 @@ impl Session {
turn_metadata_state,
turn_skills: TurnSkillsContext::new(skills_outcome),
turn_timing_state: Arc::new(TurnTimingState::default()),
server_model_warning_emitted: AtomicBool::new(false),
model_verification_emitted: AtomicBool::new(false),
}
}
@@ -54,8 +54,8 @@ use codex_shell_escalation::EscalationPolicy;
use codex_shell_escalation::EscalationSession;
use codex_shell_escalation::ExecParams;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::Permissions as EscalatedPermissions;
use codex_shell_escalation::PreparedExec;
use codex_shell_escalation::ResolvedPermissionProfile;
use codex_shell_escalation::ShellCommandExecutor;
use codex_shell_escalation::Stopwatch;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -371,13 +371,17 @@ impl CoreShellActionProvider {
.map(|_| {
// Shell request additional permissions were already normalized and
// merged into the first-attempt sandbox policy.
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: file_system_sandbox_policy.clone(),
network_sandbox_policy,
},
))
EscalationExecution::Permissions(
EscalationPermissions::ResolvedPermissionProfile(
ResolvedPermissionProfile {
permission_profile: PermissionProfile::from_runtime_permissions(
file_system_sandbox_policy,
network_sandbox_policy,
),
sandbox_policy: sandbox_policy.clone(),
},
),
)
})
.unwrap_or(EscalationExecution::TurnDefault),
}
@@ -844,9 +848,9 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
additional_permissions: None,
})?
}
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(
permission_profile,
)) => {
EscalationExecution::Permissions(
EscalationPermissions::AdditionalPermissionProfile(permission_profile),
) => {
// Merge additive permissions into the existing turn/request sandbox policy.
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
command,
@@ -858,15 +862,19 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
additional_permissions: Some(permission_profile),
})?
}
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
// Use a fully specified sandbox policy instead of merging into the turn policy.
EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
permissions,
)) => {
// Use a fully specified permission profile instead of merging into the turn policy.
let (file_system_sandbox_policy, network_sandbox_policy) =
permissions.permission_profile.to_runtime_permissions();
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
command,
workdir,
env,
sandbox_policy: &permissions.sandbox_policy,
file_system_sandbox_policy: &permissions.file_system_sandbox_policy,
network_sandbox_policy: permissions.network_sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
network_sandbox_policy,
additional_permissions: None,
})?
}
@@ -33,7 +33,7 @@ use codex_sandboxing::SandboxType;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::Permissions as EscalatedPermissions;
use codex_shell_escalation::ResolvedPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::Value;
@@ -317,11 +317,13 @@ fn shell_request_escalation_execution_is_explicit() {
network_sandbox_policy,
Some(&requested_permissions),
),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
ResolvedPermissionProfile {
permission_profile: PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
network_sandbox_policy,
),
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
},
)),
);
+1
View File
@@ -143,6 +143,7 @@ fn response_event_records_turn_ttft(event: &ResponseEvent) -> bool {
| ResponseEvent::ReasoningContentDelta { .. } => true,
ResponseEvent::Created
| ResponseEvent::ServerModel(_)
| ResponseEvent::ModelVerifications(_)
| ResponseEvent::ServerReasoningIncluded(_)
| ResponseEvent::ToolCallInputDelta { .. }
| ResponseEvent::Completed { .. }
+11
View File
@@ -632,6 +632,17 @@ pub fn ev_response_created(id: &str) -> Value {
})
}
pub fn ev_model_verification_metadata(id: &str, verifications: Vec<&str>) -> Value {
serde_json::json!({
"type": "response.metadata",
"sequence_number": 1,
"response_id": id,
"metadata": {
"openai_verification_recommendation": verifications,
}
})
}
pub fn ev_completed_with_tokens(id: &str, total_tokens: i64) -> Value {
serde_json::json!({
"type": "response.completed",
+1
View File
@@ -663,6 +663,7 @@ impl TestCodex {
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -367,6 +367,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1005,6 +1006,7 @@ async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -1103,6 +1105,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1188,6 +1191,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1344,6 +1348,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1412,6 +1417,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1489,6 +1495,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -1566,6 +1573,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
+2 -1
View File
@@ -593,8 +593,9 @@ async fn submit_turn(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
+2
View File
@@ -1660,6 +1660,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: session_configured.model.clone(),
effort: Some(ReasoningEffort::Low),
summary: Some(
@@ -1781,6 +1782,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: session_configured.model,
effort: None,
summary: Some(ReasoningSummary::Concise),
+1
View File
@@ -2619,6 +2619,7 @@ text(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -186,6 +186,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: Some(
@@ -307,6 +308,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: Some(
+7
View File
@@ -1687,6 +1687,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: resumed.session_configured.model.clone(),
effort: None,
summary: None,
@@ -1779,6 +1780,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
@@ -1805,6 +1807,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
@@ -1917,6 +1920,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
@@ -1967,6 +1971,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
@@ -3204,6 +3209,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
@@ -3230,6 +3236,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
+2
View File
@@ -54,6 +54,7 @@ async fn submit_user_turn(
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -136,6 +137,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -126,6 +126,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -214,6 +215,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
+5
View File
@@ -540,6 +540,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
@@ -618,6 +619,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
@@ -728,6 +730,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
@@ -916,6 +919,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
@@ -1031,6 +1035,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
+1
View File
@@ -83,6 +83,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
+1
View File
@@ -64,6 +64,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str)
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -131,6 +131,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -170,6 +171,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -231,6 +233,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -270,6 +273,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -413,6 +417,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -435,6 +440,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -544,6 +550,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -566,6 +573,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -678,6 +686,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -700,6 +709,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -814,6 +824,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -844,6 +855,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
@@ -1002,6 +1014,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: large_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -1063,6 +1076,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: smaller_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -124,6 +124,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -149,6 +150,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -229,6 +231,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -254,6 +257,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -367,6 +371,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: resumed.session_configured.model.clone(),
effort: resumed.config.model_reasoning_effort,
summary: None,
@@ -100,6 +100,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -111,6 +111,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
+30 -6
View File
@@ -1146,7 +1146,11 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("user_approved_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"user_approved_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -1215,7 +1219,11 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("user_approved_session_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"user_approved_session_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -1283,7 +1291,11 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("sandbox_retry_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"sandbox_retry_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -1351,7 +1363,11 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("user_denied_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"user_denied_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -1419,7 +1435,11 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("sandbox_session_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"sandbox_session_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -1487,7 +1507,11 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
mount_sse_once(
&server,
sse(vec![
ev_local_shell_call("sandbox_deny_call", "completed", vec!["/bin/date"]),
ev_local_shell_call(
"sandbox_deny_call",
"completed",
vec!["/usr/bin/touch", "codex-otel-approval-test"],
),
ev_completed("done"),
]),
)
@@ -120,6 +120,7 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -1,6 +1,7 @@
use anyhow::Result;
use codex_core::ForkSnapshot;
use codex_core::config::Constrained;
use codex_core::config_loader::ConfigLayerStack;
use codex_core::context::ContextualUserFragment;
use codex_core::context::PermissionsInstructions;
use codex_core::load_exec_policy;
@@ -551,6 +552,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let mut builder = test_codex().with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
config.config_layer_stack = ConfigLayerStack::default();
});
let test = builder.build(&server).await?;
+13
View File
@@ -106,6 +106,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -158,6 +159,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -217,6 +219,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -282,6 +285,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -335,6 +339,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -375,6 +380,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -443,6 +449,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -483,6 +490,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -564,6 +572,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -604,6 +613,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -724,6 +734,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -846,6 +857,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -886,6 +898,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -728,6 +728,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: new_policy.clone(),
permission_profile: None,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: Some(ReasoningSummary::Detailed),
@@ -842,6 +843,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
permission_profile: None,
model: default_model.clone(),
effort: default_effort,
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
@@ -864,6 +866,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
permission_profile: None,
model: default_model.clone(),
effort: default_effort,
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
@@ -970,6 +973,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
permission_profile: None,
model: default_model,
effort: default_effort,
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
@@ -992,6 +996,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: Some(ReasoningSummary::Detailed),
@@ -171,6 +171,7 @@ async fn remote_models_config_context_window_override_clamps_to_max_context_wind
summary: None,
service_tier: None,
collaboration_mode: None,
permission_profile: None,
personality: None,
environments: None,
})
@@ -248,6 +249,7 @@ async fn remote_models_config_override_above_max_uses_max_context_window() -> Re
summary: None,
service_tier: None,
collaboration_mode: None,
permission_profile: None,
personality: None,
environments: None,
})
@@ -324,6 +326,7 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re
summary: None,
service_tier: None,
collaboration_mode: None,
permission_profile: None,
personality: None,
environments: None,
})
@@ -408,6 +411,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: requested_model.to_string(),
effort: None,
summary: None,
@@ -468,6 +472,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning()
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
permission_profile: None,
model: requested_model.to_string(),
effort: None,
summary: Some(
@@ -636,6 +641,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REMOTE_MODEL_SLUG.to_string(),
effort: None,
summary: Some(ReasoningSummary::Auto),
@@ -863,6 +869,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: model.to_string(),
effort: None,
summary: Some(ReasoningSummary::Auto),
@@ -197,6 +197,7 @@ async fn submit_turn(
approval_policy,
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -150,6 +150,7 @@ async fn submit_turn(
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -141,6 +141,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -260,6 +261,7 @@ where
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -143,6 +143,7 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()>
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: None,
model: session_model,
effort: None,
summary: None,
+12
View File
@@ -290,6 +290,7 @@ async fn call_cwd_tool(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -424,6 +425,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -856,6 +858,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -989,6 +992,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1089,6 +1093,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1241,6 +1246,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1479,6 +1485,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_only_model_slug.to_string(),
effort: None,
summary: None,
@@ -1588,6 +1595,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1725,6 +1733,7 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1835,6 +1844,7 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1960,6 +1970,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -2177,6 +2188,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -2,13 +2,16 @@ use anyhow::Result;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_model_verification_metadata;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_response_once;
use core_test_support::responses::mount_response_sequence;
@@ -20,9 +23,14 @@ use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use wiremock::ResponseTemplate;
const SERVER_MODEL: &str = "gpt-5.2";
const REQUESTED_MODEL: &str = "gpt-5.3-codex";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
const CYBER_POLICY_MESSAGE: &str =
"This request has been flagged for potentially high-risk cyber activity.";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> Result<()> {
@@ -48,6 +56,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -113,6 +122,57 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cyber_policy_response_emits_typed_error_without_retry() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = ResponseTemplate::new(400).set_body_json(serde_json::json!({
"error": {
"message": CYBER_POLICY_MESSAGE,
"type": "invalid_request",
"param": null,
"code": "cyber_policy"
}
}));
let mock = mount_response_once(&server, response).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger cyber policy error".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await;
let EventMsg::Error(error) = error else {
panic!("expected error event");
};
assert_eq!(error.message, CYBER_POLICY_MESSAGE);
assert_eq!(error.codex_error_info, Some(CodexErrorInfo::CyberPolicy));
mock.single_request();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn response_model_field_mismatch_emits_warning_when_header_matches_requested() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -148,6 +208,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -235,6 +296,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -286,6 +348,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
@@ -318,3 +381,150 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_verification_emits_structured_event_without_reroute_or_warning() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = sse_response(sse(vec![
ev_response_created("resp-1"),
ev_model_verification_metadata("resp-1", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
core_test_support::responses::ev_completed("resp-1"),
]));
let _mock = mount_response_once(&server, response).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let mut verification_count = 0;
let mut reroute_count = 0;
let mut warning_count = 0;
let mut warning_item_count = 0;
loop {
let event = wait_for_event(&test.codex, |_| true).await;
match event {
EventMsg::ModelVerification(event) => {
assert_eq!(
event.verifications,
vec![ModelVerification::TrustedAccessForCyber]
);
verification_count += 1;
}
EventMsg::Warning(_) => warning_count += 1,
EventMsg::ModelReroute(_) => reroute_count += 1,
EventMsg::RawResponseItem(raw)
if matches!(
&raw.item,
ResponseItem::Message { content, .. }
if content.iter().any(|item| matches!(
item,
ContentItem::InputText { text } if text.starts_with("Warning: ")
))
) =>
{
warning_item_count += 1;
}
EventMsg::TurnComplete(_) => break,
_ => {}
}
}
assert_eq!(verification_count, 1);
assert_eq!(reroute_count, 0);
assert_eq!(warning_count, 0);
assert_eq!(warning_item_count, 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_verification_only_emits_once_per_turn() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let tool_args = serde_json::json!({
"command": "echo hello",
"timeout_ms": 1_000
});
let first_response = sse_response(sse(vec![
ev_response_created("resp-1"),
ev_function_call(
"call-1",
"shell_command",
&serde_json::to_string(&tool_args)?,
),
ev_model_verification_metadata("resp-1", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
core_test_support::responses::ev_completed("resp-1"),
]));
let second_response = sse_response(sse(vec![
ev_response_created("resp-2"),
ev_model_verification_metadata("resp-2", vec![TRUSTED_ACCESS_FOR_CYBER_VERIFICATION]),
ev_assistant_message("msg-1", "done"),
core_test_support::responses::ev_completed("resp-2"),
]));
let _mock = mount_response_sequence(&server, vec![first_response, second_response]).await;
let mut builder = test_codex().with_model(REQUESTED_MODEL);
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger follow-up model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
let mut verification_count = 0;
loop {
let event = wait_for_event(&test.codex, |_| true).await;
match event {
EventMsg::ModelVerification(_) => verification_count += 1,
EventMsg::Warning(warning) if warning.message.contains("high-risk cyber activity") => {
panic!("model verification should not emit a warning event");
}
EventMsg::TurnComplete(_) => break,
_ => {}
}
}
assert_eq!(verification_count, 1);
Ok(())
}
@@ -167,6 +167,7 @@ async fn run_snapshot_command_with_options(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -259,6 +260,7 @@ async fn run_shell_command_snapshot_with_options(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -331,6 +333,7 @@ async fn run_tool_turn_on_harness(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -567,6 +570,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
@@ -54,6 +54,7 @@ async fn submit_turn_with_policies(
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
+1
View File
@@ -115,6 +115,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -409,6 +409,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -88,6 +88,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -160,6 +161,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -242,6 +244,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -339,6 +342,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -444,6 +448,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -45,6 +45,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -363,6 +364,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
+1
View File
@@ -510,6 +510,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -176,6 +176,7 @@ async fn submit_unified_exec_turn(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -261,6 +262,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1752,6 +1754,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1846,6 +1849,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -2320,6 +2324,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
approvals_reviewer: None,
// Important!
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -2429,6 +2434,7 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: read_only_policy,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -2558,6 +2564,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -180,6 +180,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: fixture.session_configured.model.clone(),
effort: None,
summary: None,
+14
View File
@@ -169,6 +169,7 @@ async fn assert_user_turn_local_image_resizes_to(
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -290,6 +291,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -422,6 +424,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -522,6 +525,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -613,6 +617,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -714,6 +719,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -819,6 +825,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -922,6 +929,7 @@ await codex.emitImage(out);
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1043,6 +1051,7 @@ console.log(out.type);
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
@@ -1137,6 +1146,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1219,6 +1229,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1306,6 +1317,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -1444,6 +1456,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: model_slug.to_string(),
effort: None,
summary: None,
@@ -1525,6 +1538,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
@@ -160,6 +160,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
@@ -292,6 +292,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
);
CodexStatus::Running
}
ServerNotification::ModelVerification(_) => CodexStatus::Running,
ServerNotification::ThreadTokenUsageUpdated(notification) => {
self.last_total_token_usage = Some(notification.token_usage);
CodexStatus::Running
@@ -489,6 +489,7 @@ impl EventProcessorWithJsonOutput {
}));
CodexStatus::Running
}
ServerNotification::ModelVerification(_) => CodexStatus::Running,
ServerNotification::ThreadTokenUsageUpdated(notification) => {
self.last_total_token_usage = Some(notification.token_usage);
CodexStatus::Running
+3
View File
@@ -1138,6 +1138,9 @@ fn should_process_notification(
ServerNotification::ModelRerouted(notification) => {
notification.thread_id == thread_id && notification.turn_id == turn_id
}
ServerNotification::ModelVerification(notification) => {
notification.thread_id == thread_id && notification.turn_id == turn_id
}
ServerNotification::ThreadTokenUsageUpdated(notification) => {
notification.thread_id == thread_id && notification.turn_id == turn_id
}
+4 -4
View File
@@ -75,6 +75,8 @@ pub enum Feature {
GhostCommit,
/// Enable the default shell tool.
ShellTool,
/// Enable Claude-style lifecycle hooks loaded from hooks.json files.
CodexHooks,
// Experimental
/// Enable JavaScript REPL tools backed by a persistent Node kernel.
@@ -95,8 +97,6 @@ pub enum Feature {
ApplyPatchStreamingEvents,
/// Allow exec tools to request additional permissions while staying sandboxed.
ExecPermissionApprovals,
/// Enable Claude-style lifecycle hooks loaded from hooks.json files.
CodexHooks,
/// Expose the built-in request_permissions tool.
RequestPermissionsTool,
/// Allow the model to request web searches that fetch live content.
@@ -751,8 +751,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::CodexHooks,
key: "codex_hooks",
stage: Stage::UnderDevelopment,
default_enabled: false,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::RequestPermissionsTool,
+3 -1
View File
@@ -263,7 +263,9 @@ async fn run_codex_tool_session_inner(
outgoing.send_response(request_id.clone(), result).await;
break;
}
EventMsg::Warning(_) | EventMsg::GuardianWarning(_) => {
EventMsg::Warning(_)
| EventMsg::GuardianWarning(_)
| EventMsg::ModelVerification(_) => {
continue;
}
EventMsg::GuardianAssessment(_) => {

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