Skip to main content

sparrow/onboarding/
enterprise.rs

1// ─── IDE Integration stubs (WS9) ──────────────────────────────────────────────
2
3/// VS Code extension manifest. The extension is a thin WebSocket client
4/// connecting to Sparrow's runtime API (ws://127.0.0.1:9338).
5/// No business logic in the extension — all intelligence lives in the core.
6pub 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
35/// JetBrains plugin descriptor
36pub 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
47/// Neovim plugin (Lua stub)
48pub 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
66// ─── Teams & Enterprise (WS12) ─────────────────────────────────────────────────
67
68use serde::{Deserialize, Serialize};
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct OrgPolicy {
72    pub max_autonomy: String, // "supervised" | "trusted" | "autonomous"
73    pub allowed_providers: Vec<String>,
74    pub budget_per_seat_daily: f64,
75    pub blocked_paths: Vec<String>, // e.g., [".env", "*.pem", "secrets/"]
76    pub require_approval_for: Vec<String>, // risk levels requiring approval
77    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        // Check autonomy ceiling
105        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        // Check budget
118        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        // Check blocked paths
126        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// ─── Audit log ─────────────────────────────────────────────────────────────────
142
143#[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}