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