feat: add local agent MCP tools and optional source-grounded AI answers
Core CI / core (ubuntu-latest) (push) Failing after 3m6s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m41s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s

This commit is contained in:
karl committed 2026-10-12 20:28:38 +02:00
1 parent 70746ef428
commit f5b03c6fb0
27 files changed
+2988 -13

No files matched your search

+76
View File
@@ -929,6 +929,82 @@ impl SearchIndex {
)?)
}
/// Indexed file version for bounded agent handles. Size and nanosecond
/// modification time must be compared again before serving the source.
pub fn file_signature(&self, path: &str) -> Result<Option<(u64, i64)>> {
self.connection
.query_row(
"SELECT size,mtime FROM files WHERE path=?1 AND size>=0 AND mtime>=0",
[path],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.map_err(Into::into)
}
/// Read an indexed document source for agent citations or explicit answers.
/// No file content is read: the canonical source must still match the indexed
/// size and nanosecond modification time. Callers must disclose this bounded
/// source excerpt before sending it to an optional external model.
pub fn source_passage(
&self,
path: &str,
source: &SourceLocation,
max_chars: usize,
) -> Result<Option<IndexedPassage>> {
if !matches!(source.kind.as_str(), "page" | "paragraph" | "line")
|| max_chars == 0
|| max_chars > 8192
{
bail!("Invalid document source or excerpt limit");
}
// Check the opaque indexed identity before touching a caller-controlled
// filesystem path (including UNC paths on Windows).
if !self.contains_path(path)? {
return Ok(None);
}
let canonical = match fs::canonicalize(path) {
Ok(value) => value,
Err(_) => return Ok(None),
};
let metadata = match fs::metadata(&canonical) {
Ok(value) if value.is_file() => value,
_ => return Ok(None),
};
let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?;
let size = i64::try_from(metadata.len())?;
let modified = i64::try_from(mtime.as_nanos())?;
self.connection
.query_row(
"SELECT p.id,f.path,f.filename,substr(p.text,1,?6),p.kind,p.number
FROM passages p JOIN files f ON f.path=p.file_path
WHERE f.path=?1 AND p.kind=?2 AND p.number=?3
AND f.size=?4 AND f.mtime=?5 ORDER BY p.id LIMIT 1",
params![
canonical.to_string_lossy(),
source.kind,
source.number,
size,
modified,
max_chars as i64
],
|row| {
Ok(IndexedPassage {
id: row.get(0)?,
path: row.get(1)?,
filename: row.get(2)?,
text: row.get(3)?,
source: SourceLocation {
kind: row.get(4)?,
number: row.get(5)?,
},
})
},
)
.optional()
.map_err(Into::into)
}
pub fn passage_batch(&self, after_id: i64, limit: usize) -> Result<Vec<IndexedPassage>> {
let mut statement = self.connection.prepare("SELECT p.rowid,f.path,f.filename,p.text,p.kind,p.number
FROM passages p JOIN files f ON f.path=p.file_path WHERE p.rowid>?1 ORDER BY p.rowid LIMIT ?2")?;