Refactor permission request updates

This commit is contained in:
Abhinav Vedmala
2026-04-14 11:11:28 -07:00
parent 2219e3df2e
commit f7d4874a0e
12 changed files with 276 additions and 303 deletions

View File

@@ -2,14 +2,12 @@ use std::future::Future;
use std::path::Path;
use std::sync::Arc;
use codex_hooks::PermissionDirectoryUpdate;
use codex_hooks::PermissionRequestDecision;
use codex_hooks::PermissionRequestOutcome;
use codex_hooks::PermissionRequestRequest;
use codex_hooks::PermissionSuggestion;
use codex_hooks::PermissionSuggestionDestination;
use codex_hooks::PermissionSuggestionRule;
use codex_hooks::PermissionSuggestionType;
use codex_hooks::PermissionUpdate;
use codex_hooks::PermissionUpdateDestination;
use codex_hooks::PermissionUpdateRule;
use codex_hooks::PostToolUseOutcome;
use codex_hooks::PostToolUseRequest;
use codex_hooks::PreToolUseOutcome;
@@ -199,16 +197,9 @@ pub(crate) async fn run_permission_request_hooks(
if let Some(PermissionRequestDecision::Allow {
updated_permissions,
add_directories,
}) = &decision
{
apply_permission_updates_from_hook(
sess,
turn_context,
updated_permissions,
add_directories,
)
.await;
apply_permission_updates_from_hook(sess, turn_context, updated_permissions).await;
}
decision
@@ -217,28 +208,15 @@ pub(crate) async fn run_permission_request_hooks(
async fn apply_permission_updates_from_hook(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
updated_permissions: &[PermissionSuggestion],
add_directories: &[PermissionDirectoryUpdate],
updated_permissions: &[PermissionUpdate],
) {
for permission in updated_permissions {
if let Err(err) = apply_permission_update_from_hook(sess, turn_context, permission).await {
let message =
format!("PermissionRequest hook failed to apply updated permission: {err}");
warn!("{message}");
sess.send_event_raw(Event {
id: turn_context.sub_id.clone(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
}
for directory_update in add_directories {
if let Err(err) =
apply_directory_update_from_hook(sess, turn_context, directory_update).await
{
let message =
format!("PermissionRequest hook failed to apply updated writable roots: {err}");
let update_kind = match permission {
PermissionUpdate::AddRules { .. } => "updated permission",
PermissionUpdate::AddDirectories { .. } => "updated writable roots",
};
let message = format!("PermissionRequest hook failed to apply {update_kind}: {err}");
warn!("{message}");
sess.send_event_raw(Event {
id: turn_context.sub_id.clone(),
@@ -252,18 +230,20 @@ async fn apply_permission_updates_from_hook(
async fn apply_permission_update_from_hook(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
permission: &PermissionSuggestion,
permission: &PermissionUpdate,
) -> Result<(), String> {
match permission.suggestion_type {
PermissionSuggestionType::AddRules => {
for rule in &permission.rules {
match permission {
PermissionUpdate::AddRules {
rules, destination, ..
} => {
for rule in rules {
match rule {
PermissionSuggestionRule::PrefixRule { command } => {
PermissionUpdateRule::PrefixRule { command } => {
let amendment = ExecPolicyAmendment::new(command.clone());
apply_execpolicy_amendment_destination(
sess,
turn_context,
&permission.destination,
destination,
&amendment,
)
.await?;
@@ -272,23 +252,26 @@ async fn apply_permission_update_from_hook(
}
Ok(())
}
PermissionUpdate::AddDirectories { .. } => {
apply_directory_update_from_hook(sess, turn_context, permission).await
}
}
}
async fn apply_execpolicy_amendment_destination(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
destination: &PermissionSuggestionDestination,
destination: &PermissionUpdateDestination,
amendment: &ExecPolicyAmendment,
) -> Result<(), String> {
match destination {
PermissionSuggestionDestination::Session => sess
PermissionUpdateDestination::Session => sess
.services
.exec_policy
.add_amendment_to_current_policy(amendment)
.await
.map_err(|err| format!("failed to cache session prefix rule: {err}")),
PermissionSuggestionDestination::UserSettings => {
PermissionUpdateDestination::UserSettings => {
let codex_home = sess.codex_home().await;
sess.services
.exec_policy
@@ -296,7 +279,7 @@ async fn apply_execpolicy_amendment_destination(
.await
.map_err(|err| format!("failed to persist user prefix rule: {err}"))
}
PermissionSuggestionDestination::ProjectSettings => {
PermissionUpdateDestination::ProjectSettings => {
let config = turn_context.config.as_ref();
let project_codex_home = resolve_project_root(&config.config_layer_stack, &config.cwd)
.await
@@ -318,17 +301,26 @@ async fn apply_execpolicy_amendment_destination(
async fn apply_directory_update_from_hook(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
directory_update: &PermissionDirectoryUpdate,
permission: &PermissionUpdate,
) -> Result<(), String> {
let writable_roots = resolve_directory_update_paths(&turn_context.cwd, directory_update)?;
let writable_roots = resolve_directory_update_paths(&turn_context.cwd, permission)?;
match directory_update.destination {
PermissionSuggestionDestination::Session => {}
PermissionSuggestionDestination::UserSettings => {
match permission {
PermissionUpdate::AddDirectories {
destination: PermissionUpdateDestination::Session,
..
} => {}
PermissionUpdate::AddDirectories {
destination: PermissionUpdateDestination::UserSettings,
..
} => {
let codex_home = sess.codex_home().await;
append_writable_roots_to_config(codex_home.as_path(), &writable_roots).await?;
}
PermissionSuggestionDestination::ProjectSettings => {
PermissionUpdate::AddDirectories {
destination: PermissionUpdateDestination::ProjectSettings,
..
} => {
let config = turn_context.config.as_ref();
let project_codex_home = resolve_project_root(&config.config_layer_stack, &config.cwd)
.await
@@ -340,6 +332,9 @@ async fn apply_directory_update_from_hook(
.map_err(|err| format!("failed to create project config directory: {err}"))?;
append_writable_roots_to_config(&project_codex_home, &writable_roots).await?;
}
PermissionUpdate::AddRules { .. } => {
return Err("writable root update must use type:addDirectories".to_string());
}
}
sess.record_granted_session_permissions(permission_profile_for_writable_roots(&writable_roots))
@@ -349,10 +344,12 @@ async fn apply_directory_update_from_hook(
fn resolve_directory_update_paths(
cwd: &AbsolutePathBuf,
directory_update: &PermissionDirectoryUpdate,
permission: &PermissionUpdate,
) -> Result<Vec<AbsolutePathBuf>, String> {
let writable_roots = directory_update
.directories
let PermissionUpdate::AddDirectories { directories, .. } = permission else {
return Err("writable root update must use type:addDirectories".to_string());
};
let writable_roots = directories
.iter()
.map(|directory| {
let trimmed = directory.trim();

View File

@@ -33,7 +33,7 @@ use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::approval_permission_suggestions;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use crate::tools::sandboxing::with_cached_approval;
use codex_hooks::PermissionSuggestionDestination;
use codex_hooks::PermissionUpdateDestination;
use codex_network_proxy::NetworkProxy;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::PermissionProfile;
@@ -211,7 +211,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
req.exec_approval_requirement
.proposed_execpolicy_amendment(),
req.additional_permissions.as_ref(),
&[PermissionSuggestionDestination::UserSettings],
&[PermissionUpdateDestination::UserSettings],
);
Some(PermissionRequestPayload {
tool_name: "Bash".to_string(),

View File

@@ -34,7 +34,7 @@ use crate::unified_exec::NoopSpawnLifecycle;
use crate::unified_exec::UnifiedExecError;
use crate::unified_exec::UnifiedExecProcess;
use crate::unified_exec::UnifiedExecProcessManager;
use codex_hooks::PermissionSuggestionDestination;
use codex_hooks::PermissionUpdateDestination;
use codex_network_proxy::NetworkProxy;
use codex_protocol::error::CodexErr;
use codex_protocol::error::SandboxErr;
@@ -191,7 +191,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
req.exec_approval_requirement
.proposed_execpolicy_amendment(),
req.additional_permissions.as_ref(),
&[PermissionSuggestionDestination::UserSettings],
&[PermissionUpdateDestination::UserSettings],
);
Some(PermissionRequestPayload {
tool_name: "Bash".to_string(),

View File

@@ -10,11 +10,10 @@ use crate::sandboxing::ExecOptions;
use crate::sandboxing::SandboxPermissions;
use crate::state::SessionServices;
use crate::tools::network_approval::NetworkApprovalSpec;
use codex_hooks::PermissionSuggestion;
use codex_hooks::PermissionSuggestionBehavior;
use codex_hooks::PermissionSuggestionDestination;
use codex_hooks::PermissionSuggestionRule;
use codex_hooks::PermissionSuggestionType;
use codex_hooks::PermissionUpdate;
use codex_hooks::PermissionUpdateBehavior;
use codex_hooks::PermissionUpdateDestination;
use codex_hooks::PermissionUpdateRule;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext;
@@ -143,25 +142,24 @@ pub(crate) struct PermissionRequestPayload {
pub tool_name: String,
pub command: String,
pub description: Option<String>,
pub permission_suggestions: Vec<PermissionSuggestion>,
pub permission_suggestions: Vec<PermissionUpdate>,
}
pub(crate) fn exec_policy_permission_suggestions(
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
destinations: &[PermissionSuggestionDestination],
) -> Vec<PermissionSuggestion> {
destinations: &[PermissionUpdateDestination],
) -> Vec<PermissionUpdate> {
proposed_execpolicy_amendment
.into_iter()
.flat_map(|amendment| {
destinations
.iter()
.cloned()
.map(move |destination| PermissionSuggestion {
suggestion_type: PermissionSuggestionType::AddRules,
rules: vec![PermissionSuggestionRule::PrefixRule {
.map(move |destination| PermissionUpdate::AddRules {
rules: vec![PermissionUpdateRule::PrefixRule {
command: amendment.command.clone(),
}],
behavior: PermissionSuggestionBehavior::Allow,
behavior: PermissionUpdateBehavior::Allow,
destination,
})
})
@@ -172,8 +170,8 @@ pub(crate) fn approval_permission_suggestions(
network_approval_context: Option<&NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
additional_permissions: Option<&PermissionProfile>,
destinations: &[PermissionSuggestionDestination],
) -> Vec<PermissionSuggestion> {
destinations: &[PermissionUpdateDestination],
) -> Vec<PermissionUpdate> {
let available_decisions = ExecApprovalRequestEvent::default_available_decisions(
network_approval_context,
proposed_execpolicy_amendment,

View File

@@ -1,9 +1,9 @@
use super::*;
use crate::sandboxing::SandboxPermissions;
use codex_hooks::PermissionSuggestion;
use codex_hooks::PermissionSuggestionDestination;
use codex_hooks::PermissionSuggestionRule;
use codex_hooks::PermissionSuggestionType;
use codex_hooks::PermissionUpdate;
use codex_hooks::PermissionUpdateBehavior;
use codex_hooks::PermissionUpdateDestination;
use codex_hooks::PermissionUpdateRule;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol;
@@ -129,22 +129,21 @@ fn command_approval_execpolicy_amendment_maps_to_user_settings_suggestion() {
"node_modules".to_string(),
])),
/*additional_permissions*/ None,
&[PermissionSuggestionDestination::UserSettings],
&[PermissionUpdateDestination::UserSettings],
);
assert_eq!(
suggestions,
vec![PermissionSuggestion {
suggestion_type: PermissionSuggestionType::AddRules,
rules: vec![PermissionSuggestionRule::PrefixRule {
vec![PermissionUpdate::AddRules {
rules: vec![PermissionUpdateRule::PrefixRule {
command: vec![
"rm".to_string(),
"-rf".to_string(),
"node_modules".to_string(),
],
}],
behavior: PermissionSuggestionBehavior::Allow,
destination: PermissionSuggestionDestination::UserSettings,
behavior: PermissionUpdateBehavior::Allow,
destination: PermissionUpdateDestination::UserSettings,
}]
);
}
@@ -167,10 +166,10 @@ fn command_approval_with_additional_permissions_has_no_persistent_suggestions()
write: None,
}),
}),
&[PermissionSuggestionDestination::UserSettings],
&[PermissionUpdateDestination::UserSettings],
);
assert_eq!(suggestions, Vec::<PermissionSuggestion>::new());
assert_eq!(suggestions, Vec::<PermissionUpdate>::new());
}
#[test]
@@ -185,8 +184,8 @@ fn network_approval_with_execpolicy_amendment_has_no_persistent_suggestions() {
"https://example.com".to_string(),
])),
/*additional_permissions*/ None,
&[PermissionSuggestionDestination::UserSettings],
&[PermissionUpdateDestination::UserSettings],
);
assert_eq!(suggestions, Vec::<PermissionSuggestion>::new());
assert_eq!(suggestions, Vec::<PermissionUpdate>::new());
}

View File

@@ -332,7 +332,8 @@ elif mode == "allow_add_directories_session":
"hookEventName": "PermissionRequest",
"decision": {{
"behavior": "allow",
"addDirectories": [{{
"updatedPermissions": [{{
"type": "addDirectories",
"directories": [reason],
"destination": "session"
}}]
@@ -345,7 +346,8 @@ elif mode == "allow_add_directories_project":
"hookEventName": "PermissionRequest",
"decision": {{
"behavior": "allow",
"addDirectories": [{{
"updatedPermissions": [{{
"type": "addDirectories",
"directories": [reason],
"destination": "projectSettings"
}}]
@@ -358,7 +360,8 @@ elif mode == "allow_add_directories_user":
"hookEventName": "PermissionRequest",
"decision": {{
"behavior": "allow",
"addDirectories": [{{
"updatedPermissions": [{{
"type": "addDirectories",
"directories": [reason],
"destination": "userSettings"
}}]

View File

@@ -23,33 +23,65 @@
],
"type": "object"
},
"PermissionSuggestion": {
"properties": {
"behavior": {
"$ref": "#/definitions/PermissionSuggestionBehavior"
},
"destination": {
"$ref": "#/definitions/PermissionSuggestionDestination"
},
"rules": {
"items": {
"$ref": "#/definitions/PermissionSuggestionRule"
"PermissionUpdate": {
"oneOf": [
{
"properties": {
"behavior": {
"$ref": "#/definitions/PermissionUpdateBehavior"
},
"destination": {
"$ref": "#/definitions/PermissionUpdateDestination"
},
"rules": {
"items": {
"$ref": "#/definitions/PermissionUpdateRule"
},
"type": "array"
},
"type": {
"enum": [
"addRules"
],
"type": "string"
}
},
"type": "array"
"required": [
"behavior",
"destination",
"rules",
"type"
],
"type": "object"
},
"type": {
"$ref": "#/definitions/PermissionSuggestionType"
{
"properties": {
"destination": {
"$ref": "#/definitions/PermissionUpdateDestination"
},
"directories": {
"items": {
"type": "string"
},
"type": "array"
},
"type": {
"enum": [
"addDirectories"
],
"type": "string"
}
},
"required": [
"destination",
"directories",
"type"
],
"type": "object"
}
},
"required": [
"behavior",
"destination",
"rules",
"type"
],
"type": "object"
]
},
"PermissionSuggestionBehavior": {
"PermissionUpdateBehavior": {
"enum": [
"allow",
"deny",
@@ -57,7 +89,7 @@
],
"type": "string"
},
"PermissionSuggestionDestination": {
"PermissionUpdateDestination": {
"enum": [
"session",
"projectSettings",
@@ -65,7 +97,7 @@
],
"type": "string"
},
"PermissionSuggestionRule": {
"PermissionUpdateRule": {
"oneOf": [
{
"properties": {
@@ -89,12 +121,6 @@
"type": "object"
}
]
},
"PermissionSuggestionType": {
"enum": [
"addRules"
],
"type": "string"
}
},
"properties": {
@@ -120,7 +146,7 @@
},
"permission_suggestions": {
"items": {
"$ref": "#/definitions/PermissionSuggestion"
"$ref": "#/definitions/PermissionUpdate"
},
"type": "array"
},

View File

@@ -13,24 +13,6 @@
],
"type": "string"
},
"PermissionDirectoryUpdate": {
"properties": {
"destination": {
"$ref": "#/definitions/PermissionSuggestionDestination"
},
"directories": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"destination",
"directories"
],
"type": "object"
},
"PermissionRequestBehaviorWire": {
"enum": [
"allow",
@@ -41,13 +23,6 @@
"PermissionRequestDecisionWire": {
"additionalProperties": false,
"properties": {
"addDirectories": {
"default": null,
"items": {
"$ref": "#/definitions/PermissionDirectoryUpdate"
},
"type": "array"
},
"behavior": {
"$ref": "#/definitions/PermissionRequestBehaviorWire"
},
@@ -67,7 +42,7 @@
"updatedPermissions": {
"default": null,
"items": {
"$ref": "#/definitions/PermissionSuggestion"
"$ref": "#/definitions/PermissionUpdate"
},
"type": "array"
}
@@ -97,33 +72,65 @@
],
"type": "object"
},
"PermissionSuggestion": {
"properties": {
"behavior": {
"$ref": "#/definitions/PermissionSuggestionBehavior"
},
"destination": {
"$ref": "#/definitions/PermissionSuggestionDestination"
},
"rules": {
"items": {
"$ref": "#/definitions/PermissionSuggestionRule"
"PermissionUpdate": {
"oneOf": [
{
"properties": {
"behavior": {
"$ref": "#/definitions/PermissionUpdateBehavior"
},
"destination": {
"$ref": "#/definitions/PermissionUpdateDestination"
},
"rules": {
"items": {
"$ref": "#/definitions/PermissionUpdateRule"
},
"type": "array"
},
"type": {
"enum": [
"addRules"
],
"type": "string"
}
},
"type": "array"
"required": [
"behavior",
"destination",
"rules",
"type"
],
"type": "object"
},
"type": {
"$ref": "#/definitions/PermissionSuggestionType"
{
"properties": {
"destination": {
"$ref": "#/definitions/PermissionUpdateDestination"
},
"directories": {
"items": {
"type": "string"
},
"type": "array"
},
"type": {
"enum": [
"addDirectories"
],
"type": "string"
}
},
"required": [
"destination",
"directories",
"type"
],
"type": "object"
}
},
"required": [
"behavior",
"destination",
"rules",
"type"
],
"type": "object"
]
},
"PermissionSuggestionBehavior": {
"PermissionUpdateBehavior": {
"enum": [
"allow",
"deny",
@@ -131,7 +138,7 @@
],
"type": "string"
},
"PermissionSuggestionDestination": {
"PermissionUpdateDestination": {
"enum": [
"session",
"projectSettings",
@@ -139,7 +146,7 @@
],
"type": "string"
},
"PermissionSuggestionRule": {
"PermissionUpdateRule": {
"oneOf": [
{
"properties": {
@@ -163,12 +170,6 @@
"type": "object"
}
]
},
"PermissionSuggestionType": {
"enum": [
"addRules"
],
"type": "string"
}
},
"properties": {

View File

@@ -22,8 +22,7 @@ pub(crate) struct PreToolUseOutput {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PermissionRequestDecision {
Allow {
updated_permissions: Vec<crate::events::permission_request::PermissionSuggestion>,
add_directories: Vec<crate::events::permission_request::PermissionDirectoryUpdate>,
updated_permissions: Vec<crate::events::permission_request::PermissionUpdate>,
},
Deny {
message: String,
@@ -306,12 +305,9 @@ fn unsupported_permission_request_hook_specific_output(
if decision.updated_input.is_some() {
Some("PermissionRequest hook returned unsupported updatedInput".to_string())
} else if matches!(decision.behavior, PermissionRequestBehaviorWire::Deny)
&& (decision.updated_permissions.is_some() || decision.add_directories.is_some())
&& decision.updated_permissions.is_some()
{
Some(
"PermissionRequest hook returned updatedPermissions/addDirectories for deny decision"
.to_string(),
)
Some("PermissionRequest hook returned updatedPermissions for deny decision".to_string())
} else if decision.interrupt {
Some("PermissionRequest hook returned unsupported interrupt:true".to_string())
} else {
@@ -325,7 +321,6 @@ fn permission_request_decision(
match decision.behavior {
PermissionRequestBehaviorWire::Allow => PermissionRequestDecision::Allow {
updated_permissions: decision.updated_permissions.clone().unwrap_or_default(),
add_directories: decision.add_directories.clone().unwrap_or_default(),
},
PermissionRequestBehaviorWire::Deny => PermissionRequestDecision::Deny {
message: decision
@@ -488,19 +483,16 @@ mod tests {
assert_eq!(
parsed.decision,
Some(PermissionRequestDecision::Allow {
updated_permissions: vec![crate::events::permission_request::PermissionSuggestion {
suggestion_type:
crate::events::permission_request::PermissionSuggestionType::AddRules,
updated_permissions: vec![crate::events::permission_request::PermissionUpdate::AddRules {
rules: vec![
crate::events::permission_request::PermissionSuggestionRule::PrefixRule {
crate::events::permission_request::PermissionUpdateRule::PrefixRule {
command: vec!["rm".to_string(), "-f".to_string()],
},
],
behavior:
crate::events::permission_request::PermissionSuggestionBehavior::Allow,
destination: crate::events::permission_request::PermissionSuggestionDestination::UserSettings,
crate::events::permission_request::PermissionUpdateBehavior::Allow,
destination: crate::events::permission_request::PermissionUpdateDestination::UserSettings,
}],
add_directories: vec![],
})
);
}
@@ -510,15 +502,16 @@ mod tests {
let parsed = parse_permission_request(
&json!({
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"addDirectories": [{
"directories": ["./logs", "/tmp/output"],
"destination": "session"
}]
}
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": [{
"type": "addDirectories",
"directories": ["./logs", "/tmp/output"],
"destination": "session"
}]
}
}
})
.to_string(),
@@ -528,12 +521,11 @@ mod tests {
assert_eq!(
parsed.decision,
Some(PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![
crate::events::permission_request::PermissionDirectoryUpdate {
updated_permissions: vec![
crate::events::permission_request::PermissionUpdate::AddDirectories {
directories: vec!["./logs".to_string(), "/tmp/output".to_string()],
destination:
crate::events::permission_request::PermissionSuggestionDestination::Session,
crate::events::permission_request::PermissionUpdateDestination::Session,
},
],
})
@@ -568,8 +560,7 @@ mod tests {
assert_eq!(
parsed.invalid_reason,
Some(
"PermissionRequest hook returned updatedPermissions/addDirectories for deny decision"
.to_string()
"PermissionRequest hook returned updatedPermissions for deny decision".to_string()
)
);
}
@@ -579,15 +570,16 @@ mod tests {
let parsed = parse_permission_request(
&json!({
"continue": true,
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"addDirectories": [{
"directories": ["./logs"],
"destination": "session"
}]
}
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"updatedPermissions": [{
"type": "addDirectories",
"directories": ["./logs"],
"destination": "session"
}]
}
}
})
.to_string(),
@@ -597,8 +589,7 @@ mod tests {
assert_eq!(
parsed.invalid_reason,
Some(
"PermissionRequest hook returned updatedPermissions/addDirectories for deny decision"
.to_string()
"PermissionRequest hook returned updatedPermissions for deny decision".to_string()
)
);
}

View File

@@ -36,38 +36,29 @@ use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PermissionSuggestion {
#[serde(rename = "type")]
pub suggestion_type: PermissionSuggestionType,
pub rules: Vec<PermissionSuggestionRule>,
pub behavior: PermissionSuggestionBehavior,
pub destination: PermissionSuggestionDestination,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PermissionDirectoryUpdate {
pub directories: Vec<String>,
pub destination: PermissionSuggestionDestination,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PermissionSuggestionType {
AddRules,
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PermissionUpdate {
AddRules {
rules: Vec<PermissionUpdateRule>,
behavior: PermissionUpdateBehavior,
destination: PermissionUpdateDestination,
},
AddDirectories {
directories: Vec<String>,
destination: PermissionUpdateDestination,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionSuggestionBehavior {
pub enum PermissionUpdateBehavior {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub enum PermissionSuggestionDestination {
pub enum PermissionUpdateDestination {
#[serde(rename = "session")]
Session,
#[serde(rename = "projectSettings")]
@@ -78,7 +69,7 @@ pub enum PermissionSuggestionDestination {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PermissionSuggestionRule {
pub enum PermissionUpdateRule {
PrefixRule { command: Vec<String> },
}
@@ -94,14 +85,13 @@ pub struct PermissionRequestRequest {
pub run_id_suffix: String,
pub command: String,
pub description: Option<String>,
pub permission_suggestions: Vec<PermissionSuggestion>,
pub permission_suggestions: Vec<PermissionUpdate>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionRequestDecision {
Allow {
updated_permissions: Vec<PermissionSuggestion>,
add_directories: Vec<PermissionDirectoryUpdate>,
updated_permissions: Vec<PermissionUpdate>,
},
Deny {
message: String,
@@ -222,12 +212,10 @@ fn resolve_permission_request_decision<'a>(
) -> Option<PermissionRequestDecision> {
let mut saw_allow = false;
let mut updated_permissions = Vec::new();
let mut add_directories = Vec::new();
for decision in decisions {
match decision {
PermissionRequestDecision::Allow {
updated_permissions: selected_permissions,
add_directories: selected_directories,
} => {
saw_allow = true;
for permission in selected_permissions {
@@ -235,11 +223,6 @@ fn resolve_permission_request_decision<'a>(
updated_permissions.push(permission.clone());
}
}
for directory_update in selected_directories {
if !add_directories.contains(directory_update) {
add_directories.push(directory_update.clone());
}
}
}
PermissionRequestDecision::Deny { message } => {
return Some(PermissionRequestDecision::Deny {
@@ -250,17 +233,15 @@ fn resolve_permission_request_decision<'a>(
}
saw_allow.then_some(PermissionRequestDecision::Allow {
updated_permissions,
add_directories,
})
}
fn invalid_permission_updates(
decision: Option<&PermissionRequestDecision>,
offered_permissions: &[PermissionSuggestion],
offered_permissions: &[PermissionUpdate],
) -> Option<String> {
let PermissionRequestDecision::Allow {
updated_permissions,
add_directories: _,
} = decision?
else {
return None;
@@ -334,11 +315,9 @@ fn parse_completed(
match parsed_decision {
output_parser::PermissionRequestDecision::Allow {
updated_permissions,
add_directories,
} => {
decision = Some(PermissionRequestDecision::Allow {
updated_permissions,
add_directories,
});
}
output_parser::PermissionRequestDecision::Deny { message } => {
@@ -407,13 +386,11 @@ fn parse_completed(
mod tests {
use pretty_assertions::assert_eq;
use super::PermissionDirectoryUpdate;
use super::PermissionRequestDecision;
use super::PermissionSuggestion;
use super::PermissionSuggestionBehavior;
use super::PermissionSuggestionDestination;
use super::PermissionSuggestionRule;
use super::PermissionSuggestionType;
use super::PermissionUpdate;
use super::PermissionUpdateBehavior;
use super::PermissionUpdateDestination;
use super::PermissionUpdateRule;
use super::invalid_permission_updates;
use super::resolve_permission_request_decision;
@@ -422,7 +399,6 @@ mod tests {
let decisions = [
PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![],
},
PermissionRequestDecision::Deny {
message: "repo deny".to_string(),
@@ -442,11 +418,9 @@ mod tests {
let decisions = [
PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![],
},
PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![],
},
];
@@ -454,7 +428,6 @@ mod tests {
resolve_permission_request_decision(decisions.iter()),
Some(PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![],
})
);
}
@@ -468,22 +441,19 @@ mod tests {
#[test]
fn permission_request_accumulates_distinct_updated_permissions() {
let permission = PermissionSuggestion {
suggestion_type: PermissionSuggestionType::AddRules,
rules: vec![PermissionSuggestionRule::PrefixRule {
let permission = PermissionUpdate::AddRules {
rules: vec![PermissionUpdateRule::PrefixRule {
command: vec!["rm".to_string(), "-f".to_string()],
}],
behavior: PermissionSuggestionBehavior::Allow,
destination: PermissionSuggestionDestination::UserSettings,
behavior: PermissionUpdateBehavior::Allow,
destination: PermissionUpdateDestination::UserSettings,
};
let decisions = [
PermissionRequestDecision::Allow {
updated_permissions: vec![permission.clone()],
add_directories: vec![],
},
PermissionRequestDecision::Allow {
updated_permissions: vec![permission.clone()],
add_directories: vec![],
},
];
@@ -491,57 +461,50 @@ mod tests {
resolve_permission_request_decision(decisions.iter()),
Some(PermissionRequestDecision::Allow {
updated_permissions: vec![permission],
add_directories: vec![],
})
);
}
#[test]
fn permission_request_accumulates_distinct_directory_updates() {
let directory_update = PermissionDirectoryUpdate {
let directory_update = PermissionUpdate::AddDirectories {
directories: vec!["./logs".to_string(), "/tmp/output".to_string()],
destination: PermissionSuggestionDestination::Session,
destination: PermissionUpdateDestination::Session,
};
let decisions = [
PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![directory_update.clone()],
updated_permissions: vec![directory_update.clone()],
},
PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![directory_update.clone()],
updated_permissions: vec![directory_update.clone()],
},
];
assert_eq!(
resolve_permission_request_decision(decisions.iter()),
Some(PermissionRequestDecision::Allow {
updated_permissions: vec![],
add_directories: vec![directory_update],
updated_permissions: vec![directory_update],
})
);
}
#[test]
fn permission_request_rejects_unoffered_updated_permissions() {
let offered = vec![PermissionSuggestion {
suggestion_type: PermissionSuggestionType::AddRules,
rules: vec![PermissionSuggestionRule::PrefixRule {
let offered = vec![PermissionUpdate::AddRules {
rules: vec![PermissionUpdateRule::PrefixRule {
command: vec!["rm".to_string()],
}],
behavior: PermissionSuggestionBehavior::Allow,
destination: PermissionSuggestionDestination::UserSettings,
behavior: PermissionUpdateBehavior::Allow,
destination: PermissionUpdateDestination::UserSettings,
}];
let selected = PermissionRequestDecision::Allow {
updated_permissions: vec![PermissionSuggestion {
suggestion_type: PermissionSuggestionType::AddRules,
rules: vec![PermissionSuggestionRule::PrefixRule {
updated_permissions: vec![PermissionUpdate::AddRules {
rules: vec![PermissionUpdateRule::PrefixRule {
command: vec!["curl".to_string()],
}],
behavior: PermissionSuggestionBehavior::Allow,
destination: PermissionSuggestionDestination::UserSettings,
behavior: PermissionUpdateBehavior::Allow,
destination: PermissionUpdateDestination::UserSettings,
}],
add_directories: vec![],
};
assert_eq!(

View File

@@ -5,15 +5,13 @@ mod registry;
mod schema;
mod types;
pub use events::permission_request::PermissionDirectoryUpdate;
pub use events::permission_request::PermissionRequestDecision;
pub use events::permission_request::PermissionRequestOutcome;
pub use events::permission_request::PermissionRequestRequest;
pub use events::permission_request::PermissionSuggestion;
pub use events::permission_request::PermissionSuggestionBehavior;
pub use events::permission_request::PermissionSuggestionDestination;
pub use events::permission_request::PermissionSuggestionRule;
pub use events::permission_request::PermissionSuggestionType;
pub use events::permission_request::PermissionUpdate;
pub use events::permission_request::PermissionUpdateBehavior;
pub use events::permission_request::PermissionUpdateDestination;
pub use events::permission_request::PermissionUpdateRule;
pub use events::post_tool_use::PostToolUseOutcome;
pub use events::post_tool_use::PostToolUseRequest;
pub use events::pre_tool_use::PreToolUseOutcome;

View File

@@ -12,8 +12,7 @@ use serde_json::Value;
use std::path::Path;
use std::path::PathBuf;
use crate::events::permission_request::PermissionDirectoryUpdate;
use crate::events::permission_request::PermissionSuggestion;
use crate::events::permission_request::PermissionUpdate;
const GENERATED_DIR: &str = "generated";
const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.json";
@@ -147,9 +146,7 @@ pub(crate) struct PermissionRequestDecisionWire {
#[serde(default)]
pub updated_input: Option<Value>,
#[serde(default)]
pub updated_permissions: Option<Vec<PermissionSuggestion>>,
#[serde(default)]
pub add_directories: Option<Vec<PermissionDirectoryUpdate>>,
pub updated_permissions: Option<Vec<PermissionUpdate>>,
#[serde(default)]
pub message: Option<String>,
/// Reserved for future short-circuiting semantics.
@@ -266,7 +263,7 @@ pub(crate) struct PermissionRequestCommandInput {
pub tool_name: String,
pub tool_input: PermissionRequestToolInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_suggestions: Option<Vec<PermissionSuggestion>>,
pub permission_suggestions: Option<Vec<PermissionUpdate>>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]