Skip to main content

aft/
config.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::harness::Harness;
7
8/// Runtime configuration for the aft process.
9///
10/// Holds project-scoped settings and tuning knobs. Values are set at startup
11/// and remain immutable for the lifetime of the process.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SemanticBackend {
15    Fastembed,
16    #[serde(rename = "openai_compatible")]
17    OpenAiCompatible,
18    Ollama,
19}
20
21impl SemanticBackend {
22    pub const fn as_str(&self) -> &'static str {
23        match self {
24            Self::Fastembed => "fastembed",
25            Self::OpenAiCompatible => "openai_compatible",
26            Self::Ollama => "ollama",
27        }
28    }
29
30    pub fn from_name(name: &str) -> Option<Self> {
31        match name {
32            "fastembed" => Some(Self::Fastembed),
33            "openai_compatible" => Some(Self::OpenAiCompatible),
34            "ollama" => Some(Self::Ollama),
35            _ => None,
36        }
37    }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct SemanticBackendConfig {
42    pub backend: SemanticBackend,
43    pub model: String,
44    pub base_url: Option<String>,
45    pub api_key_env: Option<String>,
46    pub timeout_ms: u64,
47    pub max_batch_size: usize,
48    /// Maximum number of project files to semantically index. Guards local
49    /// fastembed memory (model + embeddings + batch buffers) on huge project
50    /// roots; remote backends that embed server-side can raise it freely.
51    pub max_files: usize,
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
55pub struct UserServerDef {
56    pub id: String,
57    pub extensions: Vec<String>,
58    pub binary: String,
59    pub args: Vec<String>,
60    pub root_markers: Vec<String>,
61    pub env: HashMap<String, String>,
62    pub initialization_options: Option<serde_json::Value>,
63    pub disabled: bool,
64}
65
66impl Default for SemanticBackendConfig {
67    fn default() -> Self {
68        Self {
69            backend: SemanticBackend::Fastembed,
70            model: DEFAULT_SEMANTIC_MODEL.to_string(),
71            base_url: None,
72            api_key_env: None,
73            // Keep the default below the plugin bridge timeout to avoid bridge-killed
74            // semantic_search requests when callers do not set an explicit timeout.
75            timeout_ms: 25_000,
76            max_batch_size: 64,
77            max_files: 20_000,
78        }
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(default)]
84pub struct InspectConfig {
85    pub enabled: bool,
86}
87
88impl Default for InspectConfig {
89    fn default() -> Self {
90        Self { enabled: true }
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(default)]
96pub struct BackupConfig {
97    pub enabled: Option<bool>,
98    pub max_depth: Option<usize>,
99    pub max_file_size: Option<u64>,
100}
101
102impl Default for BackupConfig {
103    fn default() -> Self {
104        Self {
105            enabled: Some(true),
106            max_depth: Some(crate::backup::DEFAULT_MAX_UNDO_DEPTH),
107            max_file_size: None,
108        }
109    }
110}
111
112pub const DEFAULT_SEMANTIC_MODEL: &str = "all-MiniLM-L6-v2";
113
114impl Config {
115    pub fn semantic_backend_label(&self) -> &'static str {
116        self.semantic.backend.as_str()
117    }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(default)]
122pub struct Config {
123    /// Root directory of the project being analyzed. `None` if not scoped.
124    pub project_root: Option<PathBuf>,
125    /// How many levels of call-graph edges to follow during validation (default: 1).
126    pub validation_depth: u32,
127    /// Hours before a checkpoint expires and is eligible for cleanup (default: 24).
128    pub checkpoint_ttl_hours: u32,
129    /// Maximum depth for recursive symbol resolution (default: 10).
130    pub max_symbol_depth: u32,
131    /// Seconds before killing a formatter subprocess (default: 10).
132    pub formatter_timeout_secs: u32,
133    /// Seconds before killing a type-checker subprocess (default: 30).
134    pub type_checker_timeout_secs: u32,
135    /// Whether to auto-format files after edits (default: true).
136    pub format_on_edit: bool,
137    /// Whether to auto-validate files after edits (default: false).
138    /// When "syntax", only tree-sitter parse check. When "full", runs type checker.
139    pub validate_on_edit: Option<String>,
140    /// Per-language formatter overrides. Keys: "typescript", "python", "rust", "go".
141    /// Values: "biome", "oxfmt", "prettier", "deno", "ruff", "black", "rustfmt", "goimports", "gofmt", "none".
142    pub formatter: HashMap<String, String>,
143    /// Per-language type checker overrides. Keys: "typescript", "python", "rust", "go".
144    /// Values: "tsc", "tsgo", "biome", "pyright", "ruff", "cargo", "go", "staticcheck", "none".
145    pub checker: HashMap<String, String>,
146    /// Whether to restrict file operations to within `project_root` (default: false).
147    /// When true, write-capable commands reject paths outside the project root.
148    pub restrict_to_project_root: bool,
149    /// Enable the trigram search index (default: false).
150    pub search_index: bool,
151    /// Enable semantic search (default: false).
152    pub semantic_search: bool,
153    /// Whether the plugin registered the `aft_search` tool for this surface
154    /// (default: false). Forwarded by the plugin's resolved registration
155    /// predicate (semantic on + not minimal + not disabled). Used only to pick
156    /// the grep-rewrite footer: when true the footer steers to `aft_search`,
157    /// otherwise to the `grep` tool. Not a capability gate.
158    pub aft_search_registered: bool,
159    /// Enable the persisted callgraph store substrate (default: true).
160    pub callgraph_store: bool,
161    /// Number of files to parse in a single batch during callgraph store cold build (default: 100).
162    /// Lower values reduce peak memory during cold build.
163    /// Set to 0 to disable chunking and parse all files at once.
164    pub callgraph_chunk_size: usize,
165    /// Enable experimental bash command rewriting (default: false).
166    pub experimental_bash_rewrite: bool,
167    /// Enable experimental bash command compression (default: false).
168    pub experimental_bash_compress: bool,
169    /// Enable experimental bash background execution (default: false).
170    pub experimental_bash_background: bool,
171    /// Maximum number of background bash tasks allowed to run concurrently (default: 8).
172    pub max_background_bash_tasks: usize,
173    /// Emit reminders for long-running bash tasks (default: true).
174    pub bash_long_running_reminder_enabled: bool,
175    /// Milliseconds between long-running bash reminders (default: 10 minutes).
176    pub bash_long_running_reminder_interval_ms: u64,
177    /// Milliseconds to wait before a foreground bash task is promoted to background handling.
178    #[serde(skip, default = "default_foreground_wait_window_ms")]
179    pub foreground_wait_window_ms: u64,
180    /// Enable OpenCode-style bash permission prompts (default: false).
181    pub bash_permissions: bool,
182    /// Maximum file size to fully index in bytes (default: 1MB).
183    pub search_index_max_file_size: u64,
184    pub semantic: SemanticBackendConfig,
185    pub inspect: InspectConfig,
186    pub backup: BackupConfig,
187    /// Enable Astral ty as an experimental Python LSP server (default: false).
188    pub experimental_lsp_ty: bool,
189    /// User-defined LSP servers registered by the OpenCode plugin.
190    pub lsp_servers: Vec<UserServerDef>,
191    /// Lowercase LSP server IDs disabled by user config.
192    pub disabled_lsp: HashSet<String>,
193    /// Whether the system should request inline diagnostics after a tool call edits or writes a file.
194    #[serde(skip)]
195    pub diagnostics_on_edit: bool,
196    /// Extra directories to search when resolving LSP binaries.
197    /// The plugin populates these from its own auto-install cache (e.g.
198    /// `~/.cache/aft/lsp-packages/<pkg>/node_modules/.bin/`) so a binary AFT
199    /// installed itself is discoverable without needing it on PATH.
200    /// Resolution order: `<project_root>/node_modules/.bin/<bin>` →
201    /// `lsp_paths_extra/<bin>` (in order) → PATH via `which`.
202    pub lsp_paths_extra: Vec<PathBuf>,
203    /// Binary names the hosting plugin knows how to auto-install.
204    ///
205    /// Built-in LSPs discovered from files only emit missing-binary warnings
206    /// when their binary is in this set. User-configured `lsp_servers` keep
207    /// warning unconditionally.
208    pub lsp_auto_install_binaries: HashSet<String>,
209    /// Binary names with plugin-managed auto-installs currently in flight.
210    ///
211    /// Missing-binary warnings are suppressed while the install is actively
212    /// running; install failure reporting is handled by the plugin after the
213    /// background work settles.
214    pub lsp_inflight_installs: HashSet<String>,
215    /// Persistent storage directory for indexes (trigram, semantic).
216    /// Set by the plugin to the XDG-compliant path (e.g. ~/.local/share/opencode/storage/plugin/aft/).
217    /// Falls back to ~/.cache/aft/ if not set.
218    pub storage_dir: Option<PathBuf>,
219    /// Allow URL-fetch commands to access private network hosts.
220    /// Default false; hosting plugins only forward this from user-level config.
221    pub url_fetch_allow_private: bool,
222    /// Hosting harness identity supplied by configure.
223    #[serde(default)]
224    pub harness: Option<Harness>,
225    /// Maximum number of (server, file) entries kept in the in-memory
226    /// diagnostic cache. Older entries are evicted in LRU order when the
227    /// cap is exceeded. Set to 0 to disable the cap entirely.
228    /// Default: 5000 (covers very large monorepos with bounded memory).
229    pub diagnostic_cache_size: usize,
230}
231
232impl Default for Config {
233    fn default() -> Self {
234        Config {
235            project_root: None,
236            validation_depth: 1,
237            checkpoint_ttl_hours: 24,
238            max_symbol_depth: 10,
239            formatter_timeout_secs: 10,
240            type_checker_timeout_secs: 30,
241            // Default OFF: formatting after an edit can silently reflow the file
242            // under the agent (a formatter splitting/joining lines), staling the
243            // context for the next edit/patch. Agents that want formatting opt in
244            // via `format_on_edit: true`.
245            format_on_edit: false,
246            validate_on_edit: None,
247            formatter: HashMap::new(),
248            checker: HashMap::new(),
249            // Default to false to match OpenCode's existing permission-based model.
250            // The plugin opts into root restriction explicitly when desired.
251            restrict_to_project_root: false,
252            search_index: false,
253            semantic_search: false,
254            aft_search_registered: false,
255            callgraph_store: true,
256            callgraph_chunk_size: 100,
257            experimental_bash_rewrite: false,
258            experimental_bash_compress: false,
259            experimental_bash_background: false,
260            max_background_bash_tasks: 8,
261            bash_long_running_reminder_enabled: true,
262            bash_long_running_reminder_interval_ms: 600_000,
263            foreground_wait_window_ms: default_foreground_wait_window_ms(),
264            bash_permissions: false,
265            search_index_max_file_size: 1_048_576,
266            semantic: SemanticBackendConfig::default(),
267            inspect: InspectConfig::default(),
268            backup: BackupConfig::default(),
269            experimental_lsp_ty: false,
270            lsp_servers: Vec::new(),
271            disabled_lsp: HashSet::new(),
272            diagnostics_on_edit: false,
273            lsp_paths_extra: Vec::new(),
274            lsp_auto_install_binaries: HashSet::new(),
275            lsp_inflight_installs: HashSet::new(),
276            storage_dir: None,
277            url_fetch_allow_private: false,
278            harness: None,
279            diagnostic_cache_size: 5000,
280        }
281    }
282}
283
284fn default_foreground_wait_window_ms() -> u64 {
285    15_000
286}