mirror of
https://github.com/openai/codex.git
synced 2026-04-29 08:56:38 +00:00
app-server: propagate nested experimental gating for AskForApproval::Reject (#14191)
## Summary
This change makes `AskForApproval::Reject` gate correctly anywhere it
appears inside otherwise-stable app-server protocol types.
Previously, experimental gating for `approval_policy: Reject` was
handled with request-specific logic in `ClientRequest` detection. That
covered a few request params types, but it did not generalize to other
nested uses such as `ProfileV2`, `Config`, `ConfigReadResponse`, or
`ConfigRequirements`.
This PR replaces that ad hoc handling with a generic nested experimental
propagation mechanism.
## Testing
seeing this when run app-server-test-client without experimental api
enabled:
```
initialize response: InitializeResponse { user_agent: "codex-toy-app-server/0.0.0 (Mac OS 26.3.1; arm64) vscode/2.4.36 (codex-toy-app-server; 0.0.0)" }
> {
> "id": "50244f6a-270a-425d-ace0-e9e98205bde7",
> "method": "thread/start",
> "params": {
> "approvalPolicy": {
> "reject": {
> "mcp_elicitations": false,
> "request_permissions": true,
> "rules": false,
> "sandbox_approval": true
> }
> },
> "baseInstructions": null,
> "config": null,
> "cwd": null,
> "developerInstructions": null,
> "dynamicTools": null,
> "ephemeral": null,
> "experimentalRawEvents": false,
> "mockExperimentalField": null,
> "model": null,
> "modelProvider": null,
> "persistExtendedHistory": false,
> "personality": null,
> "sandbox": null,
> "serviceName": null
> }
> }
< {
< "error": {
< "code": -32600,
< "message": "askForApproval.reject requires experimentalApi capability"
< },
< "id": "50244f6a-270a-425d-ace0-e9e98205bde7"
< }
[verified] thread/start rejected approvalPolicy=Reject without experimentalApi
```
---------
Co-authored-by: celia-oai <celia@openai.com>
This commit is contained in:
committed by
Michael Bolin
parent
722e8f08e1
commit
d5694529ca
@@ -189,7 +189,9 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, ExperimentalApi,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(rename_all = "kebab-case", export_to = "v2/")]
|
||||
pub enum AskForApproval {
|
||||
@@ -198,6 +200,7 @@ pub enum AskForApproval {
|
||||
UnlessTrusted,
|
||||
OnFailure,
|
||||
OnRequest,
|
||||
#[experimental("askForApproval.reject")]
|
||||
Reject {
|
||||
sandbox_approval: bool,
|
||||
rules: bool,
|
||||
@@ -502,12 +505,13 @@ pub struct DynamicToolSpec {
|
||||
pub input_schema: JsonValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ProfileV2 {
|
||||
pub model: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -606,6 +610,7 @@ pub struct Config {
|
||||
pub model_context_window: Option<i64>,
|
||||
pub model_auto_compact_token_limit: Option<i64>,
|
||||
pub model_provider: Option<String>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
|
||||
@@ -614,6 +619,7 @@ pub struct Config {
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub tools: Option<ToolsV2>,
|
||||
pub profile: Option<String>,
|
||||
#[experimental(nested)]
|
||||
#[serde(default)]
|
||||
pub profiles: HashMap<String, ProfileV2>,
|
||||
pub instructions: Option<String>,
|
||||
@@ -711,10 +717,11 @@ pub struct ConfigReadParams {
|
||||
pub cwd: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigReadResponse {
|
||||
#[experimental(nested)]
|
||||
pub config: Config,
|
||||
pub origins: HashMap<String, ConfigLayerMetadata>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -725,6 +732,7 @@ pub struct ConfigReadResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigRequirements {
|
||||
#[experimental(nested)]
|
||||
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
@@ -757,11 +765,12 @@ pub enum ResidencyRequirement {
|
||||
Us,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ConfigRequirementsReadResponse {
|
||||
/// Null if no requirements are configured (e.g. no requirements.toml/MDM entries).
|
||||
#[experimental(nested)]
|
||||
pub requirements: Option<ConfigRequirements>,
|
||||
}
|
||||
|
||||
@@ -2229,6 +2238,7 @@ pub struct ThreadStartParams {
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2282,7 +2292,7 @@ pub struct MockExperimentalMethodResponse {
|
||||
pub echoed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadStartResponse {
|
||||
@@ -2291,6 +2301,7 @@ pub struct ThreadStartResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -2341,6 +2352,7 @@ pub struct ThreadResumeParams {
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2360,7 +2372,7 @@ pub struct ThreadResumeParams {
|
||||
pub persist_extended_history: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadResumeResponse {
|
||||
@@ -2369,6 +2381,7 @@ pub struct ThreadResumeResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -2410,6 +2423,7 @@ pub struct ThreadForkParams {
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2427,7 +2441,7 @@ pub struct ThreadForkParams {
|
||||
pub persist_extended_history: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadForkResponse {
|
||||
@@ -2436,6 +2450,7 @@ pub struct ThreadForkResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
@@ -3490,6 +3505,7 @@ pub struct TurnStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Override the approval policy for this turn and subsequent turns.
|
||||
#[experimental(nested)]
|
||||
#[ts(optional = nullable)]
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
/// Override the sandbox policy for this turn and subsequent turns.
|
||||
@@ -6046,6 +6062,243 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_for_approval_reject_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
assert_eq!(
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&AskForApproval::OnRequest,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ProfileV2 {
|
||||
model: None,
|
||||
model_provider: None,
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
service_tier: None,
|
||||
model_reasoning_effort: None,
|
||||
model_reasoning_summary: None,
|
||||
model_verbosity: None,
|
||||
web_search: None,
|
||||
tools: None,
|
||||
chatgpt_base_url: None,
|
||||
additional: HashMap::new(),
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config {
|
||||
model: None,
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider: None,
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: false,
|
||||
rules: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
sandbox_mode: None,
|
||||
sandbox_workspace_write: None,
|
||||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
web_search: None,
|
||||
tools: None,
|
||||
profile: None,
|
||||
profiles: HashMap::new(),
|
||||
instructions: None,
|
||||
developer_instructions: None,
|
||||
compact_prompt: None,
|
||||
model_reasoning_effort: None,
|
||||
model_reasoning_summary: None,
|
||||
model_verbosity: None,
|
||||
service_tier: None,
|
||||
analytics: None,
|
||||
apps: None,
|
||||
additional: HashMap::new(),
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_nested_profile_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config {
|
||||
model: None,
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider: None,
|
||||
approval_policy: None,
|
||||
sandbox_mode: None,
|
||||
sandbox_workspace_write: None,
|
||||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
web_search: None,
|
||||
tools: None,
|
||||
profile: None,
|
||||
profiles: HashMap::from([(
|
||||
"default".to_string(),
|
||||
ProfileV2 {
|
||||
model: None,
|
||||
model_provider: None,
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
service_tier: None,
|
||||
model_reasoning_effort: None,
|
||||
model_reasoning_summary: None,
|
||||
model_verbosity: None,
|
||||
web_search: None,
|
||||
tools: None,
|
||||
chatgpt_base_url: None,
|
||||
additional: HashMap::new(),
|
||||
},
|
||||
)]),
|
||||
instructions: None,
|
||||
developer_instructions: None,
|
||||
compact_prompt: None,
|
||||
model_reasoning_effort: None,
|
||||
model_reasoning_summary: None,
|
||||
model_verbosity: None,
|
||||
service_tier: None,
|
||||
analytics: None,
|
||||
apps: None,
|
||||
additional: HashMap::new(),
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_requirements_reject_allowed_approval_policy_is_marked_experimental() {
|
||||
let reason =
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: false,
|
||||
}]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
enforce_residency: None,
|
||||
network: None,
|
||||
});
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_thread_start_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&crate::ClientRequest::ThreadStart {
|
||||
request_id: crate::RequestId::Integer(1),
|
||||
params: ThreadStartParams {
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_thread_resume_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&crate::ClientRequest::ThreadResume {
|
||||
request_id: crate::RequestId::Integer(2),
|
||||
params: ThreadResumeParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: false,
|
||||
rules: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_thread_fork_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&crate::ClientRequest::ThreadFork {
|
||||
request_id: crate::RequestId::Integer(3),
|
||||
params: ThreadForkParams {
|
||||
thread_id: "thr_456".to_string(),
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: true,
|
||||
rules: false,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_turn_start_reject_approval_policy_is_marked_experimental() {
|
||||
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(
|
||||
&crate::ClientRequest::TurnStart {
|
||||
request_id: crate::RequestId::Integer(4),
|
||||
params: TurnStartParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
input: Vec::new(),
|
||||
approval_policy: Some(AskForApproval::Reject {
|
||||
sandbox_approval: false,
|
||||
rules: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(reason, Some("askForApproval.reject"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_elicitation_response_round_trips_rmcp_result() {
|
||||
let rmcp_result = rmcp::model::CreateElicitationResult {
|
||||
|
||||
Reference in New Issue
Block a user