use std::path::{Component, Path, PathBuf};
use serde_json::Value;
use crate::model::agent::{AgentKind, AgentModel, ReasoningLevel};
use crate::model::permission::PermissionMode;
pub(super) struct PermissionModePolicy {
approval_policy: &'static str,
legacy_pre_action_decision: &'static str,
legacy_pre_action_rejection_decision: &'static str,
pre_action_decision: &'static str,
pre_action_rejection_decision: &'static str,
thread_sandbox_mode: &'static str,
turn_network_access: bool,
turn_sandbox_type: &'static str,
web_search_mode: &'static str,
}
const AUTO_EDIT_POLICY: PermissionModePolicy = PermissionModePolicy {
approval_policy: "never",
legacy_pre_action_decision: "approved",
legacy_pre_action_rejection_decision: "denied",
pre_action_decision: "accept",
pre_action_rejection_decision: "reject",
thread_sandbox_mode: "workspace-write",
turn_network_access: true,
turn_sandbox_type: "workspaceWrite",
web_search_mode: "live",
};
enum PreActionApprovalKind {
Command,
FileChange,
LegacyCommand,
LegacyPatch,
}
impl PreActionApprovalKind {
fn from_method(method: &str) -> Option<Self> {
match method {
"item/commandExecution/requestApproval" => Some(Self::Command),
"item/fileChange/requestApproval" => Some(Self::FileChange),
"execCommandApproval" => Some(Self::LegacyCommand),
"applyPatchApproval" => Some(Self::LegacyPatch),
_ => None,
}
}
fn is_legacy(&self) -> bool {
matches!(self, Self::LegacyCommand | Self::LegacyPatch)
}
fn is_command(&self) -> bool {
matches!(self, Self::Command | Self::LegacyCommand)
}
}
pub(super) const AUTO_COMPACT_INPUT_TOKEN_THRESHOLD_400K_CONTEXT: u64 = 300_000;
pub(super) const AUTO_COMPACT_INPUT_TOKEN_THRESHOLD_128K_CONTEXT: u64 = 120_000;
pub(super) fn auto_compact_input_token_threshold(model: &str) -> u64 {
let is_400k_context_model =
matches!(AgentKind::Codex.parse_model(model), Some(AgentModel::Gpt55));
if is_400k_context_model {
return AUTO_COMPACT_INPUT_TOKEN_THRESHOLD_400K_CONTEXT;
}
AUTO_COMPACT_INPUT_TOKEN_THRESHOLD_128K_CONTEXT
}
pub(super) fn approval_policy() -> &'static str {
permission_mode_policy(PermissionMode::default()).approval_policy
}
pub(super) fn thread_sandbox_mode() -> &'static str {
permission_mode_policy(PermissionMode::default()).thread_sandbox_mode
}
pub(super) fn turn_sandbox_policy() -> Value {
let policy = permission_mode_policy(PermissionMode::default());
let mut turn_sandbox_policy = serde_json::json!({
"type": policy.turn_sandbox_type
});
if policy.turn_sandbox_type == "workspaceWrite"
&& let Some(policy_object) = turn_sandbox_policy.as_object_mut()
{
policy_object.insert(
"networkAccess".to_string(),
Value::Bool(policy.turn_network_access),
);
}
turn_sandbox_policy
}
pub(super) fn thread_config(reasoning_level: ReasoningLevel) -> Value {
serde_json::json!({
"web_search": web_search_mode(),
"model_reasoning_effort": reasoning_level.codex(),
})
}
pub(super) fn web_search_mode() -> &'static str {
permission_mode_policy(PermissionMode::default()).web_search_mode
}
pub(super) fn build_pre_action_approval_response(
response_value: &Value,
session_folder: &Path,
) -> Option<Value> {
let method = response_value.get("method")?.as_str()?;
let request_id = response_value.get("id")?.clone();
let approval_kind = PreActionApprovalKind::from_method(method)?;
let decision = scoped_pre_action_decision(response_value, session_folder, &approval_kind);
Some(serde_json::json!({
"id": request_id,
"result": {
"decision": decision
}
}))
}
fn scoped_pre_action_decision(
response_value: &Value,
session_folder: &Path,
approval_kind: &PreActionApprovalKind,
) -> &'static str {
if approval_kind.is_command() {
return pre_action_approval_decision(approval_kind);
}
if approval_request_paths_are_session_local(response_value, session_folder) {
return pre_action_approval_decision(approval_kind);
}
pre_action_rejection_decision(approval_kind)
}
fn approval_request_paths_are_session_local(response_value: &Value, session_folder: &Path) -> bool {
let mut candidate_paths = Vec::new();
collect_candidate_paths(response_value, None, &mut candidate_paths);
!candidate_paths.is_empty()
&& candidate_paths
.iter()
.all(|path| path_is_session_local(path, session_folder))
}
fn collect_candidate_paths(
value: &Value,
key_hint: Option<&str>,
candidate_paths: &mut Vec<String>,
) {
match value {
Value::Object(object) => {
for (key, nested_value) in object {
collect_candidate_paths(nested_value, Some(key), candidate_paths);
}
}
Value::Array(values) => {
for nested_value in values {
collect_candidate_paths(nested_value, key_hint, candidate_paths);
}
}
Value::String(text) if key_hint.is_some_and(path_key_may_contain_file_path) => {
candidate_paths.push(text.clone());
}
_ => {}
}
}
fn path_key_may_contain_file_path(key: &str) -> bool {
let key = key.to_ascii_lowercase();
key.contains("path") || key.contains("file") || key == "cwd"
}
fn path_is_session_local(path_text: &str, session_folder: &Path) -> bool {
let candidate_path = PathBuf::from(path_text);
if candidate_path.is_absolute() {
return candidate_path.starts_with(session_folder);
}
normalize_session_relative_path(session_folder, &candidate_path)
.is_some_and(|normalized_path| normalized_path.starts_with(session_folder))
}
fn normalize_session_relative_path(session_folder: &Path, relative_path: &Path) -> Option<PathBuf> {
let mut normalized_path = session_folder.to_path_buf();
for component in relative_path.components() {
match component {
Component::Normal(path_component) => normalized_path.push(path_component),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
Some(normalized_path)
}
fn pre_action_approval_decision(approval_kind: &PreActionApprovalKind) -> &'static str {
let policy = permission_mode_policy(PermissionMode::default());
if approval_kind.is_legacy() {
return policy.legacy_pre_action_decision;
}
policy.pre_action_decision
}
fn pre_action_rejection_decision(approval_kind: &PreActionApprovalKind) -> &'static str {
let policy = permission_mode_policy(PermissionMode::default());
if approval_kind.is_legacy() {
return policy.legacy_pre_action_rejection_decision;
}
policy.pre_action_rejection_decision
}
fn permission_mode_policy(permission_mode: PermissionMode) -> &'static PermissionModePolicy {
match permission_mode {
PermissionMode::AutoEdit => &AUTO_EDIT_POLICY,
}
}