diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 46f0c6412f..b59ef033d7 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -448,7 +448,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul expected_hook_command ); assert_eq!( - hook_inputs[0]["approval_context"]["approval_attempt"], + hook_inputs[0]["approval_context"]["attempt"]["stage"], "initial" ); diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 03832c5444..08c6c79167 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -1174,12 +1174,16 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() -> assert_eq!( hook_inputs[0]["approval_context"], serde_json::json!({ - "sandbox_permissions": "use_default", - "additional_permissions": null, + "attempt": { + "stage": "initial", + "retryReason": null, + }, + "policy": { + "sandboxPermissions": "use_default", + "additionalPermissions": null, + }, "justification": null, - "approval_attempt": "initial", - "retry_reason": null, - "network_approval_context": null, + "resource": {}, }) ); assert!( @@ -1277,12 +1281,16 @@ async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> { assert_eq!( hook_inputs[0]["approval_context"], serde_json::json!({ - "sandbox_permissions": "use_default", - "additional_permissions": null, + "attempt": { + "stage": "initial", + "retryReason": null, + }, + "policy": { + "sandboxPermissions": "use_default", + "additionalPermissions": null, + }, "justification": null, - "approval_attempt": "initial", - "retry_reason": null, - "network_approval_context": null, + "resource": {}, }) ); @@ -1355,8 +1363,7 @@ allow_local_binding = true .enable(Feature::CodexHooks) .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = - Constrained::allow_any(sandbox_policy_for_config.clone()); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); let layers = config .config_layer_stack .get_layers( @@ -1444,10 +1451,21 @@ allow_local_binding = true "network-access http://codex-network-test.invalid:80" ); assert_eq!( - hook_inputs[0]["approval_context"]["network_approval_context"], + hook_inputs[0]["approval_context"], serde_json::json!({ - "host": "codex-network-test.invalid", - "protocol": "http", + "attempt": { + "stage": "initial", + "retryReason": null, + }, + "policy": { + "sandboxPermissions": "use_default", + "additionalPermissions": null, + }, + "justification": "codex-network-test.invalid is not in the allowed_domains", + "resource": { + "host": "codex-network-test.invalid", + "protocol": "http", + }, }) ); @@ -1534,12 +1552,16 @@ async fn permission_request_hook_sees_retry_context_after_sandbox_denial() -> Re assert_eq!( hook_inputs[0]["approval_context"], serde_json::json!({ - "sandbox_permissions": "use_default", - "additional_permissions": null, + "attempt": { + "stage": "retry", + "retryReason": "command failed; retry without sandbox?", + }, + "policy": { + "sandboxPermissions": "use_default", + "additionalPermissions": null, + }, "justification": null, - "approval_attempt": "retry", - "retry_reason": "command failed; retry without sandbox?", - "network_approval_context": null, + "resource": {}, }) ); diff --git a/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json b/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json index 7d5e78cdaf..9b47807a5f 100644 --- a/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json +++ b/codex-rs/hooks/schema/generated/permission-request.command.input.schema.json @@ -23,21 +23,6 @@ }, "type": "object" }, - "NetworkApprovalContext": { - "properties": { - "host": { - "type": "string" - }, - "protocol": { - "$ref": "#/definitions/NetworkApprovalProtocol" - } - }, - "required": [ - "host", - "protocol" - ], - "type": "object" - }, "NetworkApprovalProtocol": { "enum": [ "http", @@ -82,31 +67,68 @@ "PermissionRequestApprovalContext": { "additionalProperties": false, "properties": { - "additional_permissions": { - "$ref": "#/definitions/PermissionProfile" - }, - "approval_attempt": { - "$ref": "#/definitions/PermissionRequestApprovalAttempt" + "attempt": { + "$ref": "#/definitions/PermissionRequestAttemptContext" }, "justification": { "type": "string" }, - "network_approval_context": { - "$ref": "#/definitions/NetworkApprovalContext" + "policy": { + "$ref": "#/definitions/PermissionRequestPolicyContext" }, - "retry_reason": { + "resource": { + "$ref": "#/definitions/PermissionRequestResourceContext" + } + }, + "required": [ + "attempt", + "policy", + "resource" + ], + "type": "object" + }, + "PermissionRequestAttemptContext": { + "additionalProperties": false, + "properties": { + "retryReason": { "type": "string" }, - "sandbox_permissions": { + "stage": { + "$ref": "#/definitions/PermissionRequestApprovalAttempt" + } + }, + "required": [ + "stage" + ], + "type": "object" + }, + "PermissionRequestPolicyContext": { + "additionalProperties": false, + "properties": { + "additionalPermissions": { + "$ref": "#/definitions/PermissionProfile" + }, + "sandboxPermissions": { "$ref": "#/definitions/SandboxPermissions" } }, "required": [ - "approval_attempt", - "sandbox_permissions" + "sandboxPermissions" ], "type": "object" }, + "PermissionRequestResourceContext": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/NetworkApprovalProtocol" + } + }, + "type": "object" + }, "PermissionRequestToolInput": { "additionalProperties": false, "properties": { diff --git a/codex-rs/hooks/src/events/permission_request.rs b/codex-rs/hooks/src/events/permission_request.rs index cc4ebbf297..5ef2fda2e2 100644 --- a/codex-rs/hooks/src/events/permission_request.rs +++ b/codex-rs/hooks/src/events/permission_request.rs @@ -36,7 +36,10 @@ use crate::engine::command_runner::CommandRunResult; use crate::engine::dispatcher; use crate::engine::output_parser; use crate::schema::PermissionRequestApprovalContext; +use crate::schema::PermissionRequestAttemptContext; use crate::schema::PermissionRequestCommandInput; +use crate::schema::PermissionRequestPolicyContext; +use crate::schema::PermissionRequestResourceContext; use crate::schema::PermissionRequestToolInput; #[derive(Debug, Clone)] @@ -195,6 +198,11 @@ fn resolve_permission_request_decision<'a>( } fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestCommandInput { + let (host, protocol) = request + .network_approval_context + .as_ref() + .map(|context| (Some(context.host.clone()), Some(context.protocol))) + .unwrap_or((None, None)); PermissionRequestCommandInput { session_id: request.session_id.to_string(), turn_id: request.turn_id.clone(), @@ -208,12 +216,16 @@ fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestC command: request.command.clone(), }, approval_context: PermissionRequestApprovalContext { - sandbox_permissions: request.sandbox_permissions, - additional_permissions: request.additional_permissions.clone(), + attempt: PermissionRequestAttemptContext { + stage: request.approval_attempt, + retry_reason: request.retry_reason.clone(), + }, + policy: PermissionRequestPolicyContext { + sandbox_permissions: request.sandbox_permissions, + additional_permissions: request.additional_permissions.clone(), + }, justification: request.justification.clone(), - approval_attempt: request.approval_attempt, - retry_reason: request.retry_reason.clone(), - network_approval_context: request.network_approval_context.clone(), + resource: PermissionRequestResourceContext { host, protocol }, }, } } diff --git a/codex-rs/hooks/src/schema.rs b/codex-rs/hooks/src/schema.rs index b148a50e21..c3d663285f 100644 --- a/codex-rs/hooks/src/schema.rs +++ b/codex-rs/hooks/src/schema.rs @@ -12,7 +12,7 @@ use serde_json::Value; use std::path::Path; use std::path::PathBuf; -use codex_protocol::approvals::NetworkApprovalContext; +use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxPermissions; @@ -251,14 +251,38 @@ pub(crate) struct PermissionRequestToolInput { } #[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] -pub(crate) struct PermissionRequestApprovalContext { +pub(crate) struct PermissionRequestAttemptContext { + pub stage: PermissionRequestApprovalAttempt, + pub retry_reason: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub(crate) struct PermissionRequestPolicyContext { pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub(crate) struct PermissionRequestResourceContext { + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol: Option, +} + +#[derive(Debug, Clone, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct PermissionRequestApprovalContext { + pub attempt: PermissionRequestAttemptContext, + pub policy: PermissionRequestPolicyContext, pub justification: Option, - pub approval_attempt: PermissionRequestApprovalAttempt, - pub retry_reason: Option, - pub network_approval_context: Option, + pub resource: PermissionRequestResourceContext, } #[derive(Debug, Clone, Serialize, JsonSchema)]