Codex Handbook
app-server-protocol/src/protocol/v2/permissions.rs 775 lines
use super::shared::v2_enum_from_core;use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction;use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile;use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode;use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath;use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::LegacyAppPathString;use codex_utils_path_uri::PathConvention;use schemars::JsonSchema;use serde::Deserialize;use serde::Serialize;use std::io;use std::num::NonZeroUsize;use std::path::PathBuf;use ts_rs::TS;v2_enum_from_core! {    pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol {        Http,        Https,        Socks5Tcp,        Socks5Udp,    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct NetworkApprovalContext {    pub host: String,    pub protocol: NetworkApprovalProtocol,}impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {    fn from(value: CoreNetworkApprovalContext) -> Self {        Self {            host: value.host,            protocol: value.protocol.into(),        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct AdditionalFileSystemPermissions {    /// This will be removed in favor of `entries`.    pub read: Option<Vec<LegacyAppPathString>>,    /// This will be removed in favor of `entries`.    pub write: Option<Vec<LegacyAppPathString>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub glob_scan_max_depth: Option<NonZeroUsize>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub entries: Option<Vec<FileSystemSandboxEntry>>,}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl From<CoreFileSystemPermissions<AbsolutePathBuf>> for AdditionalFileSystemPermissions {    fn from(value: CoreFileSystemPermissions<AbsolutePathBuf>) -> Self {        if let Some((read, write)) = value.legacy_read_write_roots() {            let mut entries = Vec::with_capacity(                read.as_ref().map_or(0, Vec::len) + write.as_ref().map_or(0, Vec::len),            );            if let Some(paths) = read.as_ref() {                entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {                    path: FileSystemPath::Path {                        path: LegacyAppPathString::from_abs_path(path),                    },                    access: FileSystemAccessMode::Read,                }));            }            if let Some(paths) = write.as_ref() {                entries.extend(paths.iter().map(|path| FileSystemSandboxEntry {                    path: FileSystemPath::Path {                        path: LegacyAppPathString::from_abs_path(path),                    },                    access: FileSystemAccessMode::Write,                }));            }            Self {                read: read.map(|paths| {                    paths                        .iter()                        .map(LegacyAppPathString::from_abs_path)                        .collect()                }),                write: write.map(|paths| {                    paths                        .iter()                        .map(LegacyAppPathString::from_abs_path)                        .collect()                }),                glob_scan_max_depth: None,                entries: Some(entries),            }        } else {            Self {                read: None,                write: None,                glob_scan_max_depth: value.glob_scan_max_depth,                entries: Some(                    value                        .entries                        .into_iter()                        .map(FileSystemSandboxEntry::from)                        .collect(),                ),            }        }    }}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl TryFrom<AdditionalFileSystemPermissions> for CoreFileSystemPermissions<AbsolutePathBuf> {    type Error = io::Error;    fn try_from(value: AdditionalFileSystemPermissions) -> Result<Self, Self::Error> {        let mut permissions = if let Some(entries) = value.entries {            Self {                entries: entries                    .into_iter()                    .map(CoreFileSystemSandboxEntry::<AbsolutePathBuf>::try_from)                    .collect::<io::Result<_>>()?,                glob_scan_max_depth: None,            }        } else {            let read = value                .read                .map(|paths| {                    paths                        .into_iter()                        .map(|path| {                            path.to_path_uri(PathConvention::native())                                .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?                                .to_abs_path()                        })                        .collect::<io::Result<Vec<_>>>()                })                .transpose()?;            let write = value                .write                .map(|paths| {                    paths                        .into_iter()                        .map(|path| {                            path.to_path_uri(PathConvention::native())                                .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?                                .to_abs_path()                        })                        .collect::<io::Result<Vec<_>>>()                })                .transpose()?;            CoreFileSystemPermissions::from_read_write_roots(read, write)        };        permissions.glob_scan_max_depth = value.glob_scan_max_depth;        Ok(permissions)    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct AdditionalNetworkPermissions {    pub enabled: Option<bool>,}impl From<CoreNetworkPermissions> for AdditionalNetworkPermissions {    fn from(value: CoreNetworkPermissions) -> Self {        Self {            enabled: value.enabled,        }    }}impl From<AdditionalNetworkPermissions> for CoreNetworkPermissions {    fn from(value: AdditionalNetworkPermissions) -> Self {        Self {            enabled: value.enabled,        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[serde(deny_unknown_fields)]#[ts(export_to = "v2/")]pub struct RequestPermissionProfile {    pub network: Option<AdditionalNetworkPermissions>,    pub file_system: Option<AdditionalFileSystemPermissions>,}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl From<CoreRequestPermissionProfile> for RequestPermissionProfile {    fn from(value: CoreRequestPermissionProfile) -> Self {        Self {            network: value.network.map(AdditionalNetworkPermissions::from),            file_system: value.file_system.map(AdditionalFileSystemPermissions::from),        }    }}impl TryFrom<RequestPermissionProfile> for CoreRequestPermissionProfile {    type Error = io::Error;    fn try_from(value: RequestPermissionProfile) -> Result<Self, Self::Error> {        Ok(Self {            network: value.network.map(CoreNetworkPermissions::from),            file_system: value                .file_system                .map(CoreFileSystemPermissions::<AbsolutePathBuf>::try_from)                .transpose()?,        })    }}v2_enum_from_core!(    pub enum FileSystemAccessMode from CoreFileSystemAccessMode {        Read,        Write,        Deny    });#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(tag = "kind", rename_all = "snake_case")]#[ts(tag = "kind")]#[ts(export_to = "v2/")]pub enum FileSystemSpecialPath {    Root,    Minimal,    #[serde(alias = "current_working_directory")]    ProjectRoots {        subpath: Option<PathBuf>,    },    Tmpdir,    SlashTmp,    Unknown {        path: String,        subpath: Option<PathBuf>,    },}impl From<CoreFileSystemSpecialPath> for FileSystemSpecialPath {    fn from(value: CoreFileSystemSpecialPath) -> Self {        match value {            CoreFileSystemSpecialPath::Root => Self::Root,            CoreFileSystemSpecialPath::Minimal => Self::Minimal,            CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath },            CoreFileSystemSpecialPath::Tmpdir => Self::Tmpdir,            CoreFileSystemSpecialPath::SlashTmp => Self::SlashTmp,            CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath },        }    }}impl From<FileSystemSpecialPath> for CoreFileSystemSpecialPath {    fn from(value: FileSystemSpecialPath) -> Self {        match value {            FileSystemSpecialPath::Root => Self::Root,            FileSystemSpecialPath::Minimal => Self::Minimal,            FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath },            FileSystemSpecialPath::Tmpdir => Self::Tmpdir,            FileSystemSpecialPath::SlashTmp => Self::SlashTmp,            FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath },        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(tag = "type")]#[ts(export_to = "v2/")]// TODO(anp): Rename this type to distinguish it from the generic protocol FileSystemPath.pub enum FileSystemPath {    Path { path: LegacyAppPathString },    GlobPattern { pattern: String },    Special { value: FileSystemSpecialPath },}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl From<CoreFileSystemPath<AbsolutePathBuf>> for FileSystemPath {    fn from(value: CoreFileSystemPath<AbsolutePathBuf>) -> Self {        match value {            CoreFileSystemPath::Path { path } => Self::Path {                path: LegacyAppPathString::from_abs_path(&path),            },            CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },            CoreFileSystemPath::Special { value } => Self::Special {                value: value.into(),            },        }    }}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl TryFrom<FileSystemPath> for CoreFileSystemPath<AbsolutePathBuf> {    type Error = io::Error;    fn try_from(value: FileSystemPath) -> Result<Self, Self::Error> {        Ok(match value {            FileSystemPath::Path { path } => Self::Path {                path: path                    .to_path_uri(PathConvention::native())                    .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?                    .to_abs_path()?,            },            FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern },            FileSystemPath::Special { value } => Self::Special {                value: value.into(),            },        })    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct FileSystemSandboxEntry {    pub path: FileSystemPath,    pub access: FileSystemAccessMode,}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl From<CoreFileSystemSandboxEntry<AbsolutePathBuf>> for FileSystemSandboxEntry {    fn from(value: CoreFileSystemSandboxEntry<AbsolutePathBuf>) -> Self {        Self {            path: value.path.into(),            access: value.access.into(),        }    }}impl TryFrom<FileSystemSandboxEntry> for CoreFileSystemSandboxEntry<AbsolutePathBuf> {    type Error = io::Error;    fn try_from(value: FileSystemSandboxEntry) -> Result<Self, Self::Error> {        Ok(Self {            path: value.path.try_into()?,            access: value.access.to_core(),        })    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct PermissionProfileListParams {    /// Opaque pagination cursor returned by a previous call.    #[ts(optional = nullable)]    pub cursor: Option<String>,    /// Optional page size; defaults to the full result set.    #[ts(optional = nullable)]    pub limit: Option<u32>,    /// Optional working directory to resolve project config layers.    #[ts(optional = nullable)]    pub cwd: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct PermissionProfileSummary {    /// Available permission profile identifier.    pub id: String,    /// Optional user-facing description for display in clients.    pub description: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct PermissionProfileListResponse {    pub data: Vec<PermissionProfileSummary>,    /// Opaque cursor to pass to the next call to continue after the last item.    /// If None, there are no more items to return.    pub next_cursor: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct ActivePermissionProfile {    /// Identifier from `default_permissions` or the implicit built-in default,    /// such as `:workspace` or a user-defined `[permissions.<id>]` profile.    pub id: String,    /// Parent profile identifier from the selected permissions profile's    /// `extends` setting, when present.    #[serde(default)]    pub extends: Option<String>,}impl ActivePermissionProfile {    pub fn new(id: impl Into<String>) -> Self {        Self {            id: id.into(),            extends: None,        }    }    pub fn read_only() -> Self {        CoreActivePermissionProfile::read_only().into()    }}impl From<CoreActivePermissionProfile> for ActivePermissionProfile {    fn from(value: CoreActivePermissionProfile) -> Self {        Self {            id: value.id,            extends: value.extends,        }    }}impl From<ActivePermissionProfile> for CoreActivePermissionProfile {    fn from(value: ActivePermissionProfile) -> Self {        Self {            id: value.id,            extends: value.extends,        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct AdditionalPermissionProfile {    /// Partial overlay used for per-command permission requests.    pub network: Option<AdditionalNetworkPermissions>,    pub file_system: Option<AdditionalFileSystemPermissions>,}// TODO(anp): Remove this conversion once core permission paths use PathUri.impl From<CoreAdditionalPermissionProfile> for AdditionalPermissionProfile {    fn from(value: CoreAdditionalPermissionProfile) -> Self {        Self {            network: value.network.map(AdditionalNetworkPermissions::from),            file_system: value.file_system.map(AdditionalFileSystemPermissions::from),        }    }}impl TryFrom<AdditionalPermissionProfile> for CoreAdditionalPermissionProfile {    type Error = io::Error;    fn try_from(value: AdditionalPermissionProfile) -> Result<Self, Self::Error> {        Ok(Self {            network: value.network.map(CoreNetworkPermissions::from),            file_system: value                .file_system                .map(CoreFileSystemPermissions::<AbsolutePathBuf>::try_from)                .transpose()?,        })    }}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct GrantedPermissionProfile {    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub network: Option<AdditionalNetworkPermissions>,    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub file_system: Option<AdditionalFileSystemPermissions>,}impl TryFrom<GrantedPermissionProfile> for CoreAdditionalPermissionProfile {    type Error = io::Error;    fn try_from(value: GrantedPermissionProfile) -> Result<Self, Self::Error> {        Ok(Self {            network: value.network.map(CoreNetworkPermissions::from),            file_system: value                .file_system                .map(CoreFileSystemPermissions::<AbsolutePathBuf>::try_from)                .transpose()?,        })    }}#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub enum NetworkAccess {    #[default]    Restricted,    Enabled,}#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "camelCase")]#[ts(tag = "type")]#[ts(export_to = "v2/")]pub enum SandboxPolicy {    DangerFullAccess,    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    ReadOnly {        #[serde(default)]        network_access: bool,    },    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    ExternalSandbox {        #[serde(default)]        network_access: NetworkAccess,    },    #[serde(rename_all = "camelCase")]    #[ts(rename_all = "camelCase")]    WorkspaceWrite {        #[serde(default)]        writable_roots: Vec<AbsolutePathBuf>,        #[serde(default)]        network_access: bool,        #[serde(default)]        exclude_tmpdir_env_var: bool,        #[serde(default)]        exclude_slash_tmp: bool,    },}#[derive(Deserialize)]#[serde(tag = "type", rename_all = "camelCase")]enum SandboxPolicyDeserialize {    DangerFullAccess,    #[serde(rename_all = "camelCase")]    ReadOnly {        #[serde(default)]        network_access: bool,        #[serde(default)]        access: Option<LegacyReadOnlyAccess>,    },    #[serde(rename_all = "camelCase")]    ExternalSandbox {        #[serde(default)]        network_access: NetworkAccess,    },    #[serde(rename_all = "camelCase")]    WorkspaceWrite {        #[serde(default)]        writable_roots: Vec<AbsolutePathBuf>,        #[serde(default)]        read_only_access: Option<LegacyReadOnlyAccess>,        #[serde(default)]        network_access: bool,        #[serde(default)]        exclude_tmpdir_env_var: bool,        #[serde(default)]        exclude_slash_tmp: bool,    },}#[derive(Deserialize)]#[serde(tag = "type", rename_all = "camelCase")]enum LegacyReadOnlyAccess {    FullAccess,    Restricted,}impl<'de> Deserialize<'de> for SandboxPolicy {    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>    where        D: serde::Deserializer<'de>,    {        match SandboxPolicyDeserialize::deserialize(deserializer)? {            SandboxPolicyDeserialize::DangerFullAccess => Ok(SandboxPolicy::DangerFullAccess),            SandboxPolicyDeserialize::ReadOnly {                network_access,                access,            } => {                if matches!(access, Some(LegacyReadOnlyAccess::Restricted)) {                    return Err(serde::de::Error::custom(                        "readOnly.access is no longer supported; use permissionProfile for restricted reads",                    ));                }                Ok(SandboxPolicy::ReadOnly { network_access })            }            SandboxPolicyDeserialize::ExternalSandbox { network_access } => {                Ok(SandboxPolicy::ExternalSandbox { network_access })            }            SandboxPolicyDeserialize::WorkspaceWrite {                writable_roots,                read_only_access,                network_access,                exclude_tmpdir_env_var,                exclude_slash_tmp,            } => {                if matches!(read_only_access, Some(LegacyReadOnlyAccess::Restricted)) {                    return Err(serde::de::Error::custom(                        "workspaceWrite.readOnlyAccess is no longer supported; use permissionProfile for restricted reads",                    ));                }                Ok(SandboxPolicy::WorkspaceWrite {                    writable_roots,                    network_access,                    exclude_tmpdir_env_var,                    exclude_slash_tmp,                })            }        }    }}impl SandboxPolicy {    pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy {        match self {            SandboxPolicy::DangerFullAccess => {                codex_protocol::protocol::SandboxPolicy::DangerFullAccess            }            SandboxPolicy::ReadOnly { network_access } => {                codex_protocol::protocol::SandboxPolicy::ReadOnly {                    network_access: *network_access,                }            }            SandboxPolicy::ExternalSandbox { network_access } => {                codex_protocol::protocol::SandboxPolicy::ExternalSandbox {                    network_access: match network_access {                        NetworkAccess::Restricted => CoreNetworkAccess::Restricted,                        NetworkAccess::Enabled => CoreNetworkAccess::Enabled,                    },                }            }            SandboxPolicy::WorkspaceWrite {                writable_roots,                network_access,                exclude_tmpdir_env_var,                exclude_slash_tmp,            } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {                writable_roots: writable_roots.clone(),                network_access: *network_access,                exclude_tmpdir_env_var: *exclude_tmpdir_env_var,                exclude_slash_tmp: *exclude_slash_tmp,            },        }    }}impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {    fn from(value: codex_protocol::protocol::SandboxPolicy) -> Self {        match value {            codex_protocol::protocol::SandboxPolicy::DangerFullAccess => {                SandboxPolicy::DangerFullAccess            }            codex_protocol::protocol::SandboxPolicy::ReadOnly { network_access } => {                SandboxPolicy::ReadOnly { network_access }            }            codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => {                SandboxPolicy::ExternalSandbox {                    network_access: match network_access {                        CoreNetworkAccess::Restricted => NetworkAccess::Restricted,                        CoreNetworkAccess::Enabled => NetworkAccess::Enabled,                    },                }            }            codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {                writable_roots,                network_access,                exclude_tmpdir_env_var,                exclude_slash_tmp,            } => SandboxPolicy::WorkspaceWrite {                writable_roots,                network_access,                exclude_tmpdir_env_var,                exclude_slash_tmp,            },        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(transparent)]#[ts(type = "Array<string>", export_to = "v2/")]pub struct ExecPolicyAmendment {    pub command: Vec<String>,}impl ExecPolicyAmendment {    pub fn into_core(self) -> CoreExecPolicyAmendment {        CoreExecPolicyAmendment::new(self.command)    }}impl From<CoreExecPolicyAmendment> for ExecPolicyAmendment {    fn from(value: CoreExecPolicyAmendment) -> Self {        Self {            command: value.command().to_vec(),        }    }}v2_enum_from_core!(    pub enum NetworkPolicyRuleAction from CoreNetworkPolicyRuleAction {        Allow, Deny    });#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct NetworkPolicyAmendment {    pub host: String,    pub action: NetworkPolicyRuleAction,}impl NetworkPolicyAmendment {    pub fn into_core(self) -> CoreNetworkPolicyAmendment {        CoreNetworkPolicyAmendment {            host: self.host,            action: self.action.to_core(),        }    }}impl From<CoreNetworkPolicyAmendment> for NetworkPolicyAmendment {    fn from(value: CoreNetworkPolicyAmendment) -> Self {        Self {            host: value.host,            action: NetworkPolicyRuleAction::from(value.action),        }    }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct PermissionsRequestApprovalParams {    pub thread_id: String,    pub turn_id: String,    pub item_id: String,    #[serde(default)]    pub environment_id: Option<String>,    /// Unix timestamp (in milliseconds) when this approval request started.    #[ts(type = "number")]    pub started_at_ms: i64,    pub cwd: AbsolutePathBuf,    pub reason: Option<String>,    pub permissions: RequestPermissionProfile,}v2_enum_from_core!(    #[derive(Default)]    pub enum PermissionGrantScope from CorePermissionGrantScope {        #[default]        Turn,        Session    });#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]#[serde(rename_all = "camelCase")]#[ts(export_to = "v2/")]pub struct PermissionsRequestApprovalResponse {    pub permissions: GrantedPermissionProfile,    #[serde(default)]    pub scope: PermissionGrantScope,    /// Review every subsequent command in this turn before normal sandboxed execution.    #[serde(default, skip_serializing_if = "Option::is_none")]    #[ts(optional)]    pub strict_auto_review: Option<bool>,}