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