Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:33:17.783Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

agent.rs

1197 lines · 43.8 KB · rust
1use collections::{HashMap, IndexMap};
2use schemars::{JsonSchema, json_schema};
3use serde::{Deserialize, Serialize};
4use settings_macros::{MergeFrom, with_fallible_options};
5use std::sync::Arc;
6use std::{borrow::Cow, path::PathBuf};
7
8use crate::ExtendingVec;
9
10use crate::DockPosition;
11
12/// Where to position the threads sidebar.
13#[derive(
14    Clone,
15    Copy,
16    Debug,
17    Default,
18    PartialEq,
19    Eq,
20    Serialize,
21    Deserialize,
22    JsonSchema,
23    MergeFrom,
24    strum::VariantArray,
25    strum::VariantNames,
26)]
27#[serde(rename_all = "snake_case")]
28pub enum SidebarDockPosition {
29    /// Always show the sidebar on the left side.
30    #[default]
31    Left,
32    /// Always show the sidebar on the right side.
33    Right,
34}
35
36#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
37pub enum SidebarSide {
38    #[default]
39    Left,
40    Right,
41}
42
43/// How thinking blocks should be displayed by default in the agent panel.
44#[derive(
45    Clone,
46    Copy,
47    Debug,
48    Default,
49    PartialEq,
50    Eq,
51    Serialize,
52    Deserialize,
53    JsonSchema,
54    MergeFrom,
55    strum::VariantArray,
56    strum::VariantNames,
57)]
58#[serde(rename_all = "snake_case")]
59pub enum ThinkingBlockDisplay {
60    /// Thinking blocks fully expand during streaming, then auto-collapse
61    /// when the model finishes thinking. Users can re-expand after collapse.
62    #[default]
63    Auto,
64    /// Thinking blocks auto-expand with a height constraint during streaming,
65    /// then remain in their constrained state when complete. Users can click
66    /// to fully expand or collapse.
67    Preview,
68    /// Thinking blocks are always fully expanded by default (no height constraint).
69    AlwaysExpanded,
70    /// Thinking blocks are always collapsed by default.
71    AlwaysCollapsed,
72}
73
74/// Threshold at which agent auto-compaction runs. See
75/// [`AutoCompactSettingsContent::threshold`] for the accepted formats.
76///
77/// The canonical textual form is stored verbatim so it can round-trip through
78/// the settings UI; it is serialized back as a JSON string for percentages and
79/// as a JSON integer for token counts.
80#[derive(Clone, Debug, PartialEq, Eq, MergeFrom)]
81pub struct AutoCompactThreshold(pub String);
82
83impl From<String> for AutoCompactThreshold {
84    fn from(value: String) -> Self {
85        Self(value)
86    }
87}
88
89impl From<AutoCompactThreshold> for String {
90    fn from(value: AutoCompactThreshold) -> Self {
91        value.0
92    }
93}
94
95impl AsRef<str> for AutoCompactThreshold {
96    fn as_ref(&self) -> &str {
97        &self.0
98    }
99}
100
101impl Serialize for AutoCompactThreshold {
102    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
103        if self.0.ends_with('%') {
104            serializer.serialize_str(&self.0)
105        } else if let Ok(tokens) = self.0.parse::<i64>() {
106            serializer.serialize_i64(tokens)
107        } else {
108            serializer.serialize_str(&self.0)
109        }
110    }
111}
112
113impl<'de> Deserialize<'de> for AutoCompactThreshold {
114    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
115        struct ThresholdVisitor;
116
117        impl serde::de::Visitor<'_> for ThresholdVisitor {
118            type Value = AutoCompactThreshold;
119
120            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
121                formatter
122                    .write_str("a percentage string like \"90%\" or an integer number of tokens")
123            }
124
125            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
126                Ok(AutoCompactThreshold(value.to_owned()))
127            }
128
129            fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
130                Ok(AutoCompactThreshold(value.to_string()))
131            }
132
133            fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
134                Ok(AutoCompactThreshold(value.to_string()))
135            }
136
137            fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
138                Ok(AutoCompactThreshold(value.to_string()))
139            }
140        }
141
142        deserializer.deserialize_any(ThresholdVisitor)
143    }
144}
145
146impl JsonSchema for AutoCompactThreshold {
147    fn schema_name() -> Cow<'static, str> {
148        "AutoCompactThreshold".into()
149    }
150
151    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
152        json_schema!({
153            "oneOf": [
154                {
155                    "type": "string",
156                    "pattern": "^\\d+(\\.\\d+)?%$"
157                },
158                {
159                    "type": "integer"
160                }
161            ]
162        })
163    }
164}
165
166#[with_fallible_options]
167#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
168pub struct AutoCompactSettingsContent {
169    /// Whether to automatically compact the agent's context when it grows too
170    /// large, summarizing earlier messages to free up room in the model's
171    /// context window.
172    ///
173    /// Default: true
174    pub enabled: Option<bool>,
175    /// The threshold at which auto-compaction runs. This is one of:
176    ///
177    /// - A percentage string ending in `%`, e.g. `"90%"`, measured against the
178    ///   model's context window. `"90%"` compacts once the context is 90% full.
179    /// - A positive integer: compaction runs once that many tokens have been
180    ///   used. For example, `100000` compacts after 100,000 tokens are used.
181    /// - A negative integer: compaction runs once that many tokens remain in
182    ///   the context window. For example, `-20000` compacts once there are
183    ///   fewer than 20,000 tokens of headroom left in the context window.
184    ///
185    /// `0` is not a valid threshold.
186    ///
187    /// Default: "90%"
188    pub threshold: Option<AutoCompactThreshold>,
189}
190
191#[with_fallible_options]
192#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
193pub struct AgentSettingsContent {
194    /// Whether the Agent is enabled.
195    ///
196    /// Default: true
197    pub enabled: Option<bool>,
198    /// Whether to show the agent panel button in the status bar.
199    ///
200    /// Default: true
201    pub button: Option<bool>,
202    /// Where to dock the agent panel.
203    ///
204    /// Default: left (Agentic layout), right (Classic layout)
205    pub dock: Option<DockPosition>,
206    /// Whether the agent panel should use flexible (proportional) sizing.
207    ///
208    /// Default: true
209    pub flexible: Option<bool>,
210    /// Where to position the threads sidebar.
211    ///
212    /// Default: left
213    pub sidebar_side: Option<SidebarDockPosition>,
214    /// Default width in pixels when the agent panel is docked to the left or right.
215    ///
216    /// Default: 640
217    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
218    pub default_width: Option<f32>,
219    /// Default height in pixels when the agent panel is docked to the bottom.
220    ///
221    /// Default: 320
222    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
223    pub default_height: Option<f32>,
224    /// Whether to limit the content width in the agent panel. When enabled,
225    /// content will be constrained to `max_content_width` and centered when
226    /// the panel is wider than that value, for optimal readability.
227    ///
228    /// Default: true
229    pub limit_content_width: Option<bool>,
230    /// Maximum content width in pixels for the agent panel. Content will be
231    /// centered when the panel is wider than this value.
232    ///
233    /// Default: 850
234    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
235    pub max_content_width: Option<f32>,
236    /// The default model to use when creating new chats and for other features when a specific model is not specified.
237    pub default_model: Option<LanguageModelSelection>,
238    /// The model to use for subagents spawned via the `spawn_agent` tool. Defaults to the parent agent's model when not specified.
239    pub subagent_model: Option<LanguageModelSelection>,
240    /// Favorite models to show at the top of the model selector.
241    #[serde(default)]
242    pub favorite_models: Vec<LanguageModelSelection>,
243    /// Model to use for the inline assistant. Defaults to default_model when not specified.
244    pub inline_assistant_model: Option<LanguageModelSelection>,
245    /// Model to use for the inline assistant when streaming tools are enabled.
246    ///
247    /// Default: true
248    pub inline_assistant_use_streaming_tools: Option<bool>,
249    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
250    pub commit_message_model: Option<LanguageModelSelection>,
251    /// Whether to include project rules files (AGENTS.md, CLAUDE.md, .rules, etc.)
252    /// in the prompt when generating git commit messages.
253    ///
254    /// Default: true
255    pub commit_message_include_project_rules: Option<bool>,
256    /// Custom instructions to include in the prompt when generating git commit messages.
257    /// Applied in addition to any project rules files (such as `.rules` or `AGENTS.md`).
258    pub commit_message_instructions: Option<String>,
259    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
260    pub thread_summary_model: Option<LanguageModelSelection>,
261    /// Model to use for context compaction (`/compact` and auto-compaction).
262    /// Falls back to the thread's currently selected model when not specified.
263    /// If the configured model is unavailable (provider not registered, model
264    /// not found), the thread's current model is used instead.
265    pub compaction_model: Option<LanguageModelSelection>,
266    /// Additional models with which to generate alternatives when performing inline assists.
267    pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
268    /// The default profile to use in the Agent.
269    ///
270    /// Default: write
271    pub default_profile: Option<Arc<str>>,
272    /// The available agent profiles.
273    pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
274    /// Where to show a popup notification when the agent is waiting for user input.
275    ///
276    /// Default: "primary_screen"
277    pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
278    /// When to play a sound when the agent has either completed its response, or needs user input.
279    ///
280    /// Default: never
281    pub play_sound_when_agent_done: Option<PlaySoundWhenAgentDone>,
282    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
283    ///
284    /// Default: false
285    pub single_file_review: Option<bool>,
286    /// Additional parameters for language model requests. When making a request
287    /// to a model, parameters will be taken from the last entry in this list
288    /// that matches the model's provider and name. In each entry, both provider
289    /// and model are optional, so that you can specify parameters for either
290    /// one.
291    ///
292    /// Default: []
293    #[serde(default)]
294    pub model_parameters: Vec<LanguageModelParameters>,
295    /// Settings for automatic agent context compaction, which summarizes
296    /// earlier messages to free up room in the model's context window once the
297    /// context grows too large.
298    pub auto_compact: Option<AutoCompactSettingsContent>,
299    /// Whether to show thumb buttons for feedback in the agent panel.
300    ///
301    /// Default: true
302    pub enable_feedback: Option<bool>,
303    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
304    ///
305    /// Default: true
306    pub expand_edit_card: Option<bool>,
307    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
308    ///
309    /// Default: true
310    pub expand_terminal_card: Option<bool>,
311    /// Command to automatically run when Omega creates a Terminal Thread shell in the agent panel.
312    /// The command is sent to the shell as if typed, so it is interpreted by your
313    /// configured shell (including on Windows and remote/WSL projects).
314    /// An empty string disables this behavior.
315    ///
316    /// Default: ""
317    pub terminal_init_command: Option<String>,
318    /// How thinking blocks should be displayed by default in the agent panel.
319    ///
320    /// Default: automatic
321    pub thinking_display: Option<ThinkingBlockDisplay>,
322    /// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
323    /// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
324    ///
325    /// Default: true
326    pub cancel_generation_on_terminal_stop: Option<bool>,
327    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
328    ///
329    /// Default: false
330    pub use_modifier_to_send: Option<bool>,
331    /// Minimum number of lines of height the agent message editor should have.
332    ///
333    /// Default: 4
334    pub message_editor_min_lines: Option<usize>,
335    /// Whether to show turn statistics (elapsed time during generation, final turn duration).
336    ///
337    /// Default: false
338    pub show_turn_stats: Option<bool>,
339    /// Whether to show the merge conflict indicator in the status bar
340    /// that offers to resolve conflicts using the agent.
341    ///
342    /// Default: true
343    pub show_merge_conflict_indicator: Option<bool>,
344    /// Per-tool permission rules for granular control over which tool actions
345    /// require confirmation.
346    ///
347    /// The global `default` applies when no tool-specific rules match.
348    /// For external agent servers (e.g. Claude Agent) that define their own
349    /// permission modes, "deny" and "confirm" still take precedence — the
350    /// external agent's permission system is only used when Omega would allow
351    /// the action. Per-tool regex patterns (`always_allow`, `always_deny`,
352    /// `always_confirm`) match against the tool's text input (command, path,
353    /// URL, etc.).
354    pub tool_permissions: Option<ToolPermissionsContent>,
355
356    /// Persistent sandbox permission grants for agent-run terminal commands.
357    /// These are populated when choosing "Allow always" from a sandbox
358    /// escalation prompt.
359    pub sandbox_permissions: Option<SandboxPermissionsContent>,
360}
361
362impl AgentSettingsContent {
363    pub fn set_dock(&mut self, dock: DockPosition) {
364        self.dock = Some(dock);
365    }
366
367    pub fn set_sidebar_side(&mut self, position: SidebarDockPosition) {
368        self.sidebar_side = Some(position);
369    }
370
371    pub fn set_flexible_size(&mut self, flexible: bool) {
372        self.flexible = Some(flexible);
373    }
374
375    pub fn set_model(&mut self, language_model: LanguageModelSelection) {
376        self.default_model = Some(language_model)
377    }
378
379    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
380        self.inline_assistant_model = Some(LanguageModelSelection {
381            provider: provider.into(),
382            model,
383            enable_thinking: false,
384            effort: None,
385            speed: None,
386        });
387    }
388
389    pub fn set_profile(&mut self, profile_id: Arc<str>) {
390        self.default_profile = Some(profile_id);
391    }
392
393    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
394        // Note: this is intentional to not compare using `PartialEq`here.
395        // Full equality would treat entries that differ just in thinking/effort/speed
396        // as distinct and silently produce duplicates.
397        if !self
398            .favorite_models
399            .iter()
400            .any(|m| m.provider == model.provider && m.model == model.model)
401        {
402            self.favorite_models.push(model);
403        }
404    }
405
406    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
407        self.favorite_models
408            .retain(|m| !(m.provider == model.provider && m.model == model.model));
409    }
410
411    pub fn update_favorite_model<F>(&mut self, provider: &str, model: &str, f: F)
412    where
413        F: FnOnce(&mut LanguageModelSelection),
414    {
415        if let Some(entry) = self
416            .favorite_models
417            .iter_mut()
418            .find(|m| m.provider.0 == provider && m.model == model)
419        {
420            f(entry);
421        }
422    }
423
424    pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {
425        let tool_permissions = self.tool_permissions.get_or_insert_default();
426        let tool_rules = tool_permissions
427            .tools
428            .entry(Arc::from(tool_id))
429            .or_default();
430        tool_rules.default = Some(mode);
431    }
432
433    pub fn add_tool_allow_pattern(&mut self, tool_name: &str, pattern: String) {
434        let tool_permissions = self.tool_permissions.get_or_insert_default();
435        let tool_rules = tool_permissions
436            .tools
437            .entry(Arc::from(tool_name))
438            .or_default();
439        let always_allow = tool_rules.always_allow.get_or_insert_default();
440        if !always_allow.0.iter().any(|r| r.pattern == pattern) {
441            always_allow.0.push(ToolRegexRule {
442                pattern,
443                case_sensitive: None,
444            });
445        }
446    }
447
448    pub fn add_tool_deny_pattern(&mut self, tool_name: &str, pattern: String) {
449        let tool_permissions = self.tool_permissions.get_or_insert_default();
450        let tool_rules = tool_permissions
451            .tools
452            .entry(Arc::from(tool_name))
453            .or_default();
454        let always_deny = tool_rules.always_deny.get_or_insert_default();
455        if !always_deny.0.iter().any(|r| r.pattern == pattern) {
456            always_deny.0.push(ToolRegexRule {
457                pattern,
458                case_sensitive: None,
459            });
460        }
461    }
462
463    pub fn allow_sandbox_all_hosts(&mut self) {
464        self.sandbox_permissions
465            .get_or_insert_default()
466            .allow_all_hosts = Some(true);
467    }
468
469    /// The persisted sandbox network host patterns, as written (callers own
470    /// parsing/validation).
471    pub fn sandbox_network_hosts(&self) -> &[String] {
472        self.sandbox_permissions
473            .as_ref()
474            .and_then(|permissions| permissions.network_hosts.as_ref())
475            .map(|hosts| hosts.0.as_slice())
476            .unwrap_or_default()
477    }
478
479    /// Replace the persisted sandbox network host patterns. Callers compute
480    /// the new list (typically the old list plus newly granted hosts, pruned
481    /// of entries subsumed by wildcards) rather than appending blindly.
482    pub fn set_sandbox_network_hosts(&mut self, hosts: Vec<String>) {
483        self.sandbox_permissions
484            .get_or_insert_default()
485            .network_hosts = Some(ExtendingVec(hosts));
486    }
487
488    pub fn allow_sandbox_fs_write_all(&mut self) {
489        self.sandbox_permissions
490            .get_or_insert_default()
491            .allow_fs_write_all = Some(true);
492    }
493
494    pub fn allow_sandbox_unsandboxed(&mut self) {
495        self.sandbox_permissions
496            .get_or_insert_default()
497            .allow_unsandboxed = Some(true);
498    }
499
500    pub fn add_sandbox_write_path(&mut self, path: PathBuf) {
501        let write_paths = &mut self
502            .sandbox_permissions
503            .get_or_insert_default()
504            .write_paths
505            .get_or_insert_default()
506            .0;
507
508        util::paths::insert_subtree(write_paths, path);
509    }
510}
511
512#[with_fallible_options]
513#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
514pub struct AgentProfileContent {
515    pub name: Arc<str>,
516    #[serde(default)]
517    pub tools: IndexMap<Arc<str>, bool>,
518    /// Whether all context servers are enabled by default.
519    pub enable_all_context_servers: Option<bool>,
520    #[serde(default)]
521    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
522    /// The default language model selected when using this profile.
523    pub default_model: Option<LanguageModelSelection>,
524}
525
526#[with_fallible_options]
527#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
528pub struct ContextServerPresetContent {
529    pub tools: IndexMap<Arc<str>, bool>,
530}
531
532#[derive(
533    Copy,
534    Clone,
535    Default,
536    Debug,
537    Serialize,
538    Deserialize,
539    JsonSchema,
540    MergeFrom,
541    PartialEq,
542    strum::VariantArray,
543    strum::VariantNames,
544)]
545#[serde(rename_all = "snake_case")]
546pub enum NotifyWhenAgentWaiting {
547    #[default]
548    PrimaryScreen,
549    AllScreens,
550    Never,
551}
552
553#[derive(
554    Copy,
555    Clone,
556    Default,
557    Debug,
558    Serialize,
559    Deserialize,
560    JsonSchema,
561    MergeFrom,
562    PartialEq,
563    strum::VariantArray,
564    strum::VariantNames,
565)]
566#[serde(rename_all = "snake_case")]
567pub enum PlaySoundWhenAgentDone {
568    #[default]
569    Never,
570    WhenHidden,
571    Always,
572}
573
574impl PlaySoundWhenAgentDone {
575    pub fn should_play(&self, visible: bool) -> bool {
576        match self {
577            PlaySoundWhenAgentDone::Never => false,
578            PlaySoundWhenAgentDone::WhenHidden => !visible,
579            PlaySoundWhenAgentDone::Always => true,
580        }
581    }
582}
583
584#[with_fallible_options]
585#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
586pub struct LanguageModelSelection {
587    pub provider: LanguageModelProviderSetting,
588    pub model: String,
589    #[serde(default)]
590    pub enable_thinking: bool,
591    pub effort: Option<String>,
592    pub speed: Option<language_model_core::Speed>,
593}
594
595#[with_fallible_options]
596#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
597pub struct LanguageModelParameters {
598    pub provider: Option<LanguageModelProviderSetting>,
599    pub model: Option<String>,
600    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
601    pub temperature: Option<f32>,
602}
603
604#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)]
605pub struct LanguageModelProviderSetting(pub String);
606
607impl JsonSchema for LanguageModelProviderSetting {
608    fn schema_name() -> Cow<'static, str> {
609        "LanguageModelProviderSetting".into()
610    }
611
612    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
613        // list the builtin providers as a subset so that we still auto complete them in the settings
614        json_schema!({
615            "anyOf": [
616                {
617                    "type": "string",
618                    "enum": [
619                        "amazon-bedrock",
620                        "anthropic",
621                        "copilot_chat",
622                        "deepseek",
623                        "google",
624                        "lmstudio",
625                        "mistral",
626                        "ollama",
627                        "openai",
628                        "opencode",
629                        "openrouter",
630                        "vercel_ai_gateway",
631                        "x_ai",
632                        "zed.dev"
633                    ]
634                },
635                {
636                    "type": "string",
637                }
638            ]
639        })
640    }
641}
642
643impl From<String> for LanguageModelProviderSetting {
644    fn from(provider: String) -> Self {
645        Self(provider)
646    }
647}
648
649impl From<&str> for LanguageModelProviderSetting {
650    fn from(provider: &str) -> Self {
651        Self(provider.to_string())
652    }
653}
654
655#[with_fallible_options]
656#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
657#[serde(transparent)]
658pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
659
660impl std::ops::Deref for AllAgentServersSettings {
661    type Target = HashMap<String, CustomAgentServerSettings>;
662
663    fn deref(&self) -> &Self::Target {
664        &self.0
665    }
666}
667
668impl std::ops::DerefMut for AllAgentServersSettings {
669    fn deref_mut(&mut self) -> &mut Self::Target {
670        &mut self.0
671    }
672}
673
674#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq, Eq)]
675#[serde(untagged)]
676pub enum AgentConfigOptionValue {
677    ValueId(String),
678    Boolean(bool),
679}
680
681impl AgentConfigOptionValue {
682    pub fn as_value_id(&self) -> Option<&str> {
683        match self {
684            Self::ValueId(value) => Some(value),
685            Self::Boolean(_) => None,
686        }
687    }
688
689    pub fn as_bool(&self) -> Option<bool> {
690        match self {
691            Self::Boolean(value) => Some(*value),
692            Self::ValueId(_) => None,
693        }
694    }
695}
696
697impl std::fmt::Display for AgentConfigOptionValue {
698    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699        match self {
700            Self::ValueId(value) => formatter.write_str(value),
701            Self::Boolean(value) => value.fmt(formatter),
702        }
703    }
704}
705
706impl From<String> for AgentConfigOptionValue {
707    fn from(value: String) -> Self {
708        Self::ValueId(value)
709    }
710}
711
712impl From<&str> for AgentConfigOptionValue {
713    fn from(value: &str) -> Self {
714        Self::ValueId(value.to_string())
715    }
716}
717
718impl From<bool> for AgentConfigOptionValue {
719    fn from(value: bool) -> Self {
720        Self::Boolean(value)
721    }
722}
723
724#[with_fallible_options]
725#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
726#[serde(tag = "type", rename_all = "snake_case")]
727pub enum CustomAgentServerSettings {
728    Custom {
729        #[serde(rename = "command")]
730        path: PathBuf,
731        #[serde(default, skip_serializing_if = "Vec::is_empty")]
732        args: Vec<String>,
733        /// Default: {}
734        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
735        env: HashMap<String, String>,
736        /// The default mode to use for this agent.
737        ///
738        /// Note: Not only all agents support modes.
739        ///
740        /// Default: None
741        default_mode: Option<String>,
742        /// Default values for session config options.
743        ///
744        /// This is a map from config option ID to the default value for that option.
745        ///
746        /// Default: {}
747        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
748        default_config_options: HashMap<String, AgentConfigOptionValue>,
749        /// Favorited values for session config options.
750        ///
751        /// This is a map from config option ID to a list of favorited value IDs.
752        ///
753        /// Default: {}
754        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
755        favorite_config_option_values: HashMap<String, Vec<String>>,
756    },
757    // Used for the ACP extension migration
758    #[serde(alias = "extension")]
759    Registry {
760        /// Additional environment variables to pass to the agent.
761        ///
762        /// Default: {}
763        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
764        env: HashMap<String, String>,
765        /// The default mode to use for this agent.
766        ///
767        /// Note: Not only all agents support modes.
768        ///
769        /// Default: None
770        default_mode: Option<String>,
771        /// Default values for session config options.
772        ///
773        /// This is a map from config option ID to the default value for that option.
774        ///
775        /// Default: {}
776        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
777        default_config_options: HashMap<String, AgentConfigOptionValue>,
778        /// Favorited values for session config options.
779        ///
780        /// This is a map from config option ID to a list of favorited value IDs.
781        ///
782        /// Default: {}
783        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
784        favorite_config_option_values: HashMap<String, Vec<String>>,
785    },
786}
787
788#[with_fallible_options]
789#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
790pub struct SandboxPermissionsContent {
791    /// Whether sandboxed terminal commands may always reach any host over the
792    /// network without prompting.
793    /// Default: false
794    pub allow_all_hosts: Option<bool>,
795
796    /// Hosts that sandboxed terminal commands may always reach over the
797    /// network without prompting. Each entry is an exact hostname
798    /// (`github.com`) or a leading-`*.` subdomain wildcard (`*.npmjs.org`).
799    /// Default: []
800    pub network_hosts: Option<ExtendingVec<String>>,
801
802    /// Whether sandboxed terminal commands may always write anywhere on the
803    /// filesystem without prompting.
804    /// Default: false
805    pub allow_fs_write_all: Option<bool>,
806
807    /// Whether to persistently run agent terminal commands outside the OS
808    /// sandbox. This is the model-facing "off switch": when true, the sandboxed
809    /// terminal tool is not exposed and the system prompt omits the sandbox
810    /// section, so the model uses the plain `terminal` tool. On Windows, WSL
811    /// sandbox setup is skipped. Distinct from the model-requested
812    /// `unsandboxed: true` escape approved "once" or "for this thread".
813    /// Default: false
814    pub allow_unsandboxed: Option<bool>,
815
816    /// Directory subtrees that sandboxed terminal commands may always write
817    /// to without prompting. Paths written by Omega are absolute.
818    /// Default: []
819    pub write_paths: Option<ExtendingVec<PathBuf>>,
820
821    /// Whether to warn when a sandbox escalation prompt requests a domain or
822    /// write path that contains potentially confusable Unicode characters
823    /// (homoglyphs, invisible characters, or bidirectional overrides). When
824    /// enabled, such prompts show a warning that must be acknowledged before
825    /// the request can be allowed.
826    /// Default: true
827    pub warn_confusable_unicode: Option<bool>,
828}
829
830#[with_fallible_options]
831#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
832pub struct ToolPermissionsContent {
833    /// Global default permission when no tool-specific rules match.
834    /// Individual tools can override this with their own default.
835    /// Default: confirm
836    #[serde(alias = "default_mode")]
837    pub default: Option<ToolPermissionMode>,
838
839    /// Per-tool permission rules.
840    /// Keys are tool names (e.g. terminal, edit_file, fetch) including MCP
841    /// tools (e.g. mcp:server_name:tool_name). Any tool name is accepted;
842    /// even tools without meaningful text input can have a `default` set.
843    #[serde(default)]
844    pub tools: HashMap<Arc<str>, ToolRulesContent>,
845}
846
847#[with_fallible_options]
848#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
849pub struct ToolRulesContent {
850    /// Default mode when no regex rules match.
851    /// When unset, inherits from the global `tool_permissions.default`.
852    #[serde(alias = "default_mode")]
853    pub default: Option<ToolPermissionMode>,
854
855    /// Regexes for inputs to auto-approve.
856    /// For terminal: matches command. For file tools: matches path. For fetch: matches URL.
857    /// For `copy_path` and `move_path`, patterns are matched independently against each
858    /// path (source and destination).
859    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
860    /// removed by a higher-priority layer—only new patterns can be added.
861    /// Default: []
862    pub always_allow: Option<ExtendingVec<ToolRegexRule>>,
863
864    /// Regexes for inputs to auto-reject.
865    /// **SECURITY**: These take precedence over ALL other rules, across ALL settings layers.
866    /// For `copy_path` and `move_path`, patterns are matched independently against each
867    /// path (source and destination).
868    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
869    /// removed by a higher-priority layer—only new patterns can be added.
870    /// Default: []
871    pub always_deny: Option<ExtendingVec<ToolRegexRule>>,
872
873    /// Regexes for inputs that must always prompt.
874    /// Takes precedence over always_allow but not always_deny.
875    /// For `copy_path` and `move_path`, patterns are matched independently against each
876    /// path (source and destination).
877    /// Patterns accumulate across settings layers (user, project, profile) and cannot be
878    /// removed by a higher-priority layer—only new patterns can be added.
879    /// Default: []
880    pub always_confirm: Option<ExtendingVec<ToolRegexRule>>,
881}
882
883#[with_fallible_options]
884#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
885pub struct ToolRegexRule {
886    /// The regex pattern to match.
887    #[serde(default)]
888    pub pattern: String,
889
890    /// Whether the regex is case-sensitive.
891    /// Default: false (case-insensitive)
892    pub case_sensitive: Option<bool>,
893}
894
895#[derive(
896    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
897)]
898#[serde(rename_all = "snake_case")]
899pub enum ToolPermissionMode {
900    /// Auto-approve without prompting.
901    Allow,
902    /// Auto-reject with an error.
903    Deny,
904    /// Always prompt for confirmation (default behavior).
905    #[default]
906    Confirm,
907}
908
909impl std::fmt::Display for ToolPermissionMode {
910    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
911        match self {
912            ToolPermissionMode::Allow => write!(f, "Allow"),
913            ToolPermissionMode::Deny => write!(f, "Deny"),
914            ToolPermissionMode::Confirm => write!(f, "Confirm"),
915        }
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    #[test]
924    fn agent_config_option_value_serializes_value_id_as_string() {
925        let value = AgentConfigOptionValue::from("manual");
926
927        assert_eq!(
928            serde_json::to_value(&value).expect("serialize value id"),
929            serde_json::json!("manual")
930        );
931        assert_eq!(
932            serde_json::from_value::<AgentConfigOptionValue>(serde_json::json!("manual"))
933                .expect("deserialize value id"),
934            AgentConfigOptionValue::ValueId("manual".to_string())
935        );
936    }
937
938    #[test]
939    fn agent_config_option_value_serializes_boolean_as_boolean() {
940        let value = AgentConfigOptionValue::Boolean(true);
941
942        assert_eq!(
943            serde_json::to_value(&value).expect("serialize boolean"),
944            serde_json::json!(true)
945        );
946        assert_eq!(
947            serde_json::from_value::<AgentConfigOptionValue>(serde_json::json!(true))
948                .expect("deserialize boolean"),
949            AgentConfigOptionValue::Boolean(true)
950        );
951    }
952
953    #[test]
954    fn agent_config_option_value_merge_replaces_existing_value() {
955        use crate::merge_from::MergeFrom as _;
956
957        let mut value = AgentConfigOptionValue::ValueId("manual".to_string());
958        value.merge_from(&AgentConfigOptionValue::Boolean(true));
959
960        assert_eq!(value, AgentConfigOptionValue::Boolean(true));
961    }
962
963    #[test]
964    fn test_set_tool_default_permission_creates_structure() {
965        let mut settings = AgentSettingsContent::default();
966        assert!(settings.tool_permissions.is_none());
967
968        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
969
970        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
971        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
972        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
973    }
974
975    #[test]
976    fn test_set_tool_default_permission_updates_existing() {
977        let mut settings = AgentSettingsContent::default();
978
979        settings.set_tool_default_permission("terminal", ToolPermissionMode::Confirm);
980        settings.set_tool_default_permission("terminal", ToolPermissionMode::Allow);
981
982        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
983        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
984        assert_eq!(terminal_rules.default, Some(ToolPermissionMode::Allow));
985    }
986
987    #[test]
988    fn test_set_tool_default_permission_for_mcp_tool() {
989        let mut settings = AgentSettingsContent::default();
990
991        settings.set_tool_default_permission("mcp:github:create_issue", ToolPermissionMode::Allow);
992
993        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
994        let mcp_rules = tool_permissions
995            .tools
996            .get("mcp:github:create_issue")
997            .unwrap();
998        assert_eq!(mcp_rules.default, Some(ToolPermissionMode::Allow));
999    }
1000
1001    #[test]
1002    fn test_add_tool_allow_pattern_creates_structure() {
1003        let mut settings = AgentSettingsContent::default();
1004        assert!(settings.tool_permissions.is_none());
1005
1006        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1007
1008        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1009        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1010        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
1011        assert_eq!(always_allow.0.len(), 1);
1012        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
1013    }
1014
1015    #[test]
1016    fn test_add_tool_allow_pattern_appends_to_existing() {
1017        let mut settings = AgentSettingsContent::default();
1018
1019        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1020        settings.add_tool_allow_pattern("terminal", "^npm\\s".to_string());
1021
1022        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1023        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1024        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
1025        assert_eq!(always_allow.0.len(), 2);
1026        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
1027        assert_eq!(always_allow.0[1].pattern, "^npm\\s");
1028    }
1029
1030    #[test]
1031    fn test_add_tool_allow_pattern_does_not_duplicate() {
1032        let mut settings = AgentSettingsContent::default();
1033
1034        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1035        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1036        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1037
1038        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1039        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1040        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
1041        assert_eq!(
1042            always_allow.0.len(),
1043            1,
1044            "Duplicate patterns should not be added"
1045        );
1046    }
1047
1048    #[test]
1049    fn test_add_tool_allow_pattern_for_different_tools() {
1050        let mut settings = AgentSettingsContent::default();
1051
1052        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1053        settings.add_tool_allow_pattern("fetch", "^https?://github\\.com".to_string());
1054
1055        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1056
1057        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1058        assert_eq!(
1059            terminal_rules.always_allow.as_ref().unwrap().0[0].pattern,
1060            "^cargo\\s"
1061        );
1062
1063        let fetch_rules = tool_permissions.tools.get("fetch").unwrap();
1064        assert_eq!(
1065            fetch_rules.always_allow.as_ref().unwrap().0[0].pattern,
1066            "^https?://github\\.com"
1067        );
1068    }
1069
1070    #[test]
1071    fn test_add_tool_deny_pattern_creates_structure() {
1072        let mut settings = AgentSettingsContent::default();
1073        assert!(settings.tool_permissions.is_none());
1074
1075        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1076
1077        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1078        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1079        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
1080        assert_eq!(always_deny.0.len(), 1);
1081        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
1082    }
1083
1084    #[test]
1085    fn test_add_tool_deny_pattern_appends_to_existing() {
1086        let mut settings = AgentSettingsContent::default();
1087
1088        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1089        settings.add_tool_deny_pattern("terminal", "^sudo\\s".to_string());
1090
1091        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1092        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1093        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
1094        assert_eq!(always_deny.0.len(), 2);
1095        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
1096        assert_eq!(always_deny.0[1].pattern, "^sudo\\s");
1097    }
1098
1099    #[test]
1100    fn test_add_tool_deny_pattern_does_not_duplicate() {
1101        let mut settings = AgentSettingsContent::default();
1102
1103        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1104        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1105        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1106
1107        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1108        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1109        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
1110        assert_eq!(
1111            always_deny.0.len(),
1112            1,
1113            "Duplicate patterns should not be added"
1114        );
1115    }
1116
1117    #[test]
1118    fn test_add_tool_deny_and_allow_patterns_separate() {
1119        let mut settings = AgentSettingsContent::default();
1120
1121        settings.add_tool_allow_pattern("terminal", "^cargo\\s".to_string());
1122        settings.add_tool_deny_pattern("terminal", "^rm\\s".to_string());
1123
1124        let tool_permissions = settings.tool_permissions.as_ref().unwrap();
1125        let terminal_rules = tool_permissions.tools.get("terminal").unwrap();
1126
1127        let always_allow = terminal_rules.always_allow.as_ref().unwrap();
1128        assert_eq!(always_allow.0.len(), 1);
1129        assert_eq!(always_allow.0[0].pattern, "^cargo\\s");
1130
1131        let always_deny = terminal_rules.always_deny.as_ref().unwrap();
1132        assert_eq!(always_deny.0.len(), 1);
1133        assert_eq!(always_deny.0[0].pattern, "^rm\\s");
1134    }
1135
1136    #[test]
1137    fn test_allow_sandbox_permissions_create_structure() {
1138        let mut settings = AgentSettingsContent::default();
1139        assert!(settings.sandbox_permissions.is_none());
1140
1141        settings.allow_sandbox_all_hosts();
1142        assert_eq!(settings.sandbox_network_hosts(), &[] as &[String]);
1143        settings
1144            .set_sandbox_network_hosts(vec!["github.com".to_string(), "*.npmjs.org".to_string()]);
1145        assert_eq!(
1146            settings.sandbox_network_hosts(),
1147            &["github.com".to_string(), "*.npmjs.org".to_string()]
1148        );
1149        settings.allow_sandbox_fs_write_all();
1150        settings.allow_sandbox_unsandboxed();
1151        settings.add_sandbox_write_path(PathBuf::from("/tmp/build"));
1152
1153        let sandbox_permissions = settings.sandbox_permissions.as_ref().unwrap();
1154        assert_eq!(sandbox_permissions.allow_all_hosts, Some(true));
1155        assert_eq!(
1156            sandbox_permissions
1157                .network_hosts
1158                .as_ref()
1159                .unwrap()
1160                .0
1161                .as_slice(),
1162            &["github.com".to_string(), "*.npmjs.org".to_string()]
1163        );
1164        assert_eq!(sandbox_permissions.allow_fs_write_all, Some(true));
1165        assert_eq!(sandbox_permissions.allow_unsandboxed, Some(true));
1166        assert_eq!(
1167            sandbox_permissions
1168                .write_paths
1169                .as_ref()
1170                .unwrap()
1171                .0
1172                .as_slice(),
1173            &[PathBuf::from("/tmp/build")]
1174        );
1175    }
1176
1177    #[test]
1178    fn test_add_sandbox_write_path_prunes_redundant_paths() {
1179        let mut settings = AgentSettingsContent::default();
1180
1181        settings.add_sandbox_write_path(PathBuf::from("/tmp/build/cache"));
1182        settings.add_sandbox_write_path(PathBuf::from("/tmp/build"));
1183        settings.add_sandbox_write_path(PathBuf::from("/tmp/build/output"));
1184
1185        let write_paths = settings
1186            .sandbox_permissions
1187            .as_ref()
1188            .unwrap()
1189            .write_paths
1190            .as_ref()
1191            .unwrap()
1192            .0
1193            .as_slice();
1194        assert_eq!(write_paths, &[PathBuf::from("/tmp/build")]);
1195    }
1196}
1197
Served at tenant.openagents/omega Member data and write actions are omitted.