mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Add permission suggestions to PermissionRequest hooks
Expose top-level permission suggestions in PermissionRequest hook inputs and build them from source approval context so deferred unified-exec network retries carry the same suggestion data as immediate approval paths. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -169,6 +169,7 @@ pub(crate) async fn run_permission_request_hooks(
|
||||
run_id_suffix: run_id_suffix.to_string(),
|
||||
command: payload.command,
|
||||
description: payload.description,
|
||||
permission_suggestions: payload.permission_suggestions,
|
||||
};
|
||||
let preview_runs = sess.hooks().preview_permission_request(&request);
|
||||
emit_hook_started_events(sess, turn_context, preview_runs).await;
|
||||
|
||||
@@ -389,6 +389,7 @@ impl NetworkApprovalService {
|
||||
tool_name: "Bash".to_string(),
|
||||
command,
|
||||
description: Some(format!("network-access {target}")),
|
||||
permission_suggestions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -368,7 +368,7 @@ impl ToolOrchestrator {
|
||||
where
|
||||
T: ToolRuntime<Rq, Out>,
|
||||
{
|
||||
if let Some(permission_request) = tool.permission_request_payload(req) {
|
||||
if let Some(permission_request) = tool.permission_request_payload(req, &approval_ctx) {
|
||||
match run_permission_request_hooks(
|
||||
approval_ctx.session,
|
||||
approval_ctx.turn,
|
||||
|
||||
@@ -30,8 +30,10 @@ use crate::tools::sandboxing::Sandboxable;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::exec_policy_permission_suggestions;
|
||||
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_hooks::PermissionSuggestionDestination;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -199,11 +201,21 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
Some(req.exec_approval_requirement.clone())
|
||||
}
|
||||
|
||||
fn permission_request_payload(&self, req: &ShellRequest) -> Option<PermissionRequestPayload> {
|
||||
fn permission_request_payload(
|
||||
&self,
|
||||
req: &ShellRequest,
|
||||
_approval_ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
let permission_suggestions = exec_policy_permission_suggestions(
|
||||
req.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment(),
|
||||
&[PermissionSuggestionDestination::UserSettings],
|
||||
);
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
command: req.hook_command.clone(),
|
||||
description: req.justification.clone(),
|
||||
permission_suggestions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -403,6 +403,7 @@ impl CoreShellActionProvider {
|
||||
tool_name: "Bash".to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
description: None,
|
||||
permission_suggestions: Vec::new(),
|
||||
};
|
||||
let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
|
||||
match run_permission_request_hooks(
|
||||
|
||||
@@ -27,12 +27,14 @@ use crate::tools::sandboxing::Sandboxable;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::exec_policy_permission_suggestions;
|
||||
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
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_network_proxy::NetworkProxy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
@@ -182,11 +184,18 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
fn permission_request_payload(
|
||||
&self,
|
||||
req: &UnifiedExecRequest,
|
||||
_approval_ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
let permission_suggestions = exec_policy_permission_suggestions(
|
||||
req.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment(),
|
||||
&[PermissionSuggestionDestination::UserSettings],
|
||||
);
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: "Bash".to_string(),
|
||||
command: req.hook_command.clone(),
|
||||
description: req.justification.clone(),
|
||||
permission_suggestions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ 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_network_proxy::NetworkProxy;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkApprovalContext;
|
||||
@@ -136,6 +141,29 @@ pub(crate) struct PermissionRequestPayload {
|
||||
pub tool_name: String,
|
||||
pub command: String,
|
||||
pub description: Option<String>,
|
||||
pub permission_suggestions: Vec<PermissionSuggestion>,
|
||||
}
|
||||
|
||||
pub(crate) fn exec_policy_permission_suggestions(
|
||||
proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
|
||||
destinations: &[PermissionSuggestionDestination],
|
||||
) -> Vec<PermissionSuggestion> {
|
||||
proposed_execpolicy_amendment
|
||||
.into_iter()
|
||||
.flat_map(|amendment| {
|
||||
destinations
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(move |destination| PermissionSuggestion {
|
||||
suggestion_type: PermissionSuggestionType::AddRules,
|
||||
rules: vec![PermissionSuggestionRule::PrefixRule {
|
||||
command: amendment.command.clone(),
|
||||
}],
|
||||
behavior: PermissionSuggestionBehavior::Allow,
|
||||
destination,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Specifies what tool orchestrator should do with a given tool call.
|
||||
@@ -282,7 +310,11 @@ pub(crate) trait Approvable<Req> {
|
||||
|
||||
/// Return hook input for approval-time policy hooks when this runtime wants
|
||||
/// hook evaluation to run before guardian or user approval.
|
||||
fn permission_request_payload(&self, _req: &Req) -> Option<PermissionRequestPayload> {
|
||||
fn permission_request_payload(
|
||||
&self,
|
||||
_req: &Req,
|
||||
_approval_ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use super::*;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use codex_hooks::PermissionSuggestion;
|
||||
use codex_hooks::PermissionSuggestionDestination;
|
||||
use codex_hooks::PermissionSuggestionRule;
|
||||
use codex_hooks::PermissionSuggestionType;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -108,3 +113,31 @@ fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() {
|
||||
SandboxOverride::BypassSandboxFirstAttempt
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_approval_execpolicy_amendment_maps_to_user_settings_suggestion() {
|
||||
let suggestions = exec_policy_permission_suggestions(
|
||||
Some(&ExecPolicyAmendment::new(vec![
|
||||
"rm".to_string(),
|
||||
"-rf".to_string(),
|
||||
"node_modules".to_string(),
|
||||
])),
|
||||
&[PermissionSuggestionDestination::UserSettings],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
suggestions,
|
||||
vec![PermissionSuggestion {
|
||||
suggestion_type: PermissionSuggestionType::AddRules,
|
||||
rules: vec![PermissionSuggestionRule::PrefixRule {
|
||||
command: vec![
|
||||
"rm".to_string(),
|
||||
"-rf".to_string(),
|
||||
"node_modules".to_string(),
|
||||
],
|
||||
}],
|
||||
behavior: PermissionSuggestionBehavior::Allow,
|
||||
destination: PermissionSuggestionDestination::UserSettings,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -521,6 +521,7 @@ fn assert_permission_request_hook_input(
|
||||
assert!(hook_input.get("justification").is_none());
|
||||
assert!(hook_input.get("host").is_none());
|
||||
assert!(hook_input.get("protocol").is_none());
|
||||
assert!(hook_input.get("permission_suggestions").is_none());
|
||||
}
|
||||
|
||||
fn assert_single_permission_request_hook_input(
|
||||
@@ -534,6 +535,13 @@ fn assert_single_permission_request_hook_input(
|
||||
Ok(hook_inputs)
|
||||
}
|
||||
|
||||
fn assert_permission_request_hook_suggestions(hook_input: &Value, expected: Value) {
|
||||
assert_eq!(
|
||||
hook_input["permission_suggestions"], expected,
|
||||
"permission request hook should include expected suggestions"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_post_tool_use_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
|
||||
fs::read_to_string(home.join("post_tool_use_hook_log.jsonl"))
|
||||
.context("read post tool use hook log")?
|
||||
@@ -1203,11 +1211,26 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() ->
|
||||
"approved command should remove marker file"
|
||||
);
|
||||
|
||||
let hook_inputs = assert_single_permission_request_hook_input(
|
||||
test.codex_home_path(),
|
||||
&command,
|
||||
/*description*/ None,
|
||||
)?;
|
||||
let hook_inputs = read_permission_request_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PermissionRequest");
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["description"], Value::Null);
|
||||
assert_permission_request_hook_suggestions(
|
||||
&hook_inputs[0],
|
||||
serde_json::json!([
|
||||
{
|
||||
"type": "addRules",
|
||||
"rules": [{
|
||||
"type": "prefixRule",
|
||||
"command": ["rm", "-f", marker.display().to_string()],
|
||||
}],
|
||||
"behavior": "allow",
|
||||
"destination": "userSettings",
|
||||
}
|
||||
]),
|
||||
);
|
||||
assert!(
|
||||
hook_inputs[0].get("tool_use_id").is_none(),
|
||||
"PermissionRequest input should not include a tool_use_id",
|
||||
@@ -1293,11 +1316,26 @@ async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> {
|
||||
"approved exec command should remove marker file"
|
||||
);
|
||||
|
||||
assert_single_permission_request_hook_input(
|
||||
test.codex_home_path(),
|
||||
&command,
|
||||
Some(justification),
|
||||
)?;
|
||||
let hook_inputs = read_permission_request_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PermissionRequest");
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["description"], justification);
|
||||
assert_permission_request_hook_suggestions(
|
||||
&hook_inputs[0],
|
||||
serde_json::json!([
|
||||
{
|
||||
"type": "addRules",
|
||||
"rules": [{
|
||||
"type": "prefixRule",
|
||||
"command": ["rm", "-f", marker.display().to_string()],
|
||||
}],
|
||||
"behavior": "allow",
|
||||
"destination": "userSettings",
|
||||
}
|
||||
]),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1442,11 +1480,180 @@ allow_local_binding = true
|
||||
"expected the network approval hook to bypass the approval prompt"
|
||||
);
|
||||
|
||||
assert_single_permission_request_hook_input(
|
||||
test.codex_home_path(),
|
||||
command,
|
||||
Some("network-access http://codex-network-test.invalid:80"),
|
||||
let hook_inputs = read_permission_request_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PermissionRequest");
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
assert_eq!(
|
||||
hook_inputs[0]["tool_input"]["description"],
|
||||
"network-access http://codex-network-test.invalid:80"
|
||||
);
|
||||
assert!(hook_inputs[0].get("permission_suggestions").is_none());
|
||||
|
||||
test.codex.submit(Op::Shutdown {}).await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ShutdownComplete)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn permission_request_hook_sees_exec_command_network_retry_suggestions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
fs::write(
|
||||
home.path().join("config.toml"),
|
||||
r#"default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
[permissions.workspace.network]
|
||||
enabled = true
|
||||
mode = "limited"
|
||||
allow_local_binding = true
|
||||
"#,
|
||||
)?;
|
||||
let call_id = "permissionrequest-exec-network-approval";
|
||||
let command = r#"python3 -c "import urllib.request; opener = urllib.request.build_opener(urllib.request.ProxyHandler()); print('OK:' + opener.open('http://codex-network-test.invalid', timeout=2).read().decode(errors='replace'))""#;
|
||||
let args = serde_json::json!({
|
||||
"cmd": command,
|
||||
"login": true,
|
||||
"sandbox_permissions": "require_escalated",
|
||||
});
|
||||
let _responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message(
|
||||
"msg-1",
|
||||
"permission request hook allowed exec network access",
|
||||
),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let approval_policy = AskForApproval::OnFailure;
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: Default::default(),
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
let test = test_codex()
|
||||
.with_home(Arc::clone(&home))
|
||||
.with_pre_build_hook(|home| {
|
||||
if let Err(error) = install_allow_permission_request_hook(home) {
|
||||
panic!("failed to write permission request hook test fixture: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(move |config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.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);
|
||||
let layers = config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut requirements = config.config_layer_stack.requirements().clone();
|
||||
requirements.network = Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allow_local_binding: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
));
|
||||
let mut requirements_toml = config.config_layer_stack.requirements_toml().clone();
|
||||
requirements_toml.network = Some(NetworkRequirementsToml {
|
||||
enabled: Some(true),
|
||||
allow_local_binding: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
config.config_layer_stack =
|
||||
ConfigLayerStack::new(layers, requirements, requirements_toml)
|
||||
.expect("rebuild config layer stack with network requirements");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
test.submit_turn_with_policies(
|
||||
"run the exec command after network hook approval",
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
)
|
||||
.await?;
|
||||
|
||||
timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
if test
|
||||
.codex_home_path()
|
||||
.join("permission_request_hook_log.jsonl")
|
||||
.exists()
|
||||
{
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("expected exec network approval hook to run");
|
||||
|
||||
assert!(
|
||||
timeout(
|
||||
Duration::from_secs(2),
|
||||
wait_for_event(&test.codex, |event| matches!(
|
||||
event,
|
||||
EventMsg::ExecApprovalRequest(_)
|
||||
))
|
||||
)
|
||||
.await
|
||||
.is_err(),
|
||||
"expected the exec network approval hook to bypass the approval prompt"
|
||||
);
|
||||
|
||||
let hook_inputs = read_permission_request_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["hook_event_name"], "PermissionRequest");
|
||||
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
|
||||
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
|
||||
assert_eq!(
|
||||
hook_inputs[0]["tool_input"]["description"],
|
||||
"Network access to \"codex-network-test.invalid\" is blocked by policy."
|
||||
);
|
||||
assert!(hook_inputs[0].get("permission_suggestions").is_none());
|
||||
|
||||
test.codex.submit(Op::Shutdown {}).await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
|
||||
@@ -22,6 +22,79 @@
|
||||
"command"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionSuggestion": {
|
||||
"properties": {
|
||||
"behavior": {
|
||||
"$ref": "#/definitions/PermissionSuggestionBehavior"
|
||||
},
|
||||
"destination": {
|
||||
"$ref": "#/definitions/PermissionSuggestionDestination"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSuggestionRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/PermissionSuggestionType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"behavior",
|
||||
"destination",
|
||||
"rules",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PermissionSuggestionBehavior": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny",
|
||||
"ask"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionSuggestionDestination": {
|
||||
"enum": [
|
||||
"session",
|
||||
"projectSettings",
|
||||
"userSettings"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionSuggestionRule": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"command": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"prefixRule"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionSuggestionType": {
|
||||
"enum": [
|
||||
"addRules"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -45,6 +118,12 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"permission_suggestions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSuggestion"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,48 @@ use codex_protocol::protocol::HookOutputEntry;
|
||||
use codex_protocol::protocol::HookOutputEntryKind;
|
||||
use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookRunSummary;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, 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, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PermissionSuggestionType {
|
||||
AddRules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PermissionSuggestionBehavior {
|
||||
Allow,
|
||||
Deny,
|
||||
Ask,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
pub enum PermissionSuggestionDestination {
|
||||
#[serde(rename = "session")]
|
||||
Session,
|
||||
#[serde(rename = "projectSettings")]
|
||||
ProjectSettings,
|
||||
#[serde(rename = "userSettings")]
|
||||
UserSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PermissionSuggestionRule {
|
||||
PrefixRule { command: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PermissionRequestRequest {
|
||||
@@ -43,6 +85,7 @@ pub struct PermissionRequestRequest {
|
||||
pub run_id_suffix: String,
|
||||
pub command: String,
|
||||
pub description: Option<String>,
|
||||
pub permission_suggestions: Vec<PermissionSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -179,6 +222,8 @@ fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestC
|
||||
command: request.command.clone(),
|
||||
description: request.description.clone(),
|
||||
},
|
||||
permission_suggestions: (!request.permission_suggestions.is_empty())
|
||||
.then_some(request.permission_suggestions.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ mod types;
|
||||
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::post_tool_use::PostToolUseOutcome;
|
||||
pub use events::post_tool_use::PostToolUseRequest;
|
||||
pub use events::pre_tool_use::PreToolUseOutcome;
|
||||
|
||||
@@ -12,6 +12,8 @@ use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::events::permission_request::PermissionSuggestion;
|
||||
|
||||
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";
|
||||
@@ -263,6 +265,8 @@ pub(crate) struct PermissionRequestCommandInput {
|
||||
#[schemars(schema_with = "permission_request_tool_name_schema")]
|
||||
pub tool_name: String,
|
||||
pub tool_input: PermissionRequestToolInput,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permission_suggestions: Option<Vec<PermissionSuggestion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
|
||||
Reference in New Issue
Block a user