feat(pro): implement all 3 commercial phases

Phase 1 — Cost Intelligence:
- budget.rs: per-project token budgets, soft/hard limits, usage logging
- router.rs: 3 preset profiles (CostSaver/QualityFirst/Balanced)

Phase 2 — Knowledge Base:
- memory.rs: persistent agent memory with FTS5, auto-extraction, TTL
- symbols.rs: regex-based symbol graph (tree-sitter stub)

Phase 3 — Git Integration:
- git_context.rs: branch/commit/modified file context injection
- branch_policy.rs: session-level branch protection

6 modules, 32 cargo tests, 22+ Tauri plugin commands.
This commit is contained in:
Hibryda 2026-03-17 03:27:40 +01:00
parent 3798bedc4d
commit 191b869b43
7 changed files with 1509 additions and 0 deletions

View file

@ -0,0 +1,208 @@
// SPDX-License-Identifier: LicenseRef-Commercial
// Branch Policy Enforcement — block agent sessions on protected branches.
use rusqlite::params;
use serde::Serialize;
use std::process::Command;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BranchPolicy {
pub id: i64,
pub pattern: String,
pub action: String,
pub reason: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyDecision {
pub allowed: bool,
pub branch: String,
pub matched_policy: Option<BranchPolicy>,
pub reason: String,
}
fn ensure_tables(conn: &rusqlite::Connection) -> Result<(), String> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS pro_branch_policies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern TEXT NOT NULL,
action TEXT NOT NULL DEFAULT 'block',
reason TEXT NOT NULL DEFAULT ''
);"
).map_err(|e| format!("Failed to create branch_policies table: {e}"))?;
// Seed default policies if table is empty
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM pro_branch_policies", [], |row| row.get(0)
).unwrap_or(0);
if count == 0 {
conn.execute_batch(
"INSERT INTO pro_branch_policies (pattern, action, reason) VALUES
('main', 'block', 'Protected branch: direct work on main is not allowed'),
('master', 'block', 'Protected branch: direct work on master is not allowed'),
('release/*', 'block', 'Protected branch: release branches require PRs');"
).map_err(|e| format!("Failed to seed default policies: {e}"))?;
}
Ok(())
}
/// Simple glob matching: supports `*` at the end of a pattern (e.g., `release/*`).
fn glob_match(pattern: &str, value: &str) -> bool {
if pattern == value {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
return value.starts_with(prefix);
}
false
}
fn get_current_branch(project_path: &str) -> Result<String, String> {
let output = Command::new("git")
.args(["-C", project_path, "branch", "--show-current"])
.output()
.map_err(|e| format!("Failed to run git: {e}"))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err("Not a git repository or git not available".into())
}
}
#[tauri::command]
pub fn pro_branch_check(project_path: String) -> Result<PolicyDecision, String> {
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
let branch = get_current_branch(&project_path)?;
let mut stmt = conn.prepare(
"SELECT id, pattern, action, reason FROM pro_branch_policies"
).map_err(|e| format!("Query failed: {e}"))?;
let policies: Vec<BranchPolicy> = stmt.query_map([], |row| {
Ok(BranchPolicy {
id: row.get(0)?,
pattern: row.get(1)?,
action: row.get(2)?,
reason: row.get(3)?,
})
}).map_err(|e| format!("Query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row read failed: {e}"))?;
for policy in &policies {
if glob_match(&policy.pattern, &branch) {
let allowed = policy.action != "block";
return Ok(PolicyDecision {
allowed,
branch: branch.clone(),
matched_policy: Some(policy.clone()),
reason: policy.reason.clone(),
});
}
}
Ok(PolicyDecision {
allowed: true,
branch,
matched_policy: None,
reason: "No matching policy".into(),
})
}
#[tauri::command]
pub fn pro_branch_policy_list() -> Result<Vec<BranchPolicy>, String> {
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
let mut stmt = conn.prepare(
"SELECT id, pattern, action, reason FROM pro_branch_policies ORDER BY id"
).map_err(|e| format!("Query failed: {e}"))?;
let rows = stmt.query_map([], |row| {
Ok(BranchPolicy {
id: row.get(0)?,
pattern: row.get(1)?,
action: row.get(2)?,
reason: row.get(3)?,
})
}).map_err(|e| format!("Query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row read failed: {e}"))?;
Ok(rows)
}
#[tauri::command]
pub fn pro_branch_policy_add(pattern: String, action: Option<String>, reason: Option<String>) -> Result<i64, String> {
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
let act = action.unwrap_or_else(|| "block".into());
let rsn = reason.unwrap_or_default();
conn.execute(
"INSERT INTO pro_branch_policies (pattern, action, reason) VALUES (?1, ?2, ?3)",
params![pattern, act, rsn],
).map_err(|e| format!("Failed to add policy: {e}"))?;
Ok(conn.last_insert_rowid())
}
#[tauri::command]
pub fn pro_branch_policy_remove(id: i64) -> Result<(), String> {
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
conn.execute("DELETE FROM pro_branch_policies WHERE id = ?1", params![id])
.map_err(|e| format!("Failed to remove policy: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_decision_serializes_camel_case() {
let d = PolicyDecision {
allowed: false,
branch: "main".into(),
matched_policy: Some(BranchPolicy {
id: 1,
pattern: "main".into(),
action: "block".into(),
reason: "Protected".into(),
}),
reason: "Protected".into(),
};
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("matchedPolicy"));
assert!(json.contains("\"allowed\":false"));
}
#[test]
fn test_branch_policy_serializes_camel_case() {
let p = BranchPolicy {
id: 1,
pattern: "release/*".into(),
action: "block".into(),
reason: "No direct commits".into(),
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("\"pattern\":\"release/*\""));
assert!(json.contains("\"action\":\"block\""));
}
#[test]
fn test_glob_match() {
assert!(glob_match("main", "main"));
assert!(!glob_match("main", "main2"));
assert!(glob_match("release/*", "release/v1.0"));
assert!(glob_match("release/*", "release/hotfix"));
assert!(!glob_match("release/*", "feature/test"));
assert!(!glob_match("master", "main"));
}
}