Skip to main content

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