chore: migrate additional permissions to PermissionProfile (#12731)

This PR replaces the old `additional_permissions.fs_read/fs_write` shape
with a shared `PermissionProfile`
model and wires it through the command approval, sandboxing, protocol,
and TUI layers. The schema is adopted from the
`SkillManifestPermissions`, which is also refactored to use this unified
struct. This helps us easily expose permission profiles in app
server/core as a follow-up.
This commit is contained in:
Celia Chen
2026-02-24 19:35:28 -08:00
committed by GitHub
Unverified
parent e6bb5d8553
commit 16ca527c80
26 changed files with 572 additions and 263 deletions
+2 -2
View File
@@ -70,8 +70,8 @@ use codex_protocol::items::PlanItem;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::FileChange;
@@ -2571,7 +2571,7 @@ impl Session {
reason: Option<String>,
network_approval_context: Option<NetworkApprovalContext>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
) -> ReviewDecision {
// command-level approvals use `call_id`.
// `approval_id` is only present for subcommand callbacks (execve intercept)
+36 -13
View File
@@ -24,7 +24,8 @@ use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::tools::sandboxing::SandboxablePreference;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
pub use codex_protocol::models::SandboxPermissions;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -41,7 +42,7 @@ pub struct CommandSpec {
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissions>,
pub additional_permissions: Option<PermissionProfile>,
pub justification: Option<String>,
}
@@ -95,14 +96,24 @@ pub(crate) enum SandboxTransformError {
}
pub(crate) fn normalize_additional_permissions(
additional_permissions: AdditionalPermissions,
additional_permissions: PermissionProfile,
command_cwd: &Path,
) -> Result<AdditionalPermissions, String> {
let fs_read =
normalize_permission_paths(additional_permissions.fs_read, command_cwd, "fs_read")?;
let fs_write =
normalize_permission_paths(additional_permissions.fs_write, command_cwd, "fs_write")?;
Ok(AdditionalPermissions { fs_read, fs_write })
) -> Result<PermissionProfile, String> {
let Some(file_system) = additional_permissions.file_system else {
return Ok(PermissionProfile::default());
};
let read = file_system
.read
.map(|paths| normalize_permission_paths(paths, command_cwd, "file_system.read"))
.transpose()?;
let write = file_system
.write
.map(|paths| normalize_permission_paths(paths, command_cwd, "file_system.write"))
.transpose()?;
Ok(PermissionProfile {
file_system: Some(FileSystemPermissions { read, write }),
..Default::default()
})
}
fn normalize_permission_paths(
@@ -162,7 +173,7 @@ fn dedup_absolute_paths(paths: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
}
fn additional_permission_roots(
additional_permissions: &AdditionalPermissions,
additional_permissions: &PermissionProfile,
) -> Result<(Vec<AbsolutePathBuf>, Vec<AbsolutePathBuf>), SandboxTransformError> {
let to_abs = |paths: &[PathBuf]| {
let mut out = Vec::with_capacity(paths.len());
@@ -179,8 +190,20 @@ fn additional_permission_roots(
};
Ok((
to_abs(&additional_permissions.fs_read)?,
to_abs(&additional_permissions.fs_write)?,
to_abs(
additional_permissions
.file_system
.as_ref()
.and_then(|file_system| file_system.read.as_deref())
.unwrap_or_default(),
)?,
to_abs(
additional_permissions
.file_system
.as_ref()
.and_then(|file_system| file_system.write.as_deref())
.unwrap_or_default(),
)?,
))
}
@@ -206,7 +229,7 @@ fn merge_read_only_access_with_additional_reads(
fn sandbox_policy_with_additional_permissions(
sandbox_policy: &SandboxPolicy,
additional_permissions: &AdditionalPermissions,
additional_permissions: &PermissionProfile,
) -> Result<SandboxPolicy, SandboxTransformError> {
if additional_permissions.is_empty() {
return Ok(sandbox_policy.clone());
+2 -2
View File
@@ -12,10 +12,10 @@ use crate::skills::model::SkillLoadOutcome;
use crate::skills::model::SkillMetadata;
use crate::skills::model::SkillPolicy;
use crate::skills::model::SkillToolDependency;
use crate::skills::permissions::SkillManifestPermissions;
use crate::skills::permissions::compile_permission_profile;
use crate::skills::system::system_cache_root_dir;
use codex_app_server_protocol::ConfigLayerSource;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SkillScope;
use dirs::home_dir;
use dunce::canonicalize as canonicalize_path;
@@ -54,7 +54,7 @@ struct SkillMetadataFile {
#[serde(default)]
policy: Option<Policy>,
#[serde(default)]
permissions: Option<SkillManifestPermissions>,
permissions: Option<PermissionProfile>,
}
#[derive(Debug, Default, Deserialize)]
+71 -98
View File
@@ -3,10 +3,15 @@ use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsAutomationValue;
use codex_protocol::models::MacOsPermissions;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsPreferencesValue;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use dirs::home_dir;
use dunce::canonicalize as canonicalize_path;
use serde::Deserialize;
use tracing::warn;
use crate::config::Constrained;
@@ -20,63 +25,24 @@ use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
#[cfg(not(target_os = "macos"))]
type MacOsSeatbeltProfileExtensions = ();
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub(crate) struct SkillManifestPermissions {
#[serde(default)]
pub(crate) network: bool,
#[serde(default)]
pub(crate) file_system: SkillManifestFileSystemPermissions,
#[serde(default)]
pub(crate) macos: SkillManifestMacOsPermissions,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub(crate) struct SkillManifestFileSystemPermissions {
#[serde(default)]
pub(crate) read: Vec<String>,
#[serde(default)]
pub(crate) write: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub(crate) struct SkillManifestMacOsPermissions {
#[serde(default)]
pub(crate) preferences: Option<MacOsPreferencesValue>,
#[serde(default)]
pub(crate) automations: Option<MacOsAutomationValue>,
#[serde(default)]
pub(crate) accessibility: bool,
#[serde(default)]
pub(crate) calendar: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub(crate) enum MacOsPreferencesValue {
Bool(bool),
Mode(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub(crate) enum MacOsAutomationValue {
Bool(bool),
BundleIds(Vec<String>),
}
pub(crate) fn compile_permission_profile(
skill_dir: &Path,
permissions: Option<SkillManifestPermissions>,
permissions: Option<PermissionProfile>,
) -> Option<Permissions> {
let permissions = permissions?;
let PermissionProfile {
network,
file_system,
macos,
} = permissions?;
let file_system = file_system.unwrap_or_default();
let fs_read = normalize_permission_paths(
skill_dir,
&permissions.file_system.read,
file_system.read.as_deref().unwrap_or_default(),
"permissions.file_system.read",
);
let fs_write = normalize_permission_paths(
skill_dir,
&permissions.file_system.write,
file_system.write.as_deref().unwrap_or_default(),
"permissions.file_system.write",
);
let sandbox_policy = if !fs_write.is_empty() {
@@ -90,7 +56,7 @@ pub(crate) fn compile_permission_profile(
readable_roots: fs_read,
}
},
network_access: permissions.network,
network_access: network.unwrap_or_default(),
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
@@ -105,8 +71,9 @@ pub(crate) fn compile_permission_profile(
// Default sandbox policy
SandboxPolicy::new_read_only_policy()
};
let macos_permissions = macos.unwrap_or_default();
let macos_seatbelt_profile_extensions =
build_macos_seatbelt_profile_extensions(&permissions.macos);
build_macos_seatbelt_profile_extensions(&macos_permissions);
Some(Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
@@ -121,7 +88,7 @@ pub(crate) fn compile_permission_profile(
fn normalize_permission_paths(
skill_dir: &Path,
values: &[String],
values: &[PathBuf],
field: &str,
) -> Vec<AbsolutePathBuf> {
let mut paths = Vec::new();
@@ -141,9 +108,10 @@ fn normalize_permission_paths(
fn normalize_permission_path(
skill_dir: &Path,
value: &str,
value: &Path,
field: &str,
) -> Option<AbsolutePathBuf> {
let value = value.to_string_lossy();
let trimmed = value.trim();
if trimmed.is_empty() {
warn!("ignoring {field}: value is empty");
@@ -151,11 +119,10 @@ fn normalize_permission_path(
}
let expanded = expand_home(trimmed);
let path = PathBuf::from(expanded);
let absolute = if path.is_absolute() {
path
let absolute = if expanded.is_absolute() {
expanded
} else {
skill_dir.join(path)
skill_dir.join(expanded)
};
let normalized = normalize_lexically(&absolute);
let canonicalized = canonicalize_path(&normalized).unwrap_or(normalized);
@@ -168,24 +135,24 @@ fn normalize_permission_path(
}
}
fn expand_home(path: &str) -> String {
fn expand_home(path: &str) -> PathBuf {
if path == "~" {
if let Some(home) = home_dir() {
return home.to_string_lossy().to_string();
return home;
}
return path.to_string();
return PathBuf::from(path);
}
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = home_dir()
{
return home.join(rest).to_string_lossy().to_string();
return home.join(rest);
}
path.to_string()
PathBuf::from(path)
}
#[cfg(target_os = "macos")]
fn build_macos_seatbelt_profile_extensions(
permissions: &SkillManifestMacOsPermissions,
permissions: &MacOsPermissions,
) -> Option<MacOsSeatbeltProfileExtensions> {
let defaults = MacOsSeatbeltProfileExtensions::default();
@@ -198,8 +165,10 @@ fn build_macos_seatbelt_profile_extensions(
permissions.automations.as_ref(),
defaults.macos_automation,
),
macos_accessibility: permissions.accessibility,
macos_calendar: permissions.calendar,
macos_accessibility: permissions
.accessibility
.unwrap_or(defaults.macos_accessibility),
macos_calendar: permissions.calendar.unwrap_or(defaults.macos_calendar),
};
Some(extensions)
}
@@ -262,7 +231,7 @@ fn resolve_macos_automation_permission(
#[cfg(not(target_os = "macos"))]
fn build_macos_seatbelt_profile_extensions(
_: &SkillManifestMacOsPermissions,
_: &MacOsPermissions,
) -> Option<MacOsSeatbeltProfileExtensions> {
None
}
@@ -285,10 +254,6 @@ fn normalize_lexically(path: &Path) -> PathBuf {
#[cfg(test)]
mod tests {
use super::SkillManifestFileSystemPermissions;
#[cfg(target_os = "macos")]
use super::SkillManifestMacOsPermissions;
use super::SkillManifestPermissions;
use super::compile_permission_profile;
use crate::config::Constrained;
use crate::config::Permissions;
@@ -296,9 +261,18 @@ mod tests {
use crate::protocol::AskForApproval;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
use codex_protocol::models::FileSystemPermissions;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsAutomationValue;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsPermissions;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsPreferencesValue;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
#[test]
fn compile_permission_profile_normalizes_paths() {
@@ -310,16 +284,16 @@ mod tests {
let profile = compile_permission_profile(
&skill_dir,
Some(SkillManifestPermissions {
network: true,
file_system: SkillManifestFileSystemPermissions {
read: vec![
"./data".to_string(),
"./data".to_string(),
"scripts/../data".to_string(),
],
write: vec!["./output".to_string()],
},
Some(PermissionProfile {
network: Some(true),
file_system: Some(FileSystemPermissions {
read: Some(vec![
PathBuf::from("./data"),
PathBuf::from("./data"),
PathBuf::from("scripts/../data"),
]),
write: Some(vec![PathBuf::from("./output")]),
}),
..Default::default()
}),
)
@@ -380,8 +354,8 @@ mod tests {
let profile = compile_permission_profile(
&skill_dir,
Some(SkillManifestPermissions {
network: true,
Some(PermissionProfile {
network: Some(true),
..Default::default()
}),
)
@@ -415,12 +389,12 @@ mod tests {
let profile = compile_permission_profile(
&skill_dir,
Some(SkillManifestPermissions {
network: true,
file_system: SkillManifestFileSystemPermissions {
read: vec!["./data".to_string()],
write: Vec::new(),
},
Some(PermissionProfile {
network: Some(true),
file_system: Some(FileSystemPermissions {
read: Some(vec![PathBuf::from("./data")]),
write: Some(Vec::new()),
}),
..Default::default()
}),
)
@@ -464,15 +438,15 @@ mod tests {
let profile = compile_permission_profile(
&skill_dir,
Some(SkillManifestPermissions {
macos: SkillManifestMacOsPermissions {
preferences: Some(super::MacOsPreferencesValue::Mode("readwrite".to_string())),
automations: Some(super::MacOsAutomationValue::BundleIds(vec![
Some(PermissionProfile {
macos: Some(MacOsPermissions {
preferences: Some(MacOsPreferencesValue::Mode("readwrite".to_string())),
automations: Some(MacOsAutomationValue::BundleIds(vec![
"com.apple.Notes".to_string(),
])),
accessibility: true,
calendar: true,
},
accessibility: Some(true),
calendar: Some(true),
}),
..Default::default()
}),
)
@@ -502,9 +476,8 @@ mod tests {
let skill_dir = tempdir.path().join("skill");
fs::create_dir_all(&skill_dir).expect("skill dir");
let profile =
compile_permission_profile(&skill_dir, Some(SkillManifestPermissions::default()))
.expect("profile");
let profile = compile_permission_profile(&skill_dir, Some(PermissionProfile::default()))
.expect("profile");
assert_eq!(
profile.macos_seatbelt_profile_extensions,
+5 -5
View File
@@ -24,7 +24,7 @@ use crate::function_tool::FunctionCallError;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::normalize_additional_permissions;
pub use apply_patch::ApplyPatchHandler;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
pub use dynamic::DynamicToolHandler;
pub use grep_files::GrepFilesHandler;
@@ -62,9 +62,9 @@ pub(super) fn normalize_and_validate_additional_permissions(
request_permission_enabled: bool,
approval_policy: AskForApproval,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
cwd: &Path,
) -> Result<Option<AdditionalPermissions>, String> {
) -> Result<Option<PermissionProfile>, String> {
let uses_additional_permissions = matches!(
sandbox_permissions,
SandboxPermissions::WithAdditionalPermissions
@@ -87,14 +87,14 @@ pub(super) fn normalize_and_validate_additional_permissions(
}
let Some(additional_permissions) = additional_permissions else {
return Err(
"missing `additional_permissions`; provide `fs_read` and/or `fs_write` when using `with_additional_permissions`"
"missing `additional_permissions`; provide `file_system.read` and/or `file_system.write` when using `with_additional_permissions`"
.to_string(),
);
};
let normalized = normalize_additional_permissions(additional_permissions, cwd)?;
if normalized.is_empty() {
return Err(
"`additional_permissions` must include at least one path in `fs_read` or `fs_write`"
"`additional_permissions` must include at least one path in `file_system.read` or `file_system.write`"
.to_string(),
);
}
+2 -2
View File
@@ -33,7 +33,7 @@ use crate::tools::runtimes::shell::ShellRuntime;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::spec::ShellCommandBackendConfig;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
pub struct ShellHandler;
@@ -50,7 +50,7 @@ pub struct ShellCommandHandler {
struct RunExecLikeArgs {
tool_name: String,
exec_params: ExecParams,
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
prefix_rule: Option<Vec<String>>,
session: Arc<crate::codex::Session>,
turn: Arc<TurnContext>,
@@ -21,8 +21,8 @@ use crate::unified_exec::UnifiedExecProcessManager;
use crate::unified_exec::UnifiedExecResponse;
use crate::unified_exec::WriteStdinRequest;
use async_trait::async_trait;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
@@ -47,7 +47,7 @@ pub(crate) struct ExecCommandArgs {
#[serde(default)]
sandbox_permissions: SandboxPermissions,
#[serde(default)]
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
#[serde(default)]
justification: Option<String>,
#[serde(default)]
+2 -2
View File
@@ -10,7 +10,7 @@ use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::tools::sandboxing::ToolError;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
use std::collections::HashMap;
use std::path::Path;
@@ -26,7 +26,7 @@ pub(crate) fn build_command_spec(
env: &HashMap<String, String>,
expiration: ExecExpiration,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
justification: Option<String>,
) -> Result<CommandSpec, ToolError> {
let (program, args) = command
+3 -3
View File
@@ -31,7 +31,7 @@ use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use crate::tools::sandboxing::with_cached_approval;
use codex_network_proxy::NetworkProxy;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::ReviewDecision;
use futures::future::BoxFuture;
use std::collections::HashMap;
@@ -46,7 +46,7 @@ pub struct ShellRequest {
pub explicit_env_overrides: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissions>,
pub additional_permissions: Option<PermissionProfile>,
pub justification: Option<String>,
pub exec_approval_requirement: ExecApprovalRequirement,
}
@@ -89,7 +89,7 @@ pub(crate) struct ApprovalKey {
command: Vec<String>,
cwd: PathBuf,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<AdditionalPermissions>,
additional_permissions: Option<PermissionProfile>,
}
impl ShellRuntime {
@@ -32,7 +32,7 @@ use crate::unified_exec::UnifiedExecError;
use crate::unified_exec::UnifiedExecProcess;
use crate::unified_exec::UnifiedExecProcessManager;
use codex_network_proxy::NetworkProxy;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::ReviewDecision;
use futures::future::BoxFuture;
use std::collections::HashMap;
@@ -47,7 +47,7 @@ pub struct UnifiedExecRequest {
pub network: Option<NetworkProxy>,
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissions>,
pub additional_permissions: Option<PermissionProfile>,
pub justification: Option<String>,
pub exec_approval_requirement: ExecApprovalRequirement,
}
@@ -58,7 +58,7 @@ pub struct UnifiedExecApprovalKey {
pub cwd: PathBuf,
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissions>,
pub additional_permissions: Option<PermissionProfile>,
}
pub struct UnifiedExecRuntime<'a> {
+28 -21
View File
@@ -256,29 +256,36 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap<Stri
properties.insert(
"additional_permissions".to_string(),
JsonSchema::Object {
properties: BTreeMap::from([
(
"fs_read".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"Additional filesystem paths to grant read access for this command."
.to_string(),
properties: BTreeMap::from([(
"file_system".to_string(),
JsonSchema::Object {
properties: BTreeMap::from([
(
"read".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"Additional filesystem paths to grant read access for this command."
.to_string(),
),
},
),
},
),
(
"fs_write".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"Additional filesystem paths to grant write access for this command."
.to_string(),
(
"write".to_string(),
JsonSchema::Array {
items: Box::new(JsonSchema::String { description: None }),
description: Some(
"Additional filesystem paths to grant write access for this command."
.to_string(),
),
},
),
},
),
]),
required: None,
]),
required: None,
additional_properties: Some(false.into()),
},
)]),
required: Some(vec!["file_system".to_string()]),
additional_properties: Some(false.into()),
},
);
+2 -2
View File
@@ -29,7 +29,7 @@ use std::sync::Weak;
use std::time::Duration;
use codex_network_proxy::NetworkProxy;
use codex_protocol::models::AdditionalPermissions;
use codex_protocol::models::PermissionProfile;
use rand::Rng;
use rand::rng;
use tokio::sync::Mutex;
@@ -90,7 +90,7 @@ pub(crate) struct ExecCommandRequest {
pub network: Option<NetworkProxy>,
pub tty: bool,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<AdditionalPermissions>,
pub additional_permissions: Option<PermissionProfile>,
pub justification: Option<String>,
pub prefix_rule: Option<Vec<String>>,
}