mirror of
https://github.com/openai/codex.git
synced 2026-04-23 14:14:50 +00:00
Compare commits
1 Commits
refactor/s
...
codex/cent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
303f9ea525 |
430
codex-rs/core/src/tools/approval_router.rs
Normal file
430
codex-rs/core/src/tools/approval_router.rs
Normal file
@@ -0,0 +1,430 @@
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::GuardianCommandSource;
|
||||
use codex_protocol::approvals::NetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct ApprovalOutcome {
|
||||
pub decision: ReviewDecision,
|
||||
pub guardian_review_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ApprovalOutcome {
|
||||
pub(crate) fn new(decision: ReviewDecision) -> Self {
|
||||
Self {
|
||||
decision,
|
||||
guardian_review_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ApprovalCache {
|
||||
tool_name: &'static str,
|
||||
serialized_keys: Vec<String>,
|
||||
key_count: usize,
|
||||
}
|
||||
|
||||
impl ApprovalCache {
|
||||
pub(crate) fn new<K>(tool_name: &'static str, keys: Vec<K>) -> Self
|
||||
where
|
||||
K: Serialize,
|
||||
{
|
||||
let key_count = keys.len();
|
||||
let serialized_keys = keys
|
||||
.into_iter()
|
||||
.filter_map(|key| serde_json::to_string(&key).ok())
|
||||
.collect();
|
||||
Self {
|
||||
tool_name,
|
||||
serialized_keys,
|
||||
key_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_keys(&self) -> bool {
|
||||
self.key_count > 0
|
||||
}
|
||||
|
||||
fn all_keys_serialized(&self) -> bool {
|
||||
self.serialized_keys.len() == self.key_count
|
||||
}
|
||||
|
||||
async fn is_approved_for_session(&self, session: &Session) -> bool {
|
||||
if !self.has_keys() || !self.all_keys_serialized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let store = session.services.tool_approvals.lock().await;
|
||||
self.serialized_keys.iter().all(|key| {
|
||||
matches!(
|
||||
store.get_serialized(key),
|
||||
Some(ReviewDecision::ApprovedForSession)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn record_outcome(&self, session: &Session, outcome: &ApprovalOutcome) {
|
||||
if !self.has_keys() {
|
||||
return;
|
||||
}
|
||||
|
||||
session.services.session_telemetry.counter(
|
||||
"codex.approval.requested",
|
||||
/*inc*/ 1,
|
||||
&[
|
||||
("tool", self.tool_name),
|
||||
("approved", outcome.decision.to_opaque_string()),
|
||||
],
|
||||
);
|
||||
|
||||
if matches!(outcome.decision, ReviewDecision::ApprovedForSession) {
|
||||
let mut store = session.services.tool_approvals.lock().await;
|
||||
for key in &self.serialized_keys {
|
||||
store.put_serialized(key.clone(), ReviewDecision::ApprovedForSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ApprovalRequest {
|
||||
pub intent: ApprovalIntent,
|
||||
pub retry_reason: Option<String>,
|
||||
pub cache: Option<ApprovalCache>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum ApprovalIntent {
|
||||
Shell(ShellApprovalRequest),
|
||||
UnifiedExec(UnifiedExecApprovalRequest),
|
||||
ApplyPatch(ApplyPatchApprovalRequest),
|
||||
NetworkAccess(NetworkAccessApprovalRequest),
|
||||
#[cfg(unix)]
|
||||
Execve(ExecveApprovalRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ShellApprovalRequest {
|
||||
pub call_id: String,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub justification: Option<String>,
|
||||
pub reason: Option<String>,
|
||||
pub network_approval_context: Option<NetworkApprovalContext>,
|
||||
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct UnifiedExecApprovalRequest {
|
||||
pub call_id: String,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub justification: Option<String>,
|
||||
pub tty: bool,
|
||||
pub reason: Option<String>,
|
||||
pub network_approval_context: Option<NetworkApprovalContext>,
|
||||
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ApplyPatchApprovalRequest {
|
||||
pub call_id: String,
|
||||
pub cwd: PathBuf,
|
||||
pub files: Vec<AbsolutePathBuf>,
|
||||
pub patch: String,
|
||||
pub changes: HashMap<PathBuf, FileChange>,
|
||||
pub reason: Option<String>,
|
||||
pub grant_root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NetworkAccessApprovalRequest {
|
||||
pub call_id: String,
|
||||
pub turn_id: String,
|
||||
pub target: String,
|
||||
pub host: String,
|
||||
pub protocol: NetworkApprovalProtocol,
|
||||
pub port: u16,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub reason: Option<String>,
|
||||
pub network_approval_context: NetworkApprovalContext,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ExecveApprovalRequest {
|
||||
pub call_id: String,
|
||||
pub approval_id: Option<String>,
|
||||
pub source: GuardianCommandSource,
|
||||
pub program: String,
|
||||
pub argv: Vec<String>,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub reason: Option<String>,
|
||||
pub available_decisions: Vec<ReviewDecision>,
|
||||
}
|
||||
|
||||
pub(crate) async fn request_approval(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
request: ApprovalRequest,
|
||||
) -> ApprovalOutcome {
|
||||
let routes_to_guardian = routes_approval_to_guardian(turn);
|
||||
if routes_to_guardian {
|
||||
return request_guardian_approval(session, turn, request).await;
|
||||
}
|
||||
|
||||
let ApprovalRequest {
|
||||
intent,
|
||||
retry_reason,
|
||||
cache,
|
||||
} = request;
|
||||
|
||||
if should_consult_session_cache(routes_to_guardian, retry_reason.as_ref(), cache.as_ref())
|
||||
&& let Some(cache) = &cache
|
||||
&& cache.is_approved_for_session(session).await
|
||||
{
|
||||
return ApprovalOutcome::new(ReviewDecision::ApprovedForSession);
|
||||
}
|
||||
|
||||
let decision = request_user_approval(session, turn, intent).await;
|
||||
let outcome = ApprovalOutcome::new(decision);
|
||||
|
||||
if let Some(cache) = &cache {
|
||||
cache.record_outcome(session, &outcome).await;
|
||||
}
|
||||
|
||||
outcome
|
||||
}
|
||||
|
||||
fn should_consult_session_cache(
|
||||
routes_to_guardian: bool,
|
||||
retry_reason: Option<&String>,
|
||||
cache: Option<&ApprovalCache>,
|
||||
) -> bool {
|
||||
!routes_to_guardian && retry_reason.is_none() && cache.is_some()
|
||||
}
|
||||
|
||||
async fn request_guardian_approval(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
request: ApprovalRequest,
|
||||
) -> ApprovalOutcome {
|
||||
let guardian_review_id = new_guardian_review_id();
|
||||
let decision = review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
guardian_review_id.clone(),
|
||||
request.intent.into_guardian_request(),
|
||||
request.retry_reason,
|
||||
)
|
||||
.await;
|
||||
ApprovalOutcome {
|
||||
decision,
|
||||
guardian_review_id: Some(guardian_review_id),
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_user_approval(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
intent: ApprovalIntent,
|
||||
) -> ReviewDecision {
|
||||
match intent.into_user_request() {
|
||||
UserApprovalRequest::Command(command) => {
|
||||
let CommandApprovalRequest {
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
} = command;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserApprovalRequest::Patch(patch) => {
|
||||
let PatchApprovalRequest {
|
||||
call_id,
|
||||
changes,
|
||||
reason,
|
||||
grant_root,
|
||||
} = patch;
|
||||
session
|
||||
.request_patch_approval(turn, call_id, changes, reason, grant_root)
|
||||
.await
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum UserApprovalRequest {
|
||||
Command(CommandApprovalRequest),
|
||||
Patch(PatchApprovalRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CommandApprovalRequest {
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
available_decisions: Option<Vec<ReviewDecision>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct PatchApprovalRequest {
|
||||
call_id: String,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
reason: Option<String>,
|
||||
grant_root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ApprovalIntent {
|
||||
fn into_guardian_request(self) -> GuardianApprovalRequest {
|
||||
match self {
|
||||
Self::Shell(shell) => GuardianApprovalRequest::Shell {
|
||||
id: shell.call_id,
|
||||
command: shell.command,
|
||||
cwd: shell.cwd,
|
||||
sandbox_permissions: shell.sandbox_permissions,
|
||||
additional_permissions: shell.additional_permissions,
|
||||
justification: shell.justification,
|
||||
},
|
||||
Self::UnifiedExec(exec) => GuardianApprovalRequest::ExecCommand {
|
||||
id: exec.call_id,
|
||||
command: exec.command,
|
||||
cwd: exec.cwd,
|
||||
sandbox_permissions: exec.sandbox_permissions,
|
||||
additional_permissions: exec.additional_permissions,
|
||||
justification: exec.justification,
|
||||
tty: exec.tty,
|
||||
},
|
||||
Self::ApplyPatch(patch) => GuardianApprovalRequest::ApplyPatch {
|
||||
id: patch.call_id,
|
||||
cwd: patch.cwd,
|
||||
files: patch.files,
|
||||
patch: patch.patch,
|
||||
},
|
||||
Self::NetworkAccess(network) => GuardianApprovalRequest::NetworkAccess {
|
||||
id: network.call_id,
|
||||
turn_id: network.turn_id,
|
||||
target: network.target,
|
||||
host: network.host,
|
||||
protocol: network.protocol,
|
||||
port: network.port,
|
||||
},
|
||||
#[cfg(unix)]
|
||||
Self::Execve(execve) => GuardianApprovalRequest::Execve {
|
||||
id: execve.call_id,
|
||||
source: execve.source,
|
||||
program: execve.program,
|
||||
argv: execve.argv,
|
||||
cwd: execve.cwd,
|
||||
additional_permissions: execve.additional_permissions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_user_request(self) -> UserApprovalRequest {
|
||||
match self {
|
||||
Self::Shell(shell) => UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: shell.call_id,
|
||||
approval_id: None,
|
||||
command: shell.command,
|
||||
cwd: shell.cwd,
|
||||
reason: shell.reason,
|
||||
network_approval_context: shell.network_approval_context,
|
||||
proposed_execpolicy_amendment: shell.proposed_execpolicy_amendment,
|
||||
additional_permissions: shell.additional_permissions,
|
||||
available_decisions: None,
|
||||
}),
|
||||
Self::UnifiedExec(exec) => UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: exec.call_id,
|
||||
approval_id: None,
|
||||
command: exec.command,
|
||||
cwd: exec.cwd,
|
||||
reason: exec.reason,
|
||||
network_approval_context: exec.network_approval_context,
|
||||
proposed_execpolicy_amendment: exec.proposed_execpolicy_amendment,
|
||||
additional_permissions: exec.additional_permissions,
|
||||
available_decisions: None,
|
||||
}),
|
||||
Self::ApplyPatch(patch) => UserApprovalRequest::Patch(PatchApprovalRequest {
|
||||
call_id: patch.call_id,
|
||||
changes: patch.changes,
|
||||
reason: patch.reason,
|
||||
grant_root: patch.grant_root,
|
||||
}),
|
||||
Self::NetworkAccess(network) => UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: network.call_id,
|
||||
approval_id: None,
|
||||
command: network.command,
|
||||
cwd: network.cwd,
|
||||
reason: network.reason,
|
||||
network_approval_context: Some(network.network_approval_context),
|
||||
proposed_execpolicy_amendment: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
}),
|
||||
#[cfg(unix)]
|
||||
Self::Execve(execve) => UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: execve.call_id,
|
||||
approval_id: execve.approval_id,
|
||||
command: execve.command,
|
||||
cwd: execve.cwd,
|
||||
reason: execve.reason,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
additional_permissions: execve.additional_permissions,
|
||||
available_decisions: Some(execve.available_decisions),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "approval_router_tests.rs"]
|
||||
mod tests;
|
||||
157
codex-rs/core/src/tools/approval_router_tests.rs
Normal file
157
codex-rs/core/src/tools/approval_router_tests.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use super::*;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use core_test_support::PathBufExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn guardian_route_bypasses_session_cache() {
|
||||
let (_session, mut turn) = crate::codex::make_session_and_context().await;
|
||||
let mut config = (*turn.config).clone();
|
||||
config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent;
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let request = ApprovalRequest {
|
||||
intent: ApprovalIntent::Shell(ShellApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
command: vec!["echo".to_string(), "hello".to_string()],
|
||||
cwd: turn.cwd.to_path_buf(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("need output".to_string()),
|
||||
reason: Some("need output".to_string()),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
}),
|
||||
retry_reason: None,
|
||||
cache: Some(ApprovalCache::new("shell", vec!["cached-key"])),
|
||||
};
|
||||
|
||||
assert!(!should_consult_session_cache(
|
||||
routes_approval_to_guardian(&turn),
|
||||
request.retry_reason.as_ref(),
|
||||
request.cache.as_ref(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_intent_translates_to_guardian_and_user_requests() {
|
||||
let shell = ShellApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
cwd: PathBuf::from("/repo"),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("inspect repo".to_string()),
|
||||
reason: Some("inspect repo".to_string()),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ApprovalIntent::Shell(shell.clone()).into_guardian_request(),
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: "call-1".to_string(),
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
cwd: PathBuf::from("/repo"),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("inspect repo".to_string()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ApprovalIntent::Shell(shell).into_user_request(),
|
||||
UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
approval_id: None,
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
cwd: PathBuf::from("/repo"),
|
||||
reason: Some("inspect repo".to_string()),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_intent_translates_to_guardian_and_user_requests() {
|
||||
let path = std::env::temp_dir()
|
||||
.join("approval-router-apply-patch.txt")
|
||||
.abs();
|
||||
let patch = ApplyPatchApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
cwd: path.parent().expect("parent").to_path_buf(),
|
||||
files: vec![path.clone()],
|
||||
patch: "*** Begin Patch\n*** Add File: approval-router-apply-patch.txt\n+hello\n*** End Patch\n"
|
||||
.to_string(),
|
||||
changes: HashMap::from([(
|
||||
path.to_path_buf(),
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)]),
|
||||
reason: Some("apply patch".to_string()),
|
||||
grant_root: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ApprovalIntent::ApplyPatch(patch.clone()).into_guardian_request(),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: "call-1".to_string(),
|
||||
cwd: patch.cwd.clone(),
|
||||
files: vec![path.clone()],
|
||||
patch: patch.patch.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
ApprovalIntent::ApplyPatch(patch).into_user_request(),
|
||||
UserApprovalRequest::Patch(PatchApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
changes: HashMap::from([(
|
||||
path.to_path_buf(),
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)]),
|
||||
reason: Some("apply patch".to_string()),
|
||||
grant_root: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn execve_intent_preserves_available_decisions_for_user_prompt() {
|
||||
let execve = ExecveApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
approval_id: Some("approval-1".to_string()),
|
||||
source: codex_protocol::approvals::GuardianCommandSource::Shell,
|
||||
program: "/bin/ls".to_string(),
|
||||
argv: vec!["-l".to_string()],
|
||||
command: vec!["/bin/ls".to_string(), "-l".to_string()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
additional_permissions: None,
|
||||
reason: None,
|
||||
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ApprovalIntent::Execve(execve).into_user_request(),
|
||||
UserApprovalRequest::Command(CommandApprovalRequest {
|
||||
call_id: "call-1".to_string(),
|
||||
approval_id: Some("approval-1".to_string()),
|
||||
command: vec!["/bin/ls".to_string(), "-l".to_string()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: Some(vec![ReviewDecision::Approved, ReviewDecision::Abort,]),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub(crate) mod approval_router;
|
||||
pub(crate) mod code_mode;
|
||||
pub(crate) mod context;
|
||||
pub(crate) mod events;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::codex::Session;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::denied_network_policy_message;
|
||||
use crate::tools::approval_router::ApprovalIntent;
|
||||
use crate::tools::approval_router::ApprovalRequest;
|
||||
use crate::tools::approval_router::NetworkAccessApprovalRequest;
|
||||
use crate::tools::approval_router::request_approval;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_network_proxy::BlockedRequest;
|
||||
use codex_network_proxy::BlockedRequestObserver;
|
||||
@@ -371,15 +371,13 @@ impl NetworkApprovalService {
|
||||
};
|
||||
let owner_call = self.resolve_single_active_call().await;
|
||||
let guardian_approval_id = Self::approval_id_for_key(&key);
|
||||
let use_guardian = routes_approval_to_guardian(&turn_context);
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_decision = if let Some(review_id) = guardian_review_id.clone() {
|
||||
review_approval_request(
|
||||
&session,
|
||||
&turn_context,
|
||||
review_id,
|
||||
GuardianApprovalRequest::NetworkAccess {
|
||||
id: guardian_approval_id.clone(),
|
||||
let prompt_command = vec!["network-access".to_string(), target.clone()];
|
||||
let outcome = request_approval(
|
||||
&session,
|
||||
&turn_context,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::NetworkAccess(NetworkAccessApprovalRequest {
|
||||
call_id: guardian_approval_id.clone(),
|
||||
turn_id: owner_call
|
||||
.as_ref()
|
||||
.map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()),
|
||||
@@ -387,29 +385,18 @@ impl NetworkApprovalService {
|
||||
host: request.host,
|
||||
protocol,
|
||||
port: key.port,
|
||||
},
|
||||
Some(policy_denial_message.clone()),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let approval_id = Self::approval_id_for_key(&key);
|
||||
let prompt_command = vec!["network-access".to_string(), target.clone()];
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn_context.as_ref(),
|
||||
approval_id,
|
||||
/*approval_id*/ None,
|
||||
prompt_command,
|
||||
turn_context.cwd.to_path_buf(),
|
||||
Some(prompt_reason),
|
||||
Some(network_approval_context.clone()),
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
/*additional_permissions*/ None,
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
};
|
||||
command: prompt_command,
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
reason: Some(prompt_reason),
|
||||
network_approval_context: network_approval_context.clone(),
|
||||
}),
|
||||
retry_reason: Some(policy_denial_message.clone()),
|
||||
cache: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let guardian_review_id = outcome.guardian_review_id;
|
||||
let approval_decision = outcome.decision;
|
||||
|
||||
let mut cache_session_deny = false;
|
||||
let resolved = match approval_decision {
|
||||
|
||||
@@ -8,8 +8,6 @@ caching).
|
||||
*/
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::network_approval_context_from_payload;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
@@ -117,7 +115,6 @@ impl ToolOrchestrator {
|
||||
let otel_user = ToolDecisionSource::User;
|
||||
let otel_automated_reviewer = ToolDecisionSource::AutomatedReviewer;
|
||||
let otel_cfg = ToolDecisionSource::Config;
|
||||
let use_guardian = routes_approval_to_guardian(turn_ctx);
|
||||
|
||||
// 1) Approval
|
||||
let mut already_approved = false;
|
||||
@@ -133,17 +130,16 @@ impl ToolOrchestrator {
|
||||
return Err(ToolError::Rejected(reason));
|
||||
}
|
||||
ExecApprovalRequirement::NeedsApproval { reason, .. } => {
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_ctx = ApprovalCtx {
|
||||
session: &tool_ctx.session,
|
||||
turn: &tool_ctx.turn,
|
||||
call_id: &tool_ctx.call_id,
|
||||
guardian_review_id: guardian_review_id.clone(),
|
||||
retry_reason: reason,
|
||||
network_approval_context: None,
|
||||
};
|
||||
let decision = tool.start_approval_async(req, approval_ctx).await;
|
||||
let otel_source = if use_guardian {
|
||||
let outcome = tool.start_approval_async(req, approval_ctx).await;
|
||||
let decision = outcome.decision;
|
||||
let otel_source = if outcome.guardian_review_id.is_some() {
|
||||
otel_automated_reviewer.clone()
|
||||
} else {
|
||||
otel_user.clone()
|
||||
@@ -153,7 +149,8 @@ impl ToolOrchestrator {
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
let reason = if let Some(review_id) = guardian_review_id.as_deref() {
|
||||
let reason = if let Some(review_id) = outcome.guardian_review_id.as_deref()
|
||||
{
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), review_id).await
|
||||
} else {
|
||||
"rejected by user".to_string()
|
||||
@@ -291,18 +288,17 @@ impl ToolOrchestrator {
|
||||
.should_bypass_approval(approval_policy, already_approved)
|
||||
&& network_approval_context.is_none();
|
||||
if !bypass_retry_approval {
|
||||
let guardian_review_id = use_guardian.then(new_guardian_review_id);
|
||||
let approval_ctx = ApprovalCtx {
|
||||
session: &tool_ctx.session,
|
||||
turn: &tool_ctx.turn,
|
||||
call_id: &tool_ctx.call_id,
|
||||
guardian_review_id: guardian_review_id.clone(),
|
||||
retry_reason: Some(retry_reason),
|
||||
network_approval_context: network_approval_context.clone(),
|
||||
};
|
||||
|
||||
let decision = tool.start_approval_async(req, approval_ctx).await;
|
||||
let otel_source = if use_guardian {
|
||||
let outcome = tool.start_approval_async(req, approval_ctx).await;
|
||||
let decision = outcome.decision;
|
||||
let otel_source = if outcome.guardian_review_id.is_some() {
|
||||
otel_automated_reviewer
|
||||
} else {
|
||||
otel_user
|
||||
@@ -311,12 +307,13 @@ impl ToolOrchestrator {
|
||||
|
||||
match decision {
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
let reason = if let Some(review_id) = guardian_review_id.as_deref() {
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), review_id)
|
||||
.await
|
||||
} else {
|
||||
"rejected by user".to_string()
|
||||
};
|
||||
let reason =
|
||||
if let Some(review_id) = outcome.guardian_review_id.as_deref() {
|
||||
guardian_rejection_message(tool_ctx.session.as_ref(), review_id)
|
||||
.await
|
||||
} else {
|
||||
"rejected by user".to_string()
|
||||
};
|
||||
return Err(ToolError::Rejected(reason));
|
||||
}
|
||||
ReviewDecision::TimedOut => {
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
//! for `codex --codex-run-as-apply-patch` and runs it under the current
|
||||
//! `SandboxAttempt` with a minimal environment for local turns.
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::tools::approval_router::ApplyPatchApprovalRequest;
|
||||
use crate::tools::approval_router::ApprovalCache;
|
||||
use crate::tools::approval_router::ApprovalIntent;
|
||||
use crate::tools::approval_router::ApprovalOutcome;
|
||||
use crate::tools::approval_router::ApprovalRequest;
|
||||
use crate::tools::approval_router::request_approval;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
@@ -18,7 +22,6 @@ use crate::tools::sandboxing::Sandboxable;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -54,18 +57,6 @@ impl ApplyPatchRuntime {
|
||||
Self
|
||||
}
|
||||
|
||||
fn build_guardian_review_request(
|
||||
req: &ApplyPatchRequest,
|
||||
call_id: &str,
|
||||
) -> GuardianApprovalRequest {
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: call_id.to_string(),
|
||||
cwd: req.action.cwd.to_path_buf(),
|
||||
files: req.file_paths.clone(),
|
||||
patch: req.action.patch.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_sandbox_command(
|
||||
req: &ApplyPatchRequest,
|
||||
@@ -129,57 +120,61 @@ impl Sandboxable for ApplyPatchRuntime {
|
||||
}
|
||||
|
||||
impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
type ApprovalKey = AbsolutePathBuf;
|
||||
|
||||
fn approval_keys(&self, req: &ApplyPatchRequest) -> Vec<Self::ApprovalKey> {
|
||||
req.file_paths.clone()
|
||||
fn approval_cache(&self, req: &ApplyPatchRequest) -> Option<ApprovalCache> {
|
||||
Some(ApprovalCache::new("apply_patch", req.file_paths.clone()))
|
||||
}
|
||||
|
||||
fn start_approval_async<'a>(
|
||||
&'a mut self,
|
||||
req: &'a ApplyPatchRequest,
|
||||
ctx: ApprovalCtx<'a>,
|
||||
) -> BoxFuture<'a, ReviewDecision> {
|
||||
) -> BoxFuture<'a, ApprovalOutcome> {
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let approval_keys = self.approval_keys(req);
|
||||
let changes = req.changes.clone();
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
let cache = self.approval_cache(req);
|
||||
Box::pin(async move {
|
||||
if req.permissions_preapproved && retry_reason.is_none() {
|
||||
return ReviewDecision::Approved;
|
||||
}
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id);
|
||||
return review_approval_request(session, turn, review_id, action, retry_reason)
|
||||
.await;
|
||||
return ApprovalOutcome::new(ReviewDecision::Approved);
|
||||
}
|
||||
if let Some(reason) = retry_reason {
|
||||
let rx_approve = session
|
||||
.request_patch_approval(
|
||||
turn,
|
||||
call_id,
|
||||
changes.clone(),
|
||||
Some(reason),
|
||||
/*grant_root*/ None,
|
||||
)
|
||||
.await;
|
||||
return rx_approve.await.unwrap_or_default();
|
||||
return request_approval(
|
||||
session,
|
||||
turn,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
call_id,
|
||||
cwd: req.action.cwd.to_path_buf(),
|
||||
files: req.file_paths.clone(),
|
||||
patch: req.action.patch.clone(),
|
||||
changes: changes.clone(),
|
||||
reason: Some(reason),
|
||||
grant_root: None,
|
||||
}),
|
||||
retry_reason: ctx.retry_reason.clone(),
|
||||
cache: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
with_cached_approval(
|
||||
&session.services,
|
||||
"apply_patch",
|
||||
approval_keys,
|
||||
|| async move {
|
||||
let rx_approve = session
|
||||
.request_patch_approval(
|
||||
turn, call_id, changes, /*reason*/ None, /*grant_root*/ None,
|
||||
)
|
||||
.await;
|
||||
rx_approve.await.unwrap_or_default()
|
||||
request_approval(
|
||||
session,
|
||||
turn,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
call_id,
|
||||
cwd: req.action.cwd.to_path_buf(),
|
||||
files: req.file_paths.clone(),
|
||||
patch: req.action.patch.clone(),
|
||||
changes,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
retry_reason: None,
|
||||
cache,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -30,45 +30,6 @@ fn wants_no_sandbox_approval_granular_respects_sandbox_flag() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_request_includes_patch_context() {
|
||||
let path = std::env::temp_dir()
|
||||
.join("guardian-apply-patch-test.txt")
|
||||
.abs();
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let expected_cwd = action.cwd.to_path_buf();
|
||||
let expected_patch = action.patch.clone();
|
||||
let request = ApplyPatchRequest {
|
||||
action,
|
||||
file_paths: vec![path.clone()],
|
||||
changes: HashMap::from([(
|
||||
path.to_path_buf(),
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)]),
|
||||
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
},
|
||||
additional_permissions: None,
|
||||
permissions_preapproved: false,
|
||||
timeout_ms: None,
|
||||
};
|
||||
|
||||
let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1");
|
||||
|
||||
assert_eq!(
|
||||
guardian_request,
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: "call-1".to_string(),
|
||||
cwd: expected_cwd,
|
||||
files: request.file_paths,
|
||||
patch: expected_patch,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[test]
|
||||
fn build_sandbox_command_prefers_configured_codex_self_exe_for_apply_patch() {
|
||||
|
||||
@@ -10,12 +10,16 @@ pub(crate) mod zsh_fork_backend;
|
||||
|
||||
use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::approval_router::ApprovalCache;
|
||||
use crate::tools::approval_router::ApprovalIntent;
|
||||
use crate::tools::approval_router::ApprovalOutcome;
|
||||
use crate::tools::approval_router::ApprovalRequest;
|
||||
use crate::tools::approval_router::ShellApprovalRequest;
|
||||
use crate::tools::approval_router::request_approval;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -30,11 +34,9 @@ use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -127,68 +129,54 @@ impl Sandboxable for ShellRuntime {
|
||||
}
|
||||
|
||||
impl Approvable<ShellRequest> for ShellRuntime {
|
||||
type ApprovalKey = ApprovalKey;
|
||||
|
||||
fn approval_keys(&self, req: &ShellRequest) -> Vec<Self::ApprovalKey> {
|
||||
vec![ApprovalKey {
|
||||
command: canonicalize_command_for_approval(&req.command),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
}]
|
||||
fn approval_cache(&self, req: &ShellRequest) -> Option<ApprovalCache> {
|
||||
Some(ApprovalCache::new(
|
||||
"shell",
|
||||
vec![ApprovalKey {
|
||||
command: canonicalize_command_for_approval(&req.command),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
}],
|
||||
))
|
||||
}
|
||||
|
||||
fn start_approval_async<'a>(
|
||||
&'a mut self,
|
||||
req: &'a ShellRequest,
|
||||
ctx: ApprovalCtx<'a>,
|
||||
) -> BoxFuture<'a, ReviewDecision> {
|
||||
let keys = self.approval_keys(req);
|
||||
) -> BoxFuture<'a, ApprovalOutcome> {
|
||||
let command = req.command.clone();
|
||||
let cwd = req.cwd.to_path_buf();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let cache = self.approval_cache(req);
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: call_id,
|
||||
request_approval(
|
||||
session,
|
||||
turn,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::Shell(ShellApprovalRequest {
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
},
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
with_cached_approval(&session.services, "shell", keys, move || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
network_approval_context: ctx.network_approval_context.clone(),
|
||||
proposed_execpolicy_amendment: req
|
||||
.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}),
|
||||
retry_reason,
|
||||
cache,
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ use super::ShellRequest;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::approval_router::ApprovalIntent;
|
||||
use crate::tools::approval_router::ApprovalRequest;
|
||||
use crate::tools::approval_router::ExecveApprovalRequest;
|
||||
use crate::tools::approval_router::request_approval;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
@@ -391,47 +391,35 @@ impl CoreShellActionProvider {
|
||||
let call_id = self.call_id.clone();
|
||||
let approval_id = Some(Uuid::new_v4().to_string());
|
||||
let source = self.tool_name;
|
||||
let guardian_review_id = routes_approval_to_guardian(&turn).then(new_guardian_review_id);
|
||||
Ok(stopwatch
|
||||
.pause_for(async move {
|
||||
if let Some(review_id) = guardian_review_id.clone() {
|
||||
let decision = review_approval_request(
|
||||
&session,
|
||||
&turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::Execve {
|
||||
id: call_id.clone(),
|
||||
let outcome = request_approval(
|
||||
&session,
|
||||
&turn,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::Execve(ExecveApprovalRequest {
|
||||
call_id,
|
||||
approval_id,
|
||||
source,
|
||||
program: program.to_string_lossy().into_owned(),
|
||||
argv: argv.to_vec(),
|
||||
cwd: workdir,
|
||||
additional_permissions,
|
||||
},
|
||||
/*retry_reason*/ None,
|
||||
)
|
||||
.await;
|
||||
return PromptDecision {
|
||||
decision,
|
||||
guardian_review_id,
|
||||
};
|
||||
}
|
||||
let decision = session
|
||||
.request_command_approval(
|
||||
&turn,
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
workdir,
|
||||
/*reason*/ None,
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
additional_permissions,
|
||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
||||
)
|
||||
.await;
|
||||
command,
|
||||
cwd: workdir.clone(),
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
reason: None,
|
||||
available_decisions: vec![
|
||||
ReviewDecision::Approved,
|
||||
ReviewDecision::Abort,
|
||||
],
|
||||
}),
|
||||
retry_reason: None,
|
||||
cache: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
PromptDecision {
|
||||
decision,
|
||||
guardian_review_id: None,
|
||||
decision: outcome.decision,
|
||||
guardian_review_id: outcome.guardian_review_id,
|
||||
}
|
||||
})
|
||||
.await)
|
||||
|
||||
@@ -7,11 +7,15 @@ the process manager to spawn PTYs once an ExecRequest is prepared.
|
||||
use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::approval_router::ApprovalCache;
|
||||
use crate::tools::approval_router::ApprovalIntent;
|
||||
use crate::tools::approval_router::ApprovalOutcome;
|
||||
use crate::tools::approval_router::ApprovalRequest;
|
||||
use crate::tools::approval_router::UnifiedExecApprovalRequest;
|
||||
use crate::tools::approval_router::request_approval;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
@@ -27,7 +31,6 @@ use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
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;
|
||||
@@ -36,7 +39,6 @@ use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
@@ -102,24 +104,24 @@ impl Sandboxable for UnifiedExecRuntime<'_> {
|
||||
}
|
||||
|
||||
impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
type ApprovalKey = UnifiedExecApprovalKey;
|
||||
|
||||
fn approval_keys(&self, req: &UnifiedExecRequest) -> Vec<Self::ApprovalKey> {
|
||||
vec![UnifiedExecApprovalKey {
|
||||
command: canonicalize_command_for_approval(&req.command),
|
||||
cwd: req.cwd.clone(),
|
||||
tty: req.tty,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
}]
|
||||
fn approval_cache(&self, req: &UnifiedExecRequest) -> Option<ApprovalCache> {
|
||||
Some(ApprovalCache::new(
|
||||
"unified_exec",
|
||||
vec![UnifiedExecApprovalKey {
|
||||
command: canonicalize_command_for_approval(&req.command),
|
||||
cwd: req.cwd.clone(),
|
||||
tty: req.tty,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
}],
|
||||
))
|
||||
}
|
||||
|
||||
fn start_approval_async<'b>(
|
||||
&'b mut self,
|
||||
req: &'b UnifiedExecRequest,
|
||||
ctx: ApprovalCtx<'b>,
|
||||
) -> BoxFuture<'b, ReviewDecision> {
|
||||
let keys = self.approval_keys(req);
|
||||
) -> BoxFuture<'b, ApprovalOutcome> {
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
@@ -127,45 +129,31 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
let cwd = req.cwd.to_path_buf();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
let cache = self.approval_cache(req);
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::ExecCommand {
|
||||
id: call_id,
|
||||
request_approval(
|
||||
session,
|
||||
turn,
|
||||
ApprovalRequest {
|
||||
intent: ApprovalIntent::UnifiedExec(UnifiedExecApprovalRequest {
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
tty: req.tty,
|
||||
},
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
with_cached_approval(&session.services, "unified_exec", keys, || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
network_approval_context: ctx.network_approval_context.clone(),
|
||||
proposed_execpolicy_amendment: req
|
||||
.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}),
|
||||
retry_reason,
|
||||
cache,
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::state::SessionServices;
|
||||
use crate::tools::approval_router::ApprovalCache;
|
||||
use crate::tools::approval_router::ApprovalOutcome;
|
||||
use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
@@ -27,12 +28,9 @@ use codex_sandboxing::SandboxTransformError;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use futures::Future;
|
||||
use futures::future::BoxFuture;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::Hash;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -48,7 +46,7 @@ impl ApprovalStore {
|
||||
K: Serialize,
|
||||
{
|
||||
let s = serde_json::to_string(key).ok()?;
|
||||
self.map.get(&s).cloned()
|
||||
self.get_serialized(&s)
|
||||
}
|
||||
|
||||
pub fn put<K>(&mut self, key: K, value: ReviewDecision)
|
||||
@@ -56,63 +54,17 @@ impl ApprovalStore {
|
||||
K: Serialize,
|
||||
{
|
||||
if let Ok(s) = serde_json::to_string(&key) {
|
||||
self.map.insert(s, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a vector of approval keys and returns a ReviewDecision.
|
||||
/// There will be one key in most cases, but apply_patch can modify multiple files at once.
|
||||
///
|
||||
/// - If all keys are already approved for session, we skip prompting.
|
||||
/// - If the user approves for session, we store the decision for each key individually
|
||||
/// so future requests touching any subset can also skip prompting.
|
||||
pub(crate) async fn with_cached_approval<K, F, Fut>(
|
||||
services: &SessionServices,
|
||||
// Name of the tool, used for metrics collection.
|
||||
tool_name: &str,
|
||||
keys: Vec<K>,
|
||||
fetch: F,
|
||||
) -> ReviewDecision
|
||||
where
|
||||
K: Serialize,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = ReviewDecision>,
|
||||
{
|
||||
// To be defensive here, don't bother with checking the cache if keys are empty.
|
||||
if keys.is_empty() {
|
||||
return fetch().await;
|
||||
}
|
||||
|
||||
let already_approved = {
|
||||
let store = services.tool_approvals.lock().await;
|
||||
keys.iter()
|
||||
.all(|key| matches!(store.get(key), Some(ReviewDecision::ApprovedForSession)))
|
||||
};
|
||||
|
||||
if already_approved {
|
||||
return ReviewDecision::ApprovedForSession;
|
||||
}
|
||||
|
||||
let decision = fetch().await;
|
||||
|
||||
services.session_telemetry.counter(
|
||||
"codex.approval.requested",
|
||||
/*inc*/ 1,
|
||||
&[
|
||||
("tool", tool_name),
|
||||
("approved", decision.to_opaque_string()),
|
||||
],
|
||||
);
|
||||
|
||||
if matches!(decision, ReviewDecision::ApprovedForSession) {
|
||||
let mut store = services.tool_approvals.lock().await;
|
||||
for key in keys {
|
||||
store.put(key, ReviewDecision::ApprovedForSession);
|
||||
self.put_serialized(s, value);
|
||||
}
|
||||
}
|
||||
|
||||
decision
|
||||
pub fn get_serialized(&self, key: &str) -> Option<ReviewDecision> {
|
||||
self.map.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn put_serialized(&mut self, key: String, value: ReviewDecision) {
|
||||
self.map.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -120,13 +72,6 @@ pub(crate) struct ApprovalCtx<'a> {
|
||||
pub session: &'a Arc<Session>,
|
||||
pub turn: &'a Arc<TurnContext>,
|
||||
pub call_id: &'a str,
|
||||
/// Guardian review lifecycle ID for this approval, when guardian is reviewing it.
|
||||
///
|
||||
/// This is separate from `call_id`: `call_id` identifies the tool item under
|
||||
/// review, while this ID identifies the review itself. Keeping both lets
|
||||
/// denial handling, overrides, and app-server notifications refer to the
|
||||
/// review without overloading the tool call ID as a review ID.
|
||||
pub guardian_review_id: Option<String>,
|
||||
pub retry_reason: Option<String>,
|
||||
pub network_approval_context: Option<NetworkApprovalContext>,
|
||||
}
|
||||
@@ -241,16 +186,9 @@ pub(crate) fn sandbox_override_for_first_attempt(
|
||||
}
|
||||
|
||||
pub(crate) trait Approvable<Req> {
|
||||
type ApprovalKey: Hash + Eq + Clone + Debug + Serialize;
|
||||
|
||||
// In most cases (shell, unified_exec), a request will have a single approval key.
|
||||
//
|
||||
// However, apply_patch needs session "Allow, don't ask again" semantics that
|
||||
// apply to multiple atomic targets (e.g., apply_patch approves per file path). Returning
|
||||
// a list of keys lets the runtime treat the request as approved-for-session only if
|
||||
// *all* keys are already approved, while still caching approvals per-key so future
|
||||
// requests touching a subset can be auto-approved.
|
||||
fn approval_keys(&self, req: &Req) -> Vec<Self::ApprovalKey>;
|
||||
fn approval_cache(&self, _req: &Req) -> Option<ApprovalCache> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Some tools may request to skip the sandbox on the first attempt
|
||||
/// (e.g., when the request explicitly asks for escalated permissions).
|
||||
@@ -288,7 +226,7 @@ pub(crate) trait Approvable<Req> {
|
||||
&'a mut self,
|
||||
req: &'a Req,
|
||||
ctx: ApprovalCtx<'a>,
|
||||
) -> BoxFuture<'a, ReviewDecision>;
|
||||
) -> BoxFuture<'a, ApprovalOutcome>;
|
||||
}
|
||||
|
||||
pub(crate) trait Sandboxable {
|
||||
|
||||
Reference in New Issue
Block a user