Skip to repository content1197 lines · 43.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:34:34.797Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
agent.rs
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