mirror of
https://github.com/openai/codex.git
synced 2026-05-01 03:42:05 +03:00
fix: prepare ExecPolicy in exec-server for execpolicy2 cutover (#6888)
This PR introduces an extra layer of abstraction to prepare us for the migration to execpolicy2: - introduces a new trait, `EscalationPolicy`, whose `determine_action()` method is responsible for producing the `EscalateAction` - the existing `ExecPolicy` typedef is changed to return an intermediate `ExecPolicyOutcome` instead of `EscalateAction` - the default implementation of `EscalationPolicy`, `McpEscalationPolicy`, composes `ExecPolicy` - the `ExecPolicyOutcome` includes `codex_execpolicy2::Decision`, which has a `Prompt` variant - when `McpEscalationPolicy` gets `Decision::Prompt` back from `ExecPolicy`, it prompts the user via an MCP elicitation and maps the result into an `ElicitationAction` - now that the end user can reply to an elicitation with `Decline` or `Cancel`, we introduce a new variant, `EscalateAction::Deny`, which the client handles by returning exit code `1` without running anything Note the way the elicitation is created is still not quite right, but I will fix that once we have things running end-to-end for real in a follow-up PR.
This commit is contained in:
117
codex-rs/exec-server/src/posix/mcp_escalation_policy.rs
Normal file
117
codex-rs/exec-server/src/posix/mcp_escalation_policy.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rmcp::ErrorData as McpError;
|
||||
use rmcp::RoleServer;
|
||||
use rmcp::model::CreateElicitationRequestParam;
|
||||
use rmcp::model::CreateElicitationResult;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationSchema;
|
||||
use rmcp::model::PrimitiveSchema;
|
||||
use rmcp::model::StringSchema;
|
||||
use rmcp::service::RequestContext;
|
||||
|
||||
use crate::posix::escalate_protocol::EscalateAction;
|
||||
use crate::posix::escalation_policy::EscalationPolicy;
|
||||
|
||||
/// This is the policy which decides how to handle an exec() call.
|
||||
///
|
||||
/// `file` is the absolute, canonical path to the executable to run, i.e. the first arg to exec.
|
||||
/// `argv` is the argv, including the program name (`argv[0]`).
|
||||
/// `workdir` is the absolute, canonical path to the working directory in which to execute the
|
||||
/// command.
|
||||
pub(crate) type ExecPolicy = fn(file: &Path, argv: &[String], workdir: &Path) -> ExecPolicyOutcome;
|
||||
|
||||
pub(crate) enum ExecPolicyOutcome {
|
||||
Allow {
|
||||
run_with_escalated_permissions: bool,
|
||||
},
|
||||
Prompt {
|
||||
run_with_escalated_permissions: bool,
|
||||
},
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
/// ExecPolicy with access to the MCP RequestContext so that it can leverage
|
||||
/// elicitations.
|
||||
pub(crate) struct McpEscalationPolicy {
|
||||
policy: ExecPolicy,
|
||||
context: RequestContext<RoleServer>,
|
||||
}
|
||||
|
||||
impl McpEscalationPolicy {
|
||||
pub(crate) fn new(policy: ExecPolicy, context: RequestContext<RoleServer>) -> Self {
|
||||
Self { policy, context }
|
||||
}
|
||||
|
||||
async fn prompt(
|
||||
&self,
|
||||
_file: &Path,
|
||||
argv: &[String],
|
||||
workdir: &Path,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<CreateElicitationResult, McpError> {
|
||||
let command = shlex::try_join(argv.iter().map(String::as_str)).unwrap_or_default();
|
||||
context
|
||||
.peer
|
||||
.create_elicitation(CreateElicitationRequestParam {
|
||||
message: format!("Allow Codex to run `{command:?}` in `{workdir:?}`?"),
|
||||
#[allow(clippy::expect_used)]
|
||||
requested_schema: ElicitationSchema::builder()
|
||||
.property("dummy", PrimitiveSchema::String(StringSchema::new()))
|
||||
.build()
|
||||
.expect("failed to build elicitation schema"),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(e.to_string(), None))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EscalationPolicy for McpEscalationPolicy {
|
||||
async fn determine_action(
|
||||
&self,
|
||||
file: &Path,
|
||||
argv: &[String],
|
||||
workdir: &Path,
|
||||
) -> Result<EscalateAction, rmcp::ErrorData> {
|
||||
let outcome = (self.policy)(file, argv, workdir);
|
||||
let action = match outcome {
|
||||
ExecPolicyOutcome::Allow {
|
||||
run_with_escalated_permissions,
|
||||
} => {
|
||||
if run_with_escalated_permissions {
|
||||
EscalateAction::Escalate
|
||||
} else {
|
||||
EscalateAction::Run
|
||||
}
|
||||
}
|
||||
ExecPolicyOutcome::Prompt {
|
||||
run_with_escalated_permissions,
|
||||
} => {
|
||||
let result = self
|
||||
.prompt(file, argv, workdir, self.context.clone())
|
||||
.await?;
|
||||
// TODO: Extract reason from `result.content`.
|
||||
match result.action {
|
||||
ElicitationAction::Accept => {
|
||||
if run_with_escalated_permissions {
|
||||
EscalateAction::Escalate
|
||||
} else {
|
||||
EscalateAction::Run
|
||||
}
|
||||
}
|
||||
ElicitationAction::Decline => EscalateAction::Deny {
|
||||
reason: Some("User declined execution".to_string()),
|
||||
},
|
||||
ElicitationAction::Cancel => EscalateAction::Deny {
|
||||
reason: Some("User cancelled execution".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
ExecPolicyOutcome::Forbidden => EscalateAction::Deny {
|
||||
reason: Some("Execution forbidden by policy".to_string()),
|
||||
},
|
||||
};
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user