From a06dd40b0b614d89026325756d8d727e3a116830 Mon Sep 17 00:00:00 2001
From: Sarthak Agrawal
Date: Wed, 2 Sep 2026 21:52:27 +0530
Subject: [PATCH 1/4] feat: stage verification backend modules 4/4
---
.../src-tauri/src/commands/tool_collectors.rs | 843 ++++++++++++++++++
1 file changed, 843 insertions(+)
create mode 100644 apps/desktop/src-tauri/src/commands/tool_collectors.rs
diff --git a/apps/desktop/src-tauri/src/commands/tool_collectors.rs b/apps/desktop/src-tauri/src/commands/tool_collectors.rs
new file mode 100644
index 00000000..fe2a96de
--- /dev/null
+++ b/apps/desktop/src-tauri/src/commands/tool_collectors.rs
@@ -0,0 +1,843 @@
+//! Local, bounded adapters for external verification collectors.
+//!
+//! Collectors remain subordinate evidence. This module owns source identity,
+//! process limits, redaction, and the normalized receipt; a tool exit code does
+//! not become a CodeVetter verdict.
+
+use std::collections::BTreeSet;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+use std::time::{Duration, Instant};
+
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use sha2::{Digest, Sha256};
+use tokio::io::{AsyncRead, AsyncReadExt};
+use tokio::process::Command;
+
+use super::trex_preview::{resolve_scope_change, TrexSourceReceipt};
+
+const GITLEAKS_VERSION: &str = "8.30.1";
+const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120);
+const MAX_STDOUT_BYTES: usize = 256 * 1024;
+const MAX_STDERR_BYTES: usize = 256 * 1024;
+const MAX_REPORT_BYTES: usize = 8 * 1024 * 1024;
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(rename_all = "kebab-case")]
+pub enum CollectorKind {
+ Gitleaks,
+ CargoAudit,
+ CargoLlvmCov,
+}
+
+impl CollectorKind {
+ pub fn parse(value: &str) -> Result {
+ match value {
+ "gitleaks" => Ok(Self::Gitleaks),
+ "cargo-audit" => Ok(Self::CargoAudit),
+ "cargo-llvm-cov" => Ok(Self::CargoLlvmCov),
+ _ => Err(format!(
+ "unsupported collector `{value}`; expected gitleaks, cargo-audit, or cargo-llvm-cov"
+ )),
+ }
+ }
+
+ fn binary_name(self) -> &'static str {
+ match self {
+ Self::Gitleaks => "gitleaks",
+ Self::CargoAudit => "cargo-audit",
+ Self::CargoLlvmCov => "cargo-llvm-cov",
+ }
+ }
+
+ fn expected_version(self) -> &'static str {
+ match self {
+ Self::Gitleaks => GITLEAKS_VERSION,
+ Self::CargoAudit => "0.22.2",
+ Self::CargoLlvmCov => "0.9.0",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum CollectorStatus {
+ Clean,
+ Findings,
+ Unavailable,
+ Error,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct CollectorToolIdentity {
+ pub name: String,
+ pub version: String,
+ pub source: String,
+ pub sha256: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CollectorResult {
+ pub collector: CollectorKind,
+ pub status: CollectorStatus,
+ pub duration_ms: u64,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub tool: Option,
+ pub finding_count: usize,
+ pub evidence: Value,
+ pub limitations: Vec,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ToolCollectionReceipt {
+ pub schema_version: String,
+ pub ran_at: String,
+ pub repo_path: String,
+ pub source: TrexSourceReceipt,
+ pub collectors: Vec,
+ pub limitations: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub struct ToolCollectionInput {
+ pub repo_path: PathBuf,
+ pub change: String,
+ pub collectors: Vec,
+}
+
+#[derive(Debug, Clone, Default)]
+struct ToolPaths {
+ gitleaks: Option,
+ cargo_audit: Option,
+ cargo_llvm_cov: Option,
+}
+
+#[derive(Debug, Clone)]
+struct ProductBinary {
+ path: PathBuf,
+ source: &'static str,
+}
+
+impl ToolPaths {
+ fn product() -> Self {
+ Self {
+ gitleaks: resolve_product_binary(
+ "CODEVETTER_GITLEAKS_BIN",
+ CollectorKind::Gitleaks.binary_name(),
+ ),
+ cargo_audit: resolve_product_binary(
+ "CODEVETTER_CARGO_AUDIT_BIN",
+ CollectorKind::CargoAudit.binary_name(),
+ ),
+ cargo_llvm_cov: resolve_product_binary(
+ "CODEVETTER_CARGO_LLVM_COV_BIN",
+ CollectorKind::CargoLlvmCov.binary_name(),
+ ),
+ }
+ }
+
+ fn get(&self, kind: CollectorKind) -> Option<&ProductBinary> {
+ match kind {
+ CollectorKind::Gitleaks => self.gitleaks.as_ref(),
+ CollectorKind::CargoAudit => self.cargo_audit.as_ref(),
+ CollectorKind::CargoLlvmCov => self.cargo_llvm_cov.as_ref(),
+ }
+ }
+}
+
+pub async fn collect_tool_evidence(
+ input: ToolCollectionInput,
+) -> Result {
+ collect_tool_evidence_with_paths(input, ToolPaths::product()).await
+}
+
+async fn collect_tool_evidence_with_paths(
+ input: ToolCollectionInput,
+ paths: ToolPaths,
+) -> Result {
+ if input.collectors.is_empty() {
+ return Err("At least one collector is required".into());
+ }
+ let repo = canonical_clean_repository(&input.repo_path)?;
+ let repo_text = repo.to_string_lossy().into_owned();
+ let source = resolve_scope_change(&repo_text, &input.change).await?;
+ require_checked_out_head(&repo, &source.head_sha)?;
+ let selected = input.collectors.into_iter().collect::>();
+ let mut collectors = Vec::with_capacity(selected.len());
+ for kind in selected {
+ let started = Instant::now();
+ let result = match (kind, paths.get(kind)) {
+ (CollectorKind::Gitleaks, Some(binary)) => {
+ run_gitleaks(binary, &repo, &source, started).await
+ }
+ (_, Some(binary)) => preflight_only(kind, binary, started).await,
+ (_, None) => unavailable(
+ kind,
+ started,
+ format!(
+ "The pinned {} {} product binary is not bundled or explicitly provided",
+ kind.binary_name(),
+ kind.expected_version()
+ ),
+ ),
+ };
+ collectors.push(result);
+ }
+ let limitations = collectors
+ .iter()
+ .filter(|result| {
+ matches!(
+ result.status,
+ CollectorStatus::Unavailable | CollectorStatus::Error
+ )
+ })
+ .flat_map(|result| result.limitations.iter().cloned())
+ .collect();
+ Ok(ToolCollectionReceipt {
+ schema_version: "codevetter.tool-collection/v1".into(),
+ ran_at: chrono::Utc::now().to_rfc3339(),
+ repo_path: repo_text,
+ source,
+ collectors,
+ limitations,
+ })
+}
+
+async fn run_gitleaks(
+ binary: &ProductBinary,
+ repo: &Path,
+ source: &TrexSourceReceipt,
+ started: Instant,
+) -> CollectorResult {
+ let identity = match tool_identity(CollectorKind::Gitleaks, binary).await {
+ Ok(identity) => identity,
+ Err(error) => return error_result(CollectorKind::Gitleaks, started, error),
+ };
+ let range = format!("{}..{}", source.base_sha, source.head_sha);
+ let mut command = Command::new(&binary.path);
+ command
+ .args([
+ "git",
+ "--no-banner",
+ "--no-color",
+ "--redact=100",
+ "--report-format",
+ "json",
+ "--report-path",
+ "-",
+ ])
+ .arg("--log-opts")
+ .arg(&range)
+ .arg(repo)
+ .current_dir(repo)
+ .env_clear()
+ .env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin")
+ .env("NO_COLOR", "1")
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .kill_on_drop(true);
+
+ let execution =
+ execute_bounded(command, DEFAULT_TIMEOUT, MAX_REPORT_BYTES, MAX_STDERR_BYTES).await;
+ let result = match execution {
+ Err(error) => error_result(CollectorKind::Gitleaks, started, error),
+ Ok(output) if !matches!(output.code, 0 | 1) => error_result(
+ CollectorKind::Gitleaks,
+ started,
+ format!(
+ "gitleaks exited with {}: {}",
+ output.code,
+ safe_diagnostic(&output.stderr)
+ ),
+ ),
+ Ok(output) => match read_gitleaks_report(&output.stdout)
+ .and_then(|findings| validate_gitleaks_attribution(findings, source))
+ {
+ Ok(findings) => {
+ let finding_count = findings.len();
+ let status = if finding_count == 0 {
+ CollectorStatus::Clean
+ } else {
+ CollectorStatus::Findings
+ };
+ CollectorResult {
+ collector: CollectorKind::Gitleaks,
+ status,
+ duration_ms: elapsed_ms(started),
+ tool: Some(identity),
+ finding_count,
+ evidence: json!({
+ "range": range,
+ "configuration": gitleaks_configuration_identity(repo),
+ "redaction": "secret and match values dropped before normalization",
+ "findings": findings,
+ "process_exit_code": output.code,
+ }),
+ limitations: Vec::new(),
+ }
+ }
+ Err(error) => error_result(CollectorKind::Gitleaks, started, error),
+ },
+ };
+ result
+}
+
+async fn preflight_only(
+ kind: CollectorKind,
+ binary: &ProductBinary,
+ started: Instant,
+) -> CollectorResult {
+ match tool_identity(kind, binary).await {
+ Ok(identity) => CollectorResult {
+ collector: kind,
+ status: CollectorStatus::Unavailable,
+ duration_ms: elapsed_ms(started),
+ tool: Some(identity),
+ finding_count: 0,
+ evidence: json!({"preflight": "binary identity verified"}),
+ limitations: vec![format!(
+ "{} execution remains claim-closed until its offline data/toolchain prerequisite is packaged and qualified",
+ kind.binary_name()
+ )],
+ },
+ Err(error) => error_result(kind, started, error),
+ }
+}
+
+async fn tool_identity(
+ kind: CollectorKind,
+ binary: &ProductBinary,
+) -> Result {
+ if !binary.path.is_file() {
+ return Err(format!("{} binary is missing", kind.binary_name()));
+ }
+ let argument = match kind {
+ CollectorKind::Gitleaks => "version",
+ CollectorKind::CargoAudit | CollectorKind::CargoLlvmCov => "--version",
+ };
+ let mut command = Command::new(&binary.path);
+ command
+ .arg(argument)
+ .env_clear()
+ .env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin")
+ .env("NO_COLOR", "1")
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .kill_on_drop(true);
+ let output = execute_bounded(
+ command,
+ Duration::from_secs(10),
+ MAX_STDOUT_BYTES,
+ MAX_STDERR_BYTES,
+ )
+ .await?;
+ if output.code != 0 {
+ return Err(format!(
+ "{} version probe exited with {}",
+ kind.binary_name(),
+ output.code
+ ));
+ }
+ let version = String::from_utf8(output.stdout)
+ .map_err(|_| format!("{} returned a non-UTF-8 version", kind.binary_name()))?;
+ if !version
+ .split_ascii_whitespace()
+ .any(|token| token.trim_start_matches('v') == kind.expected_version())
+ {
+ return Err(format!(
+ "{} version mismatch: expected {}, received {}",
+ kind.binary_name(),
+ kind.expected_version(),
+ safe_diagnostic(version.as_bytes())
+ ));
+ }
+ Ok(CollectorToolIdentity {
+ name: kind.binary_name().into(),
+ version: kind.expected_version().into(),
+ source: binary.source.into(),
+ sha256: sha256_file(&binary.path)?,
+ })
+}
+
+fn sha256_file(path: &Path) -> Result {
+ let mut file = std::fs::File::open(path)
+ .map_err(|error| format!("open {} binary for identity: {error}", path.display()))?;
+ let mut digest = Sha256::new();
+ let mut buffer = [0_u8; 64 * 1024];
+ loop {
+ let read = file
+ .read(&mut buffer)
+ .map_err(|error| format!("hash {} binary: {error}", path.display()))?;
+ if read == 0 {
+ break;
+ }
+ digest.update(&buffer[..read]);
+ }
+ Ok(format!("{:x}", digest.finalize()))
+}
+
+#[derive(Debug)]
+struct ProcessOutput {
+ code: i32,
+ stdout: Vec,
+ stderr: Vec,
+}
+
+async fn execute_bounded(
+ mut command: Command,
+ timeout: Duration,
+ stdout_limit: usize,
+ stderr_limit: usize,
+) -> Result {
+ let mut child = command
+ .spawn()
+ .map_err(|error| format!("launch collector: {error}"))?;
+ let stdout = child
+ .stdout
+ .take()
+ .ok_or_else(|| "collector stdout was unavailable".to_string())?;
+ let stderr = child
+ .stderr
+ .take()
+ .ok_or_else(|| "collector stderr was unavailable".to_string())?;
+ let execution = tokio::time::timeout(timeout, async {
+ let (status, stdout, stderr) = tokio::join!(
+ child.wait(),
+ read_capped(stdout, stdout_limit),
+ read_capped(stderr, stderr_limit)
+ );
+ (status, stdout, stderr)
+ })
+ .await;
+ let (status, stdout, stderr) = match execution {
+ Ok(output) => output,
+ Err(_) => {
+ let _ = child.kill().await;
+ return Err(format!(
+ "collector exceeded the {} second limit",
+ timeout.as_secs()
+ ));
+ }
+ };
+ let status = status.map_err(|error| format!("wait for collector: {error}"))?;
+ let (stdout, stdout_exceeded) =
+ stdout.map_err(|error| format!("read collector stdout: {error}"))?;
+ let (stderr, stderr_exceeded) =
+ stderr.map_err(|error| format!("read collector stderr: {error}"))?;
+ if stdout_exceeded || stderr_exceeded {
+ return Err("collector output exceeded the bounded evidence limit".into());
+ }
+ Ok(ProcessOutput {
+ code: status.code().unwrap_or(-1),
+ stdout,
+ stderr,
+ })
+}
+
+async fn read_capped(
+ mut reader: R,
+ limit: usize,
+) -> std::io::Result<(Vec, bool)> {
+ let mut output = Vec::with_capacity(limit.min(64 * 1024));
+ let mut exceeded = false;
+ let mut buffer = [0_u8; 8192];
+ loop {
+ let read = reader.read(&mut buffer).await?;
+ if read == 0 {
+ break;
+ }
+ let remaining = limit.saturating_sub(output.len());
+ output.extend_from_slice(&buffer[..read.min(remaining)]);
+ exceeded |= read > remaining;
+ }
+ Ok((output, exceeded))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct RawGitleaksFinding {
+ #[serde(rename = "RuleID")]
+ rule_id: String,
+ #[serde(default)]
+ description: String,
+ file: String,
+ #[serde(default)]
+ start_line: u64,
+ #[serde(default)]
+ end_line: u64,
+ #[serde(default)]
+ commit: String,
+ #[serde(default)]
+ fingerprint: String,
+}
+
+#[derive(Debug, Serialize)]
+struct GitleaksFinding {
+ rule_id: String,
+ description: String,
+ file: String,
+ start_line: u64,
+ end_line: u64,
+ commit: String,
+ fingerprint: String,
+}
+
+fn read_gitleaks_report(bytes: &[u8]) -> Result, String> {
+ if bytes.len() > MAX_REPORT_BYTES {
+ return Err("gitleaks report is oversized".into());
+ }
+ let raw_findings: Vec =
+ serde_json::from_slice(bytes).map_err(|error| format!("parse gitleaks report: {error}"))?;
+ let mut findings = raw_findings
+ .into_iter()
+ .map(|finding| {
+ Ok(GitleaksFinding {
+ rule_id: safe_text(&finding.rule_id, 120),
+ description: safe_text(&finding.description, 240),
+ file: normalize_relative_path(&finding.file)?,
+ start_line: finding.start_line,
+ end_line: finding.end_line,
+ commit: safe_hex(&finding.commit, 40),
+ fingerprint: safe_text(&finding.fingerprint, 240),
+ })
+ })
+ .collect::, String>>()?;
+ findings.sort_by(|left, right| {
+ (&left.file, left.start_line, &left.rule_id).cmp(&(
+ &right.file,
+ right.start_line,
+ &right.rule_id,
+ ))
+ });
+ Ok(findings)
+}
+
+fn validate_gitleaks_attribution(
+ findings: Vec,
+ source: &TrexSourceReceipt,
+) -> Result, String> {
+ let commits = source
+ .commits
+ .iter()
+ .map(String::as_str)
+ .collect::>();
+ let changed_paths = source
+ .changed_paths
+ .iter()
+ .map(String::as_str)
+ .collect::>();
+ if findings.iter().any(|finding| {
+ !commits.contains(finding.commit.as_str()) || !changed_paths.contains(finding.file.as_str())
+ }) {
+ return Err("gitleaks returned a finding outside the resolved change".into());
+ }
+ Ok(findings)
+}
+
+fn gitleaks_configuration_identity(repo: &Path) -> Value {
+ for name in [".gitleaks.toml", "gitleaks.toml"] {
+ let path = repo.join(name);
+ if let Ok(bytes) = std::fs::read(&path) {
+ return json!({
+ "source": name,
+ "sha256": format!("{:x}", Sha256::digest(bytes)),
+ });
+ }
+ }
+ json!({"source": "gitleaks-8.30.1-embedded-default"})
+}
+
+fn normalize_relative_path(value: &str) -> Result {
+ let normalized = value.replace('\\', "/");
+ let path = Path::new(&normalized);
+ if path.is_absolute()
+ || path
+ .components()
+ .any(|part| matches!(part, std::path::Component::ParentDir))
+ {
+ return Err("gitleaks returned a non-contained finding path".into());
+ }
+ Ok(normalized)
+}
+
+fn canonical_clean_repository(path: &Path) -> Result {
+ let canonical = path
+ .canonicalize()
+ .map_err(|error| format!("repository {} is unavailable: {error}", path.display()))?;
+ if !canonical.is_dir() {
+ return Err("Tool collection requires a Git repository root".into());
+ }
+ let reported_root = git_text(&canonical, &["rev-parse", "--show-toplevel"])?;
+ let reported_root = PathBuf::from(reported_root)
+ .canonicalize()
+ .map_err(|error| format!("canonicalize Git repository root: {error}"))?;
+ if reported_root != canonical {
+ return Err("Tool collection requires the Git repository root, not a subdirectory".into());
+ }
+ if !git_text(
+ &canonical,
+ &["status", "--porcelain=v1", "--untracked-files=normal"],
+ )?
+ .is_empty()
+ {
+ return Err(
+ "Tool collection requires a clean checkout so evidence maps to one immutable source"
+ .into(),
+ );
+ }
+ Ok(canonical)
+}
+
+fn require_checked_out_head(repo: &Path, expected: &str) -> Result<(), String> {
+ let head = git_text(repo, &["rev-parse", "HEAD"])?;
+ if head != expected {
+ return Err("Resolved change head is not the checked-out repository HEAD".into());
+ }
+ Ok(())
+}
+
+fn git_text(repo: &Path, arguments: &[&str]) -> Result {
+ let output = std::process::Command::new("git")
+ .args(arguments)
+ .current_dir(repo)
+ .stdin(Stdio::null())
+ .output()
+ .map_err(|error| format!("Could not run Git: {error}"))?;
+ if !output.status.success() {
+ return Err(format!(
+ "Git could not inspect the local checkout: {}",
+ safe_diagnostic(&output.stderr)
+ ));
+ }
+ String::from_utf8(output.stdout)
+ .map(|value| value.trim().to_string())
+ .map_err(|_| "Git returned non-UTF-8 evidence".into())
+}
+
+fn resolve_product_binary(variable: &str, name: &str) -> Option {
+ #[cfg(any(test, debug_assertions))]
+ {
+ if let Some(path) = std::env::var_os(variable).map(PathBuf::from) {
+ if path.is_file() {
+ return Some(ProductBinary {
+ path,
+ source: "explicit_debug_override",
+ });
+ }
+ }
+ }
+ #[cfg(not(any(test, debug_assertions)))]
+ let _ = variable;
+ std::env::current_exe()
+ .ok()
+ .and_then(|executable| executable.parent().map(|parent| parent.join(name)))
+ .filter(|path| path.is_file())
+ .map(|path| ProductBinary {
+ path,
+ source: "application_bundle_sibling",
+ })
+}
+
+fn unavailable(kind: CollectorKind, started: Instant, reason: String) -> CollectorResult {
+ CollectorResult {
+ collector: kind,
+ status: CollectorStatus::Unavailable,
+ duration_ms: elapsed_ms(started),
+ tool: None,
+ finding_count: 0,
+ evidence: json!({"preflight": "unavailable"}),
+ limitations: vec![reason],
+ }
+}
+
+fn error_result(kind: CollectorKind, started: Instant, reason: String) -> CollectorResult {
+ CollectorResult {
+ collector: kind,
+ status: CollectorStatus::Error,
+ duration_ms: elapsed_ms(started),
+ tool: None,
+ finding_count: 0,
+ evidence: json!({"error_category": "collector_execution"}),
+ limitations: vec![safe_text(&reason, 500)],
+ }
+}
+
+fn safe_diagnostic(bytes: &[u8]) -> String {
+ safe_text(&String::from_utf8_lossy(bytes), 500)
+}
+
+fn safe_text(value: &str, limit: usize) -> String {
+ value
+ .chars()
+ .filter_map(|character| match character {
+ '\n' | '\r' | '\t' => Some(' '),
+ value if value.is_control() => None,
+ value => Some(value),
+ })
+ .take(limit)
+ .collect::()
+ .split_whitespace()
+ .collect::>()
+ .join(" ")
+}
+
+fn safe_hex(value: &str, limit: usize) -> String {
+ value
+ .chars()
+ .filter(|character| character.is_ascii_hexdigit())
+ .take(limit)
+ .collect()
+}
+
+fn elapsed_ms(started: Instant) -> u64 {
+ started.elapsed().as_millis().try_into().unwrap_or(u64::MAX)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::unix::fs::PermissionsExt;
+ use tempfile::TempDir;
+
+ fn executable(path: &Path, contents: &str) {
+ std::fs::write(path, contents).expect("write fixture executable");
+ let mut permissions = std::fs::metadata(path)
+ .expect("fixture metadata")
+ .permissions();
+ permissions.set_mode(0o755);
+ std::fs::set_permissions(path, permissions).expect("fixture permissions");
+ }
+
+ fn repository() -> (TempDir, String, String) {
+ let directory = tempfile::tempdir().expect("temp repository");
+ let repo = directory.path();
+ for args in [
+ vec!["init"],
+ vec!["config", "user.email", "fixture@example.com"],
+ vec!["config", "user.name", "Fixture"],
+ ] {
+ assert!(std::process::Command::new("git")
+ .args(args)
+ .current_dir(repo)
+ .status()
+ .expect("git")
+ .success());
+ }
+ std::fs::write(repo.join("README.md"), "base\n").expect("base file");
+ assert!(std::process::Command::new("git")
+ .args(["add", "README.md"])
+ .current_dir(repo)
+ .status()
+ .expect("git add")
+ .success());
+ assert!(std::process::Command::new("git")
+ .args(["commit", "-m", "base"])
+ .current_dir(repo)
+ .status()
+ .expect("git commit")
+ .success());
+ let base = git_text(repo, &["rev-parse", "HEAD"]).expect("base sha");
+ std::fs::write(repo.join("README.md"), "base\nchange\n").expect("changed file");
+ assert!(std::process::Command::new("git")
+ .args(["add", "README.md"])
+ .current_dir(repo)
+ .status()
+ .expect("git add")
+ .success());
+ assert!(std::process::Command::new("git")
+ .args(["commit", "-m", "change"])
+ .current_dir(repo)
+ .status()
+ .expect("git commit")
+ .success());
+ let head = git_text(repo, &["rev-parse", "HEAD"]).expect("head sha");
+ (directory, base, head)
+ }
+
+ #[tokio::test]
+ async fn gitleaks_receipt_drops_raw_secret_fields() {
+ let (repo, base, head) = repository();
+ let tools = tempfile::tempdir().expect("tool directory");
+ let binary = tools.path().join("gitleaks");
+ executable(
+ &binary,
+ r##"#!/bin/sh
+if [ "$1" = "version" ]; then printf '8.30.1\n'; exit 0; fi
+printf '[{"RuleID":"fixture-token","Description":"fixture","File":"README.md","StartLine":2,"EndLine":2,"Commit":"HEAD_SHA","Fingerprint":"safe-fingerprint","Secret":"must-not-survive","Match":"token=must-not-survive"}]'
+exit 1
+"##,
+ );
+ let script = std::fs::read_to_string(&binary)
+ .expect("read fixture executable")
+ .replace("HEAD_SHA", &head);
+ executable(&binary, &script);
+ let receipt = collect_tool_evidence_with_paths(
+ ToolCollectionInput {
+ repo_path: repo.path().into(),
+ change: format!("{base}..{head}"),
+ collectors: vec![CollectorKind::Gitleaks],
+ },
+ ToolPaths {
+ gitleaks: Some(ProductBinary {
+ path: binary,
+ source: "test_fixture",
+ }),
+ ..ToolPaths::default()
+ },
+ )
+ .await
+ .expect("collector receipt");
+ assert_eq!(receipt.collectors[0].status, CollectorStatus::Findings);
+ assert_eq!(receipt.collectors[0].finding_count, 1);
+ let serialized = serde_json::to_string(&receipt).expect("serialize receipt");
+ assert!(!serialized.contains("must-not-survive"));
+ assert!(!serialized.contains("\"Secret\""));
+ assert!(!serialized.contains("\"Match\""));
+ }
+
+ #[tokio::test]
+ async fn missing_product_tools_are_explicitly_unavailable() {
+ let (repo, base, head) = repository();
+ let receipt = collect_tool_evidence_with_paths(
+ ToolCollectionInput {
+ repo_path: repo.path().into(),
+ change: format!("{base}..{head}"),
+ collectors: vec![CollectorKind::CargoAudit, CollectorKind::CargoLlvmCov],
+ },
+ ToolPaths::default(),
+ )
+ .await
+ .expect("collector receipt");
+ assert!(receipt
+ .collectors
+ .iter()
+ .all(|collector| collector.status == CollectorStatus::Unavailable));
+ assert_eq!(receipt.limitations.len(), 2);
+ }
+
+ #[test]
+ fn gitleaks_findings_must_belong_to_the_resolved_change() {
+ let source = TrexSourceReceipt {
+ kind: super::super::trex_preview::TrexChangeKind::Range,
+ input: "base..head".into(),
+ base_sha: "a".repeat(40),
+ head_sha: "b".repeat(40),
+ commits: vec!["b".repeat(40)],
+ changed_paths: vec!["src/changed.rs".into()],
+ };
+ let finding = GitleaksFinding {
+ rule_id: "fixture".into(),
+ description: "fixture".into(),
+ file: "src/outside.rs".into(),
+ start_line: 1,
+ end_line: 1,
+ commit: "c".repeat(40),
+ fingerprint: "fixture".into(),
+ };
+ assert!(validate_gitleaks_attribution(vec![finding], &source).is_err());
+ }
+}
From 80d4c54545e0f91e7253a6c2f85246b6d4ee6673 Mon Sep 17 00:00:00 2001
From: Sarthak Agrawal
Date: Wed, 2 Sep 2026 23:39:09 +0530
Subject: [PATCH 2/4] fix(ci): keep integration layer type-safe
---
.../differential-daemon-contracts.ts | 23 +++-
apps/desktop/src/pages/Rubrics.tsx | 4 -
apps/desktop/src/pages/Settings.tsx | 124 ------------------
3 files changed, 22 insertions(+), 129 deletions(-)
diff --git a/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts b/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
index c13159c9..6a7bc81d 100644
--- a/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
+++ b/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
@@ -7,6 +7,7 @@ import {
VERIFY_CONTRACT_LIMITS,
type ContractIssue,
type ContractValidation,
+ type DaemonError,
} from './contracts';
import {
DIFFERENTIAL_CLASSIFICATIONS,
@@ -122,7 +123,8 @@ export type DifferentialDaemonResponse =
| { type: 'differential_prepared'; summary: DifferentialPreparedSummary }
| { type: 'differential_result'; summary: DifferentialRunSummary }
| { type: 'differential_status'; summary: DifferentialStatusSummary }
- | { type: 'differential_cleanup'; summary: DifferentialCleanupSummary };
+ | { type: 'differential_cleanup'; summary: DifferentialCleanupSummary }
+ | { type: 'error'; error: DaemonError };
export interface DifferentialDaemonResponseEnvelope {
protocol_version: 1;
request_id: string;
@@ -287,6 +289,25 @@ function validateRequest(value: unknown, issues: ContractIssue[]) {
function validateResponse(value: unknown, issues: ContractIssue[]) {
const response = object(value, '$.response', issues);
if (!response) return;
+ if (response.type === 'error') {
+ exactKeys(response, '$.response', ['type', 'error'], issues);
+ const error = object(response.error, '$.response.error', issues);
+ if (!error) return;
+ exactKeys(
+ error,
+ '$.response.error',
+ error.remediation === undefined
+ ? ['code', 'message', 'retryable']
+ : ['code', 'message', 'remediation', 'retryable'],
+ issues
+ );
+ stringField(error, 'code', '$.response.error', issues, { pattern: ID });
+ stringField(error, 'message', '$.response.error', issues);
+ if (error.remediation !== undefined)
+ stringField(error, 'remediation', '$.response.error', issues);
+ boolean(error, 'retryable', '$.response.error', issues);
+ return;
+ }
exactKeys(response, '$.response', ['type', 'summary'], issues);
const rules: Record = {
differential_prepared: prepared,
diff --git a/apps/desktop/src/pages/Rubrics.tsx b/apps/desktop/src/pages/Rubrics.tsx
index 41eaada9..3e2bdc72 100644
--- a/apps/desktop/src/pages/Rubrics.tsx
+++ b/apps/desktop/src/pages/Rubrics.tsx
@@ -27,10 +27,6 @@ import { getStandardsPackUsage, isTauriAvailable } from '@/lib/tauri-ipc';
function fallbackConfig(): ReviewConfig {
return {
- gatewayBaseUrl: '',
- gatewayApiKey: '',
- gatewayModel: 'auto',
- reviewTone: 'direct',
activeStandardsPack: DEFAULT_STANDARDS_PACKS[0].id,
standardsPacks: [],
};
diff --git a/apps/desktop/src/pages/Settings.tsx b/apps/desktop/src/pages/Settings.tsx
index 0d0eca8d..8557710d 100644
--- a/apps/desktop/src/pages/Settings.tsx
+++ b/apps/desktop/src/pages/Settings.tsx
@@ -6,12 +6,6 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
-import {
- loadReviewConfig,
- PROVIDER_PRESETS,
- type ReviewConfig,
- saveReviewConfig,
-} from '@/lib/review-service';
import type {
GitHubAuthStatus,
LinearUser,
@@ -590,48 +584,6 @@ export default function Settings() {
const [claudeCodePath, setClaudeCodePath] = usePref('claude_cli_path', '');
const [codexPath, setCodexPath] = usePref('codex_cli_path', '');
- // AI Provider
- const [aiProvider, setAiProvider] = useState('anthropic');
- const [aiBaseUrl, setAiBaseUrl] = useState('');
- const [aiApiKey, setAiApiKey] = useState('');
- const [aiModel, setAiModel] = useState('');
- const [aiConfigSaved, setAiConfigSaved] = useState(false);
-
- useEffect(() => {
- const existing = loadReviewConfig();
- if (existing) {
- setAiBaseUrl(existing.gatewayBaseUrl);
- setAiApiKey(existing.gatewayApiKey);
- setAiModel(existing.gatewayModel);
- // Detect provider from URL
- if (existing.gatewayBaseUrl.includes('anthropic')) setAiProvider('anthropic');
- else if (existing.gatewayBaseUrl.includes('openai.com')) setAiProvider('openai');
- else if (existing.gatewayBaseUrl.includes('openrouter')) setAiProvider('openrouter');
- else setAiProvider('custom');
- }
- }, []);
-
- function handleProviderChange(provider: string) {
- setAiProvider(provider);
- setAiConfigSaved(false);
- if (provider !== 'custom' && PROVIDER_PRESETS[provider]) {
- setAiBaseUrl(PROVIDER_PRESETS[provider].baseUrl);
- setAiModel(PROVIDER_PRESETS[provider].model);
- }
- }
-
- function handleSaveAiConfig() {
- const config: ReviewConfig = {
- gatewayBaseUrl: aiBaseUrl,
- gatewayApiKey: aiApiKey,
- gatewayModel: aiModel,
- reviewTone: defaultTone,
- };
- saveReviewConfig(config);
- setAiConfigSaved(true);
- setTimeout(() => setAiConfigSaved(false), 2000);
- }
-
// Notifications
const [notifyReviewDone, toggleNotifyReviewDone] = useBoolPref('notify_review_done', true);
const [notifyAgentError, toggleNotifyAgentError] = useBoolPref('notify_agent_error', true);
@@ -1500,82 +1452,6 @@ export default function Settings() {
-
-
- AI Provider
-
-
-
-
-
-
-
{
- setAiApiKey(v);
- setAiConfigSaved(false);
- }}
- />
-
- {aiProvider === 'custom' && (
- <>
-
- {
- setAiBaseUrl(v);
- setAiConfigSaved(false);
- }}
- />
- >
- )}
-
-
-
- {
- setAiModel(v);
- setAiConfigSaved(false);
- }}
- />
-
-
-
- {!aiApiKey && (
- API key required to run reviews
- )}
-
-
);
From ca0d963660d36428f3d7fcda4809a722934a376d Mon Sep 17 00:00:00 2001
From: Sarthak Agrawal
Date: Wed, 2 Sep 2026 23:46:18 +0530
Subject: [PATCH 3/4] Revert "fix(ci): keep integration layer type-safe"
This reverts commit 80d4c54545e0f91e7253a6c2f85246b6d4ee6673.
---
.../differential-daemon-contracts.ts | 23 +---
apps/desktop/src/pages/Rubrics.tsx | 4 +
apps/desktop/src/pages/Settings.tsx | 124 ++++++++++++++++++
3 files changed, 129 insertions(+), 22 deletions(-)
diff --git a/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts b/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
index 6a7bc81d..c13159c9 100644
--- a/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
+++ b/apps/desktop/src/lib/warm-verification/differential-daemon-contracts.ts
@@ -7,7 +7,6 @@ import {
VERIFY_CONTRACT_LIMITS,
type ContractIssue,
type ContractValidation,
- type DaemonError,
} from './contracts';
import {
DIFFERENTIAL_CLASSIFICATIONS,
@@ -123,8 +122,7 @@ export type DifferentialDaemonResponse =
| { type: 'differential_prepared'; summary: DifferentialPreparedSummary }
| { type: 'differential_result'; summary: DifferentialRunSummary }
| { type: 'differential_status'; summary: DifferentialStatusSummary }
- | { type: 'differential_cleanup'; summary: DifferentialCleanupSummary }
- | { type: 'error'; error: DaemonError };
+ | { type: 'differential_cleanup'; summary: DifferentialCleanupSummary };
export interface DifferentialDaemonResponseEnvelope {
protocol_version: 1;
request_id: string;
@@ -289,25 +287,6 @@ function validateRequest(value: unknown, issues: ContractIssue[]) {
function validateResponse(value: unknown, issues: ContractIssue[]) {
const response = object(value, '$.response', issues);
if (!response) return;
- if (response.type === 'error') {
- exactKeys(response, '$.response', ['type', 'error'], issues);
- const error = object(response.error, '$.response.error', issues);
- if (!error) return;
- exactKeys(
- error,
- '$.response.error',
- error.remediation === undefined
- ? ['code', 'message', 'retryable']
- : ['code', 'message', 'remediation', 'retryable'],
- issues
- );
- stringField(error, 'code', '$.response.error', issues, { pattern: ID });
- stringField(error, 'message', '$.response.error', issues);
- if (error.remediation !== undefined)
- stringField(error, 'remediation', '$.response.error', issues);
- boolean(error, 'retryable', '$.response.error', issues);
- return;
- }
exactKeys(response, '$.response', ['type', 'summary'], issues);
const rules: Record = {
differential_prepared: prepared,
diff --git a/apps/desktop/src/pages/Rubrics.tsx b/apps/desktop/src/pages/Rubrics.tsx
index 3e2bdc72..41eaada9 100644
--- a/apps/desktop/src/pages/Rubrics.tsx
+++ b/apps/desktop/src/pages/Rubrics.tsx
@@ -27,6 +27,10 @@ import { getStandardsPackUsage, isTauriAvailable } from '@/lib/tauri-ipc';
function fallbackConfig(): ReviewConfig {
return {
+ gatewayBaseUrl: '',
+ gatewayApiKey: '',
+ gatewayModel: 'auto',
+ reviewTone: 'direct',
activeStandardsPack: DEFAULT_STANDARDS_PACKS[0].id,
standardsPacks: [],
};
diff --git a/apps/desktop/src/pages/Settings.tsx b/apps/desktop/src/pages/Settings.tsx
index 8557710d..0d0eca8d 100644
--- a/apps/desktop/src/pages/Settings.tsx
+++ b/apps/desktop/src/pages/Settings.tsx
@@ -6,6 +6,12 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
+import {
+ loadReviewConfig,
+ PROVIDER_PRESETS,
+ type ReviewConfig,
+ saveReviewConfig,
+} from '@/lib/review-service';
import type {
GitHubAuthStatus,
LinearUser,
@@ -584,6 +590,48 @@ export default function Settings() {
const [claudeCodePath, setClaudeCodePath] = usePref('claude_cli_path', '');
const [codexPath, setCodexPath] = usePref('codex_cli_path', '');
+ // AI Provider
+ const [aiProvider, setAiProvider] = useState('anthropic');
+ const [aiBaseUrl, setAiBaseUrl] = useState('');
+ const [aiApiKey, setAiApiKey] = useState('');
+ const [aiModel, setAiModel] = useState('');
+ const [aiConfigSaved, setAiConfigSaved] = useState(false);
+
+ useEffect(() => {
+ const existing = loadReviewConfig();
+ if (existing) {
+ setAiBaseUrl(existing.gatewayBaseUrl);
+ setAiApiKey(existing.gatewayApiKey);
+ setAiModel(existing.gatewayModel);
+ // Detect provider from URL
+ if (existing.gatewayBaseUrl.includes('anthropic')) setAiProvider('anthropic');
+ else if (existing.gatewayBaseUrl.includes('openai.com')) setAiProvider('openai');
+ else if (existing.gatewayBaseUrl.includes('openrouter')) setAiProvider('openrouter');
+ else setAiProvider('custom');
+ }
+ }, []);
+
+ function handleProviderChange(provider: string) {
+ setAiProvider(provider);
+ setAiConfigSaved(false);
+ if (provider !== 'custom' && PROVIDER_PRESETS[provider]) {
+ setAiBaseUrl(PROVIDER_PRESETS[provider].baseUrl);
+ setAiModel(PROVIDER_PRESETS[provider].model);
+ }
+ }
+
+ function handleSaveAiConfig() {
+ const config: ReviewConfig = {
+ gatewayBaseUrl: aiBaseUrl,
+ gatewayApiKey: aiApiKey,
+ gatewayModel: aiModel,
+ reviewTone: defaultTone,
+ };
+ saveReviewConfig(config);
+ setAiConfigSaved(true);
+ setTimeout(() => setAiConfigSaved(false), 2000);
+ }
+
// Notifications
const [notifyReviewDone, toggleNotifyReviewDone] = useBoolPref('notify_review_done', true);
const [notifyAgentError, toggleNotifyAgentError] = useBoolPref('notify_agent_error', true);
@@ -1452,6 +1500,82 @@ export default function Settings() {
+
+
+ AI Provider
+
+
+
+
+
+
+
{
+ setAiApiKey(v);
+ setAiConfigSaved(false);
+ }}
+ />
+
+ {aiProvider === 'custom' && (
+ <>
+
+ {
+ setAiBaseUrl(v);
+ setAiConfigSaved(false);
+ }}
+ />
+ >
+ )}
+
+
+
+ {
+ setAiModel(v);
+ setAiConfigSaved(false);
+ }}
+ />
+
+
+
+ {!aiApiKey && (
+ API key required to run reviews
+ )}
+
+
);
From 49cb40a73a32b0d10c1e11e5dae01b317f3da182 Mon Sep 17 00:00:00 2001
From: Sarthak Agrawal
Date: Wed, 2 Sep 2026 23:47:00 +0530
Subject: [PATCH 4/4] chore: establish repository policy inputs
---
.github/actionlint.yaml | 5 +++++
.gitleaksignore | 10 ++++++++++
apps/desktop/.size-limit.json | 8 ++++++++
biome.json | 3 ++-
4 files changed, 25 insertions(+), 1 deletion(-)
create mode 100644 .github/actionlint.yaml
create mode 100644 .gitleaksignore
create mode 100644 apps/desktop/.size-limit.json
diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml
new file mode 100644
index 00000000..545dde8f
--- /dev/null
+++ b/.github/actionlint.yaml
@@ -0,0 +1,5 @@
+# Actionlint 1.7.12 predates GitHub's official Xcode 27 preview image label.
+# Treat it as declared until the pinned validator includes the hosted label.
+self-hosted-runner:
+ labels:
+ - xcode-27
diff --git a/.gitleaksignore b/.gitleaksignore
new file mode 100644
index 00000000..2bd3815f
--- /dev/null
+++ b/.gitleaksignore
@@ -0,0 +1,10 @@
+# Exact historical false positives retained for auditability. Each entry is
+# scoped to one commit, path, rule, and line; future findings in the same files
+# remain visible.
+# PostHog browser ingestion key, public by design (see tooling docs).
+930c8952a3c80edd61c06c2f49106e616c8dde9a:apps/desktop/src/lib/analytics.ts:generic-api-key:25
+39f414d063cac9ebe9258c69a81c05348f3e4b89:apps/desktop/src/lib/analytics.ts:generic-api-key:25
+# Historical Foundry public project identifier.
+81df29eebf1d724d2939cbfd8123b86e9848a940:foundry.json:generic-api-key:4
+# Historical database migration fixture value.
+ac5d18ee9b33811accec2df5d7bc33b69f9bb6c3:apps/desktop/src-tauri/src/db/schema.rs:generic-api-key:23
diff --git a/apps/desktop/.size-limit.json b/apps/desktop/.size-limit.json
new file mode 100644
index 00000000..6452c5a9
--- /dev/null
+++ b/apps/desktop/.size-limit.json
@@ -0,0 +1,8 @@
+[
+ {
+ "name": "Complete lazy JavaScript distribution (raw)",
+ "path": "out/assets/*.js",
+ "limit": "1800 kB",
+ "brotli": false
+ }
+]
diff --git a/biome.json b/biome.json
index 5fdb8c3a..97562ff0 100644
--- a/biome.json
+++ b/biome.json
@@ -36,7 +36,8 @@
"!**/*.css",
"!**/*.svg",
"!**/*.astro",
- "!benchmark/cases"
+ "!benchmark/cases",
+ "!benchmarks/public-catch-rate/cases"
]
},
"formatter": {