mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add PermissionRequest hooks support (#17563)
## Why We need `PermissionRequest` hook support! Also addresses: - https://github.com/openai/codex/issues/16301 - run a script on Hook to do things like play a sound to draw attention but actually no-op so user can still approve - can omit the `decision` object from output or just have the script exit 0 and print nothing - https://github.com/openai/codex/issues/15311 - let the script approve/deny on its own - external UI what will run on Hook and relay decision back to codex ## Reviewer Note There's a lot of plumbing for the new hook, key files to review are: - New hook added in `codex-rs/hooks/src/events/permission_request.rs` - Wiring for network approvals `codex-rs/core/src/tools/network_approval.rs` - Wiring for tool orchestrator `codex-rs/core/src/tools/orchestrator.rs` - Wiring for execve `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs` ## What - Wires shell, unified exec, and network approval prompts into the `PermissionRequest` hook flow. - Lets hooks allow or deny approval prompts; quiet or invalid hooks fall back to the normal approval path. - Uses `tool_input.description` for user-facing context when it helps: - shell / `exec_command`: the request justification, when present - network approvals: `network-access <domain>` - Uses `tool_name: Bash` for shell, unified exec, and network approval permission-request hooks. - For network approvals, passes the originating command in `tool_input.command` when there is a single owning call; otherwise falls back to the synthetic `network-access ...` command. <details> <summary>Example `PermissionRequest` hook input for a shell approval</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "rm -f /tmp/example" } } ``` </details> <details> <summary>Example `PermissionRequest` hook input for an escalated `exec_command` request</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "cp /tmp/source.json /Users/alice/export/source.json", "description": "Need to copy a generated file outside the workspace" } } ``` </details> <details> <summary>Example `PermissionRequest` hook input for a network approval</summary> ```json { "session_id": "<session-id>", "turn_id": "<turn-id>", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/path/to/cwd", "hook_event_name": "PermissionRequest", "model": "gpt-5", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "curl http://codex-network-test.invalid", "description": "network-access http://codex-network-test.invalid" } } ``` </details> ## Follow-ups - Implement the `PermissionRequest` semantics for `updatedInput`, `updatedPermissions`, `interrupt`, and suggestions / `permission_suggestions` - Add `PermissionRequest` support for the `request_permissions` tool path --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
d0047de7cb
commit
8494e5bd7b
@@ -15,6 +15,8 @@ use std::path::PathBuf;
|
||||
const GENERATED_DIR: &str = "generated";
|
||||
const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.json";
|
||||
const POST_TOOL_USE_OUTPUT_FIXTURE: &str = "post-tool-use.command.output.schema.json";
|
||||
const PERMISSION_REQUEST_INPUT_FIXTURE: &str = "permission-request.command.input.schema.json";
|
||||
const PERMISSION_REQUEST_OUTPUT_FIXTURE: &str = "permission-request.command.output.schema.json";
|
||||
const PRE_TOOL_USE_INPUT_FIXTURE: &str = "pre-tool-use.command.input.schema.json";
|
||||
const PRE_TOOL_USE_OUTPUT_FIXTURE: &str = "pre-tool-use.command.output.schema.json";
|
||||
const SESSION_START_INPUT_FIXTURE: &str = "session-start.command.input.schema.json";
|
||||
@@ -69,6 +71,8 @@ pub(crate) struct HookUniversalOutputWire {
|
||||
pub(crate) enum HookEventNameWire {
|
||||
#[serde(rename = "PreToolUse")]
|
||||
PreToolUse,
|
||||
#[serde(rename = "PermissionRequest")]
|
||||
PermissionRequest,
|
||||
#[serde(rename = "PostToolUse")]
|
||||
PostToolUse,
|
||||
#[serde(rename = "SessionStart")]
|
||||
@@ -109,6 +113,58 @@ pub(crate) struct PostToolUseCommandOutputWire {
|
||||
pub hook_specific_output: Option<PostToolUseHookSpecificOutputWire>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "permission-request.command.output")]
|
||||
pub(crate) struct PermissionRequestCommandOutputWire {
|
||||
#[serde(flatten)]
|
||||
pub universal: HookUniversalOutputWire,
|
||||
#[serde(default)]
|
||||
pub hook_specific_output: Option<PermissionRequestHookSpecificOutputWire>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PermissionRequestHookSpecificOutputWire {
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub decision: Option<PermissionRequestDecisionWire>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PermissionRequestDecisionWire {
|
||||
pub behavior: PermissionRequestBehaviorWire,
|
||||
/// Reserved for a future input-rewrite capability.
|
||||
///
|
||||
/// PermissionRequest hooks currently fail closed if this field is present.
|
||||
#[serde(default)]
|
||||
pub updated_input: Option<Value>,
|
||||
/// Reserved for a future permission-rewrite capability.
|
||||
///
|
||||
/// PermissionRequest hooks currently fail closed if this field is present.
|
||||
#[serde(default)]
|
||||
pub updated_permissions: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
/// Reserved for future short-circuiting semantics.
|
||||
///
|
||||
/// PermissionRequest hooks currently fail closed if this field is `true`.
|
||||
#[serde(default)]
|
||||
pub interrupt: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub(crate) enum PermissionRequestBehaviorWire {
|
||||
#[serde(rename = "allow")]
|
||||
Allow,
|
||||
#[serde(rename = "deny")]
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -181,6 +237,34 @@ pub(crate) struct PreToolUseCommandInput {
|
||||
pub tool_use_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PermissionRequestToolInput {
|
||||
pub command: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(rename = "permission-request.command.input")]
|
||||
pub(crate) struct PermissionRequestCommandInput {
|
||||
pub session_id: String,
|
||||
/// Codex extension: expose the active turn id to internal turn-scoped hooks.
|
||||
pub turn_id: String,
|
||||
pub transcript_path: NullableString,
|
||||
pub cwd: String,
|
||||
#[schemars(schema_with = "permission_request_hook_event_name_schema")]
|
||||
pub hook_event_name: String,
|
||||
pub model: String,
|
||||
#[schemars(schema_with = "permission_mode_schema")]
|
||||
pub permission_mode: String,
|
||||
#[schemars(schema_with = "permission_request_tool_name_schema")]
|
||||
pub tool_name: String,
|
||||
pub tool_input: PermissionRequestToolInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -358,6 +442,14 @@ pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> {
|
||||
&generated_dir.join(POST_TOOL_USE_OUTPUT_FIXTURE),
|
||||
schema_json::<PostToolUseCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PERMISSION_REQUEST_INPUT_FIXTURE),
|
||||
schema_json::<PermissionRequestCommandInput>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PERMISSION_REQUEST_OUTPUT_FIXTURE),
|
||||
schema_json::<PermissionRequestCommandOutputWire>()?,
|
||||
)?;
|
||||
write_schema(
|
||||
&generated_dir.join(PRE_TOOL_USE_INPUT_FIXTURE),
|
||||
schema_json::<PreToolUseCommandInput>()?,
|
||||
@@ -461,10 +553,18 @@ fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PreToolUse")
|
||||
}
|
||||
|
||||
fn permission_request_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("PermissionRequest")
|
||||
}
|
||||
|
||||
fn pre_tool_use_tool_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Bash")
|
||||
}
|
||||
|
||||
fn permission_request_tool_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("Bash")
|
||||
}
|
||||
|
||||
fn user_prompt_submit_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
string_const_schema("UserPromptSubmit")
|
||||
}
|
||||
@@ -516,10 +616,13 @@ fn default_continue() -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PERMISSION_REQUEST_INPUT_FIXTURE;
|
||||
use super::PERMISSION_REQUEST_OUTPUT_FIXTURE;
|
||||
use super::POST_TOOL_USE_INPUT_FIXTURE;
|
||||
use super::POST_TOOL_USE_OUTPUT_FIXTURE;
|
||||
use super::PRE_TOOL_USE_INPUT_FIXTURE;
|
||||
use super::PRE_TOOL_USE_OUTPUT_FIXTURE;
|
||||
use super::PermissionRequestCommandInput;
|
||||
use super::PostToolUseCommandInput;
|
||||
use super::PreToolUseCommandInput;
|
||||
use super::SESSION_START_INPUT_FIXTURE;
|
||||
@@ -544,6 +647,12 @@ mod tests {
|
||||
POST_TOOL_USE_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/post-tool-use.command.output.schema.json")
|
||||
}
|
||||
PERMISSION_REQUEST_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/permission-request.command.input.schema.json")
|
||||
}
|
||||
PERMISSION_REQUEST_OUTPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/permission-request.command.output.schema.json")
|
||||
}
|
||||
PRE_TOOL_USE_INPUT_FIXTURE => {
|
||||
include_str!("../schema/generated/pre-tool-use.command.input.schema.json")
|
||||
}
|
||||
@@ -585,6 +694,8 @@ mod tests {
|
||||
for fixture in [
|
||||
POST_TOOL_USE_INPUT_FIXTURE,
|
||||
POST_TOOL_USE_OUTPUT_FIXTURE,
|
||||
PERMISSION_REQUEST_INPUT_FIXTURE,
|
||||
PERMISSION_REQUEST_OUTPUT_FIXTURE,
|
||||
PRE_TOOL_USE_INPUT_FIXTURE,
|
||||
PRE_TOOL_USE_OUTPUT_FIXTURE,
|
||||
SESSION_START_INPUT_FIXTURE,
|
||||
@@ -615,6 +726,11 @@ mod tests {
|
||||
.expect("serialize post tool use input schema"),
|
||||
)
|
||||
.expect("parse post tool use input schema");
|
||||
let permission_request: Value = serde_json::from_slice(
|
||||
&schema_json::<PermissionRequestCommandInput>()
|
||||
.expect("serialize permission request input schema"),
|
||||
)
|
||||
.expect("parse permission request input schema");
|
||||
let user_prompt_submit: Value = serde_json::from_slice(
|
||||
&schema_json::<UserPromptSubmitCommandInput>()
|
||||
.expect("serialize user prompt submit input schema"),
|
||||
@@ -625,7 +741,13 @@ mod tests {
|
||||
)
|
||||
.expect("parse stop input schema");
|
||||
|
||||
for schema in [&pre_tool_use, &post_tool_use, &user_prompt_submit, &stop] {
|
||||
for schema in [
|
||||
&pre_tool_use,
|
||||
&permission_request,
|
||||
&post_tool_use,
|
||||
&user_prompt_submit,
|
||||
&stop,
|
||||
] {
|
||||
assert_eq!(schema["properties"]["turn_id"]["type"], "string");
|
||||
assert!(
|
||||
schema["required"]
|
||||
|
||||
Reference in New Issue
Block a user