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