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