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:
Michael Bolin
2025-11-19 13:55:29 -08:00
committed by GitHub
parent c2ec477d93
commit 056c8f8279
9 changed files with 266 additions and 56 deletions

View File

@@ -64,25 +64,17 @@ use tracing_subscriber::{self};
use crate::posix::escalate_protocol::EscalateAction;
use crate::posix::escalate_server::EscalateServer;
use crate::posix::escalation_policy::EscalationPolicy;
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;
mod escalate_client;
mod escalate_protocol;
mod escalate_server;
mod escalation_policy;
mod mcp;
mod mcp_escalation_policy;
mod socket;
fn dummy_exec_policy(file: &Path, argv: &[String], _workdir: &Path) -> EscalateAction {
// TODO: execpolicy
if file == Path::new("/opt/homebrew/bin/gh")
&& let [_, arg1, arg2, ..] = argv
&& arg1 == "issue"
&& arg2 == "list"
{
return EscalateAction::Escalate;
}
EscalateAction::Run
}
#[derive(Parser)]
#[command(version)]
pub struct Cli {
@@ -135,7 +127,7 @@ pub async fn main() -> anyhow::Result<()> {
}
Some(Commands::ShellExec(args)) => {
let bash_path = mcp::get_bash_path()?;
let escalate_server = EscalateServer::new(bash_path, dummy_exec_policy);
let escalate_server = EscalateServer::new(bash_path, DummyEscalationPolicy {});
let result = escalate_server
.exec(
args.command.clone(),
@@ -162,3 +154,59 @@ pub async fn main() -> anyhow::Result<()> {
}
}
}
// TODO: replace with execpolicy2
struct DummyEscalationPolicy;
#[async_trait::async_trait]
impl EscalationPolicy for DummyEscalationPolicy {
async fn determine_action(
&self,
file: &Path,
argv: &[String],
workdir: &Path,
) -> Result<EscalateAction, rmcp::ErrorData> {
let outcome = dummy_exec_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::Forbidden => EscalateAction::Deny {
reason: Some("Execution forbidden by policy".to_string()),
},
ExecPolicyOutcome::Prompt { .. } => EscalateAction::Deny {
reason: Some("Could not prompt user for permission".to_string()),
},
};
Ok(action)
}
}
fn dummy_exec_policy(file: &Path, argv: &[String], _workdir: &Path) -> ExecPolicyOutcome {
if file.ends_with("/rm") {
ExecPolicyOutcome::Forbidden
} else if file.ends_with("/git") {
ExecPolicyOutcome::Prompt {
run_with_escalated_permissions: false,
}
} else if file == Path::new("/opt/homebrew/bin/gh")
&& let [_, arg1, arg2, ..] = argv
&& arg1 == "issue"
&& arg2 == "list"
{
ExecPolicyOutcome::Allow {
run_with_escalated_permissions: true,
}
} else {
ExecPolicyOutcome::Allow {
run_with_escalated_permissions: false,
}
}
}