Skip to content

Commit 01fa4f0

Browse files
authored
core: remove special execve handling for skill scripts (#15812)
1 parent 6dcac41 commit 01fa4f0

File tree

7 files changed

+33
-1071
lines changed

7 files changed

+33
-1071
lines changed

codex-rs/core/src/codex.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1870,7 +1870,6 @@ impl Session {
18701870
session_telemetry,
18711871
models_manager: Arc::clone(&models_manager),
18721872
tool_approvals: Mutex::new(ApprovalStore::default()),
1873-
execve_session_approvals: RwLock::new(HashMap::new()),
18741873
skills_manager,
18751874
plugins_manager: Arc::clone(&plugins_manager),
18761875
mcp_manager: Arc::clone(&mcp_manager),

codex-rs/core/src/codex_tests.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,7 +2683,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
26832683
session_telemetry: session_telemetry.clone(),
26842684
models_manager: Arc::clone(&models_manager),
26852685
tool_approvals: Mutex::new(ApprovalStore::default()),
2686-
execve_session_approvals: RwLock::new(HashMap::new()),
26872686
skills_manager,
26882687
plugins_manager,
26892688
mcp_manager,
@@ -3521,7 +3520,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
35213520
session_telemetry: session_telemetry.clone(),
35223521
models_manager: Arc::clone(&models_manager),
35233522
tool_approvals: Mutex::new(ApprovalStore::default()),
3524-
execve_session_approvals: RwLock::new(HashMap::new()),
35253523
skills_manager,
35263524
plugins_manager,
35273525
mcp_manager,

codex-rs/core/src/state/service.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::collections::HashMap;
21
use std::sync::Arc;
32

43
use crate::AuthManager;
@@ -16,14 +15,12 @@ use crate::skills_watcher::SkillsWatcher;
1615
use crate::state_db::StateDbHandle;
1716
use crate::tools::code_mode::CodeModeService;
1817
use crate::tools::network_approval::NetworkApprovalService;
19-
use crate::tools::runtimes::ExecveSessionApproval;
2018
use crate::tools::sandboxing::ApprovalStore;
2119
use crate::unified_exec::UnifiedExecProcessManager;
2220
use codex_analytics::AnalyticsEventsClient;
2321
use codex_exec_server::Environment;
2422
use codex_hooks::Hooks;
2523
use codex_otel::SessionTelemetry;
26-
use codex_utils_absolute_path::AbsolutePathBuf;
2724
use std::path::PathBuf;
2825
use tokio::sync::Mutex;
2926
use tokio::sync::RwLock;
@@ -49,8 +46,6 @@ pub(crate) struct SessionServices {
4946
pub(crate) models_manager: Arc<ModelsManager>,
5047
pub(crate) session_telemetry: SessionTelemetry,
5148
pub(crate) tool_approvals: Mutex<ApprovalStore>,
52-
#[cfg_attr(not(unix), allow(dead_code))]
53-
pub(crate) execve_session_approvals: RwLock<HashMap<AbsolutePathBuf, ExecveSessionApproval>>,
5449
pub(crate) skills_manager: Arc<SkillsManager>,
5550
pub(crate) plugins_manager: Arc<PluginsManager>,
5651
pub(crate) mcp_manager: Arc<McpManager>,

codex-rs/core/src/tools/runtimes/mod.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Module: runtimes
44
Concrete ToolRuntime implementations for specific tools. Each runtime stays
55
small and focused and reuses the orchestrator for approvals + sandbox + retry.
66
*/
7-
use crate::SkillMetadata;
87
use crate::path_utils;
98
use crate::shell::Shell;
109
use crate::tools::sandboxing::ToolError;
@@ -17,14 +16,6 @@ pub mod apply_patch;
1716
pub mod shell;
1817
pub mod unified_exec;
1918

20-
#[derive(Debug, Clone)]
21-
pub(crate) struct ExecveSessionApproval {
22-
/// If this execve session approval is associated with a skill script, this
23-
/// field contains metadata about the skill.
24-
#[cfg_attr(not(unix), allow(dead_code))]
25-
pub skill: Option<SkillMetadata>,
26-
}
27-
2819
/// Shared helper to construct sandbox transform inputs from a tokenized command line.
2920
/// Validates that at least a program is present.
3021
pub(crate) fn build_sandbox_command(

codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs

Lines changed: 4 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use super::ShellRequest;
2-
use crate::SkillMetadata;
32
use crate::error::CodexErr;
43
use crate::error::SandboxErr;
54
use crate::exec::ExecCapturePolicy;
@@ -13,8 +12,6 @@ use crate::sandboxing::ExecOptions;
1312
use crate::sandboxing::ExecRequest;
1413
use crate::sandboxing::SandboxPermissions;
1514
use crate::shell::ShellType;
16-
use crate::skills_load_input_from_config;
17-
use crate::tools::runtimes::ExecveSessionApproval;
1815
use crate::tools::runtimes::build_sandbox_command;
1916
use crate::tools::sandboxing::SandboxAttempt;
2017
use crate::tools::sandboxing::ToolCtx;
@@ -31,7 +28,6 @@ use codex_protocol::models::PermissionProfile;
3128
use codex_protocol::permissions::FileSystemSandboxPolicy;
3229
use codex_protocol::permissions::NetworkSandboxPolicy;
3330
use codex_protocol::protocol::AskForApproval;
34-
use codex_protocol::protocol::ExecApprovalRequestSkillMetadata;
3531
use codex_protocol::protocol::NetworkPolicyRuleAction;
3632
use codex_protocol::protocol::ReviewDecision;
3733
use codex_protocol::protocol::SandboxPolicy;
@@ -74,9 +70,6 @@ const REJECT_SANDBOX_APPROVAL_REASON: &str =
7470
"approval required by policy, but AskForApproval::Granular.sandbox_approval is false";
7571
const REJECT_RULES_APPROVAL_REASON: &str =
7672
"approval required by policy rule, but AskForApproval::Granular.rules is false";
77-
const REJECT_SKILL_APPROVAL_REASON: &str =
78-
"approval required by skill, but AskForApproval::Granular.skill_approval is false";
79-
8073
fn approval_sandbox_permissions(
8174
sandbox_permissions: SandboxPermissions,
8275
additional_permissions_preapproved: bool,
@@ -327,9 +320,6 @@ struct CoreShellActionProvider {
327320

328321
#[allow(clippy::large_enum_variant)]
329322
enum DecisionSource {
330-
SkillScript {
331-
skill: SkillMetadata,
332-
},
333323
PrefixRule,
334324
/// Often, this is `is_safe_command()`.
335325
UnmatchedCommandFallback,
@@ -341,11 +331,6 @@ fn execve_prompt_is_rejected_by_policy(
341331
) -> Option<&'static str> {
342332
match (approval_policy, decision_source) {
343333
(AskForApproval::Never, _) => Some(PROMPT_CONFLICT_REASON),
344-
(AskForApproval::Granular(granular_config), DecisionSource::SkillScript { .. })
345-
if !granular_config.allows_skill_approval() =>
346-
{
347-
Some(REJECT_SKILL_APPROVAL_REASON)
348-
}
349334
(AskForApproval::Granular(granular_config), DecisionSource::PrefixRule)
350335
if !granular_config.allows_rules_approval() =>
351336
{
@@ -397,25 +382,13 @@ impl CoreShellActionProvider {
397382
}
398383
}
399384

400-
fn skill_escalation_execution(skill: &SkillMetadata) -> EscalationExecution {
401-
let permission_profile = skill.permission_profile.clone().unwrap_or_default();
402-
if permission_profile.is_empty() {
403-
EscalationExecution::TurnDefault
404-
} else {
405-
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(
406-
permission_profile,
407-
))
408-
}
409-
}
410-
411385
async fn prompt(
412386
&self,
413387
program: &AbsolutePathBuf,
414388
argv: &[String],
415389
workdir: &AbsolutePathBuf,
416390
stopwatch: &Stopwatch,
417391
additional_permissions: Option<PermissionProfile>,
418-
decision_source: &DecisionSource,
419392
) -> anyhow::Result<ReviewDecision> {
420393
let command = join_program_and_argv(program, argv);
421394
let workdir = workdir.to_path_buf();
@@ -442,28 +415,6 @@ impl CoreShellActionProvider {
442415
)
443416
.await;
444417
}
445-
let available_decisions = vec![
446-
Some(ReviewDecision::Approved),
447-
// Currently, ApprovedForSession is only honored for skills,
448-
// so only offer it for skill script approvals.
449-
if matches!(decision_source, DecisionSource::SkillScript { .. }) {
450-
Some(ReviewDecision::ApprovedForSession)
451-
} else {
452-
None
453-
},
454-
Some(ReviewDecision::Abort),
455-
]
456-
.into_iter()
457-
.flatten()
458-
.collect();
459-
let skill_metadata = match decision_source {
460-
DecisionSource::SkillScript { skill } => {
461-
Some(ExecApprovalRequestSkillMetadata {
462-
path_to_skills_md: skill.path_to_skills_md.clone(),
463-
})
464-
}
465-
DecisionSource::PrefixRule | DecisionSource::UnmatchedCommandFallback => None,
466-
};
467418
session
468419
.request_command_approval(
469420
&turn,
@@ -475,48 +426,14 @@ impl CoreShellActionProvider {
475426
/*network_approval_context*/ None,
476427
/*proposed_execpolicy_amendment*/ None,
477428
additional_permissions,
478-
skill_metadata,
479-
Some(available_decisions),
429+
/*skill_metadata*/ None,
430+
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
480431
)
481432
.await
482433
})
483434
.await)
484435
}
485436

486-
/// Because we should be intercepting execve(2) calls, `program` should be
487-
/// an absolute path. The idea is that we check to see whether it matches
488-
/// any skills.
489-
async fn find_skill(&self, program: &AbsolutePathBuf) -> Option<SkillMetadata> {
490-
let force_reload = false;
491-
let turn_config = self.turn.config.as_ref();
492-
let plugin_outcome = self
493-
.session
494-
.services
495-
.plugins_manager
496-
.plugins_for_config(turn_config);
497-
let effective_skill_roots = plugin_outcome.effective_skill_roots();
498-
let skills_input = skills_load_input_from_config(turn_config, effective_skill_roots);
499-
let skills_outcome = self
500-
.session
501-
.services
502-
.skills_manager
503-
.skills_for_cwd(&skills_input, force_reload)
504-
.await;
505-
506-
let program_path = program.as_path();
507-
for skill in skills_outcome.skills {
508-
// We intentionally ignore "enabled" status here for now.
509-
let Some(skill_root) = skill.path_to_skills_md.parent() else {
510-
continue;
511-
};
512-
if program_path.starts_with(skill_root.join("scripts")) {
513-
return Some(skill);
514-
}
515-
}
516-
517-
None
518-
}
519-
520437
#[allow(clippy::too_many_arguments)]
521438
async fn process_decision(
522439
&self,
@@ -540,51 +457,18 @@ impl CoreShellActionProvider {
540457
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
541458
} else {
542459
match self
543-
.prompt(
544-
program,
545-
argv,
546-
workdir,
547-
&self.stopwatch,
548-
prompt_permissions,
549-
&decision_source,
550-
)
460+
.prompt(program, argv, workdir, &self.stopwatch, prompt_permissions)
551461
.await?
552462
{
553463
ReviewDecision::Approved
464+
| ReviewDecision::ApprovedForSession
554465
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
555466
if needs_escalation {
556467
EscalationDecision::escalate(escalation_execution.clone())
557468
} else {
558469
EscalationDecision::run()
559470
}
560471
}
561-
ReviewDecision::ApprovedForSession => {
562-
// Currently, we only add session approvals for
563-
// skill scripts because we are storing only the
564-
// `program` whereas prefix rules may be restricted by a longer prefix.
565-
if let DecisionSource::SkillScript { skill } = decision_source {
566-
tracing::debug!(
567-
"Adding session approval for {program:?} due to user approval of skill script {skill:?}"
568-
);
569-
self.session
570-
.services
571-
.execve_session_approvals
572-
.write()
573-
.await
574-
.insert(
575-
program.clone(),
576-
ExecveSessionApproval {
577-
skill: Some(skill.clone()),
578-
},
579-
);
580-
}
581-
582-
if needs_escalation {
583-
EscalationDecision::escalate(escalation_execution.clone())
584-
} else {
585-
EscalationDecision::run()
586-
}
587-
}
588472
ReviewDecision::NetworkPolicyAmendment {
589473
network_policy_amendment,
590474
} => match network_policy_amendment.action {
@@ -641,69 +525,6 @@ impl EscalationPolicy for CoreShellActionProvider {
641525
"Determining escalation action for command {program:?} with args {argv:?} in {workdir:?}"
642526
);
643527

644-
// Check to see whether `program` has an existing entry in
645-
// `execve_session_approvals`. If so, we can skip policy checks and user
646-
// prompts and go straight to allowing execution.
647-
let approval = {
648-
self.session
649-
.services
650-
.execve_session_approvals
651-
.read()
652-
.await
653-
.get(program)
654-
.cloned()
655-
};
656-
if let Some(approval) = approval {
657-
tracing::debug!(
658-
"Found session approval for {program:?}, allowing execution without further checks"
659-
);
660-
let execution = approval
661-
.skill
662-
.as_ref()
663-
.map(Self::skill_escalation_execution)
664-
.unwrap_or(EscalationExecution::TurnDefault);
665-
666-
return Ok(EscalationDecision::escalate(execution));
667-
}
668-
669-
// In the usual case, the execve wrapper reports the command being
670-
// executed in `program`, so a direct skill lookup is sufficient.
671-
if let Some(skill) = self.find_skill(program).await {
672-
// For now, scripts that look like they belong to skills bypass
673-
// general exec policy evaluation. Permissionless skills inherit the
674-
// turn sandbox directly; skills with declared permissions still
675-
// prompt here before applying their permission profile.
676-
let prompt_permissions = skill.permission_profile.clone();
677-
if prompt_permissions
678-
.as_ref()
679-
.is_none_or(PermissionProfile::is_empty)
680-
{
681-
tracing::debug!(
682-
"Matched {program:?} to permissionless skill {skill:?}, inheriting turn sandbox"
683-
);
684-
return Ok(EscalationDecision::escalate(
685-
EscalationExecution::TurnDefault,
686-
));
687-
}
688-
tracing::debug!("Matched {program:?} to skill {skill:?}, prompting for approval");
689-
let needs_escalation = true;
690-
let decision_source = DecisionSource::SkillScript {
691-
skill: skill.clone(),
692-
};
693-
return self
694-
.process_decision(
695-
Decision::Prompt,
696-
needs_escalation,
697-
program,
698-
argv,
699-
workdir,
700-
prompt_permissions,
701-
Self::skill_escalation_execution(&skill),
702-
decision_source,
703-
)
704-
.await;
705-
}
706-
707528
let evaluation = {
708529
let policy = self.policy.read().await;
709530
evaluate_intercepted_exec_policy(
@@ -746,7 +567,6 @@ impl EscalationPolicy for CoreShellActionProvider {
746567
.macos_seatbelt_profile_extensions
747568
.as_ref(),
748569
),
749-
DecisionSource::SkillScript { .. } => unreachable!("handled above"),
750570
};
751571
self.process_decision(
752572
evaluation.decision,

0 commit comments

Comments
 (0)