1pub const VSCODE_EXTENSION_MANIFEST: &str = r#"{
7 "name": "sparrow",
8 "displayName": "Sparrow",
9 "description": "The only CLI you install — now in your editor",
10 "version": "0.1.0",
11 "publisher": "sparrow-dev",
12 "engines": { "vscode": "^1.90.0" },
13 "activationEvents": ["onStartupFinished"],
14 "main": "./dist/extension.js",
15 "contributes": {
16 "commands": [
17 { "command": "sparrow.chat", "title": "Sparrow: Open Chat" },
18 { "command": "sparrow.run", "title": "Sparrow: Run Task" },
19 { "command": "sparrow.approve", "title": "Sparrow: Approve" },
20 { "command": "sparrow.deny", "title": "Sparrow: Deny" },
21 { "command": "sparrow.rewind", "title": "Sparrow: Rewind" }
22 ],
23 "configuration": {
24 "title": "Sparrow",
25 "properties": {
26 "sparrow.apiUrl": {
27 "type": "string", "default": "ws://127.0.0.1:9338/ws",
28 "description": "Sparrow API WebSocket URL"
29 }
30 }
31 }
32 }
33}"#;
34
35pub const JETBRAINS_PLUGIN_XML: &str = r#"<idea-plugin>
37 <id>dev.sparrow</id>
38 <name>Sparrow</name>
39 <vendor>Sparrow</vendor>
40 <description>The only CLI you install — now in your IDE</description>
41 <depends>com.intellij.modules.platform</depends>
42 <extensions defaultExtensionNs="com.intellij">
43 <toolWindow id="Sparrow" anchor="right" factoryClass="dev.sparrow.SparrowToolWindowFactory"/>
44 </extensions>
45</idea-plugin>"#;
46
47pub const NEOVIM_PLUGIN_LUA: &str = r#"-- Sparrow Neovim plugin
49-- Connects to Sparrow runtime via WebSocket
50-- Usage: require('sparrow').setup({ api_url = 'ws://127.0.0.1:9338/ws' })
51
52local M = {}
53
54function M.setup(opts)
55 opts = opts or {}
56 local api_url = opts.api_url or 'ws://127.0.0.1:9338/ws'
57 -- Thin renderer: connects to runtime, renders events
58 vim.api.nvim_create_user_command('SparrowRun', function(cmd)
59 vim.fn.jobstart({'sparrow', 'run', cmd.args}, {})
60 end, { nargs = 1 })
61end
62
63return M
64"#;
65
66use serde::{Deserialize, Serialize};
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct OrgPolicy {
72 pub max_autonomy: String, pub allowed_providers: Vec<String>,
74 pub budget_per_seat_daily: f64,
75 pub blocked_paths: Vec<String>, pub require_approval_for: Vec<String>, pub audit_enabled: bool,
78 pub sso_provider: Option<String>,
79 pub air_gapped: bool,
80}
81
82impl Default for OrgPolicy {
83 fn default() -> Self {
84 Self {
85 max_autonomy: "trusted".into(),
86 allowed_providers: vec![],
87 budget_per_seat_daily: 10.0,
88 blocked_paths: vec![".env".into(), "*.pem".into(), "secrets/".into()],
89 require_approval_for: vec!["destructive".into()],
90 audit_enabled: true,
91 sso_provider: None,
92 air_gapped: false,
93 }
94 }
95}
96
97impl OrgPolicy {
98 pub fn enforce(
99 &self,
100 autonomy: &crate::event::AutonomyLevel,
101 cost: f64,
102 path: &str,
103 ) -> Result<(), String> {
104 let max = match self.max_autonomy.as_str() {
106 "supervised" => crate::event::AutonomyLevel::Supervised,
107 "trusted" => crate::event::AutonomyLevel::Trusted,
108 _ => crate::event::AutonomyLevel::Autonomous,
109 };
110 if autonomy.as_float() > max.as_float() {
111 return Err(format!(
112 "Org policy limits autonomy to {}",
113 self.max_autonomy
114 ));
115 }
116
117 if cost > self.budget_per_seat_daily {
119 return Err(format!(
120 "Budget exceeded: ${:.2} > ${:.2}/day",
121 cost, self.budget_per_seat_daily
122 ));
123 }
124
125 for blocked in &self.blocked_paths {
127 if blocked.ends_with('/') && path.starts_with(blocked) {
128 return Err(format!("Path '{}' is blocked by org policy", path));
129 }
130 if path == *blocked
131 || (blocked.contains('*') && path.contains(&blocked.replace('*', "")))
132 {
133 return Err(format!("File '{}' is protected by org policy", path));
134 }
135 }
136
137 Ok(())
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AuditEntry {
145 pub timestamp: String,
146 pub user: String,
147 pub action: String,
148 pub run_id: String,
149 pub cost_usd: f64,
150 pub tokens: u64,
151 pub autonomy: String,
152 pub status: String,
153}
154
155pub fn export_audit_log(entries: &[AuditEntry], format: &str) -> String {
156 match format {
157 "json" => serde_json::to_string_pretty(entries).unwrap_or_default(),
158 "csv" => {
159 let mut csv =
160 String::from("timestamp,user,action,run_id,cost,tokens,autonomy,status\n");
161 for e in entries {
162 csv.push_str(&format!(
163 "{},{},{},{},{:.4},{},{},{}\n",
164 e.timestamp,
165 e.user,
166 e.action,
167 e.run_id,
168 e.cost_usd,
169 e.tokens,
170 e.autonomy,
171 e.status
172 ));
173 }
174 csv
175 }
176 _ => format!("{} entries", entries.len()),
177 }
178}