Files
codex/codex-rs/protocol/src/approvals.rs
Dylan Hurd f6053fdfb3 feat(core) Introduce Feature::RequestPermissions (#11871)
## Summary
Introduces the initial implementation of Feature::RequestPermissions.
RequestPermissions allows the model to request that a command be run
inside the sandbox, with additional permissions, like writing to a
specific folder. Eventually this will include other rules as well, and
the ability to persist these permissions, but this PR is already quite
large - let's get the core flow working and go from there!

<img width="1279" height="541" alt="Screenshot 2026-02-15 at 2 26 22 PM"
src="https://github.com/user-attachments/assets/0ee3ec0f-02ec-4509-91a2-809ac80be368"
/>

## Testing
- [x] Added tests
- [x] Tested locally
- [x] Feature
2026-02-24 09:48:57 -08:00

156 lines
5.4 KiB
Rust

use std::collections::HashMap;
use std::path::PathBuf;
use crate::mcp::RequestId;
use crate::models::AdditionalPermissions;
use crate::parse_command::ParsedCommand;
use crate::protocol::FileChange;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
/// Proposed execpolicy change to allow commands starting with this prefix.
///
/// The `command` tokens form the prefix that would be added as an execpolicy
/// `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
/// commands that start with this token sequence.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(transparent)]
#[ts(type = "Array<string>")]
pub struct ExecPolicyAmendment {
pub command: Vec<String>,
}
impl ExecPolicyAmendment {
pub fn new(command: Vec<String>) -> Self {
Self { command }
}
pub fn command(&self) -> &[String] {
&self.command
}
}
impl From<Vec<String>> for ExecPolicyAmendment {
fn from(command: Vec<String>) -> Self {
Self { command }
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum NetworkApprovalProtocol {
// TODO(viyatb): Add websocket protocol variants when managed proxy policy
// decisions expose websocket traffic as a distinct approval context.
Http,
#[serde(alias = "https_connect", alias = "http-connect")]
Https,
Socks5Tcp,
Socks5Udp,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct NetworkApprovalContext {
pub host: String,
pub protocol: NetworkApprovalProtocol,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum NetworkPolicyRuleAction {
Allow,
Deny,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct NetworkPolicyAmendment {
pub host: String,
pub action: NetworkPolicyRuleAction,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecApprovalRequestEvent {
/// Identifier for the associated command execution item.
pub call_id: String,
/// Identifier for this specific approval callback.
///
/// When absent, the approval is for the command item itself (`call_id`).
/// This is present for subcommand approvals (via execve intercept).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub approval_id: Option<String>,
/// Turn ID that this command belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory.
pub cwd: PathBuf,
/// Optional human-readable reason for the approval (e.g. retry without sandbox).
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
/// Optional network context for a blocked request that can be approved.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub network_approval_context: Option<NetworkApprovalContext>,
/// Proposed execpolicy amendment that can be applied to allow future runs.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
/// Proposed network policy amendments (for example allow/deny this host in future).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
/// Optional additional filesystem permissions requested for this command.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub additional_permissions: Option<AdditionalPermissions>,
pub parsed_cmd: Vec<ParsedCommand>,
}
impl ExecApprovalRequestEvent {
pub fn effective_approval_id(&self) -> String {
self.approval_id
.clone()
.unwrap_or_else(|| self.call_id.clone())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ElicitationRequestEvent {
pub server_name: String,
#[ts(type = "string | number")]
pub id: RequestId,
pub message: String,
// TODO: MCP servers can request we fill out a schema for the elicitation. We don't support
// this yet.
// pub requested_schema: ElicitRequestParamsRequestedSchema,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ApplyPatchApprovalRequestEvent {
/// Responses API call id for the associated patch apply call, if available.
pub call_id: String,
/// Turn ID that this patch belongs to.
/// Uses `#[serde(default)]` for backwards compatibility with older senders.
#[serde(default)]
pub turn_id: String,
pub changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
/// When set, the agent is asking the user to allow writes under this root for the remainder of the session.
#[serde(skip_serializing_if = "Option::is_none")]
pub grant_root: Option<PathBuf>,
}