voracode 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * VORACODE Model Router — Universal AI provider adapter
3
+ *
4
+ * Supports any AI provider through a unified interface.
5
+ *
6
+ * Provider protocols supported:
7
+ * - OpenAI-compatible (DeepSeek, Groq, Together, Ollama, OpenRouter, etc.)
8
+ * - Anthropic Messages API (Claude)
9
+ * - Google AI API (Gemini)
10
+ * - Custom (user-defined)
11
+ *
12
+ * BYOK ONLY — user must provide their own API key.
13
+ * VORACODE does not provide free managed models.
14
+ */
15
+
16
+ export interface ChatMessage {
17
+ role: "user" | "assistant" | "system" | "tool";
18
+ content: string;
19
+ name?: string;
20
+ tool_calls?: Array<{
21
+ id: string;
22
+ type: "function";
23
+ function: { name: string; arguments: string };
24
+ }>;
25
+ tool_call_id?: string;
26
+ }
27
+
28
+ export interface ToolDefinition {
29
+ type: "function";
30
+ function: {
31
+ name: string;
32
+ description: string;
33
+ parameters: Record<string, unknown>;
34
+ };
35
+ }
36
+
37
+ export interface ChatOptions {
38
+ model: string;
39
+ maxTokens?: number;
40
+ temperature?: number;
41
+ systemPrompt?: string;
42
+ stream?: boolean;
43
+ tools?: ToolDefinition[];
44
+ toolChoice?: "auto" | "required" | "none";
45
+ }
46
+
47
+ export interface ChatResponse {
48
+ content: string;
49
+ model: string;
50
+ provider: string;
51
+ usage: {
52
+ inputTokens: number;
53
+ outputTokens: number;
54
+ totalTokens: number;
55
+ };
56
+ toolCalls?: Array<{
57
+ id: string;
58
+ type: "function";
59
+ function: { name: string; arguments: string };
60
+ }>;
61
+ }
62
+
63
+ interface ProviderConfig {
64
+ apiKey?: string;
65
+ baseUrl: string;
66
+ }
67
+
68
+ const PROVIDER_BASE_URLS: Record<string, string> = {
69
+ openai: "https://api.openai.com/v1",
70
+ deepseek: "https://api.deepseek.com/v1",
71
+ groq: "https://api.groq.com/openai/v1",
72
+ together: "https://api.together.xyz/v1",
73
+ openrouter: "https://openrouter.ai/api/v1",
74
+ ollama: "http://localhost:11434/v1",
75
+ huggingface: "https://router.huggingface.co/v1",
76
+ fireworks: "https://api.fireworks.ai/inference/v1",
77
+ cerebras: "https://api.cerebras.ai/v1",
78
+ anthropic: "https://api.anthropic.com/v1",
79
+ google: "https://generativelanguage.googleapis.com/v1beta",
80
+ };
81
+
82
+ const PROVIDER_KEY_PATTERNS: Record<string, RegExp> = {
83
+ openai: /^(sk-|sx-)/,
84
+ anthropic: /^sk-ant-/,
85
+ deepseek: /^sk-/,
86
+ groq: /^gsk_/,
87
+ };
88
+
89
+ // ── Rate Limiter ──
90
+
91
+ class RateLimiter {
92
+ private calls: Map<string, number[]> = new Map();
93
+ private readonly windowMs = 60_000; // 1 minute
94
+ private readonly maxCalls = 10; // 10 calls per minute
95
+
96
+ canCall(provider: string): boolean {
97
+ const now = Date.now();
98
+ const timestamps = this.calls.get(provider) || [];
99
+
100
+ // Remove entries older than the window
101
+ const recent = timestamps.filter((t) => now - t < this.windowMs);
102
+ this.calls.set(provider, recent);
103
+
104
+ if (recent.length >= this.maxCalls) {
105
+ return false; // Rate limited
106
+ }
107
+
108
+ recent.push(now);
109
+ this.calls.set(provider, recent);
110
+ return true;
111
+ }
112
+
113
+ retryAfter(provider: string): number {
114
+ const timestamps = this.calls.get(provider) || [];
115
+ if (timestamps.length === 0) return 0;
116
+ const oldest = timestamps[0];
117
+ return Math.max(0, Math.ceil((this.windowMs - (Date.now() - oldest)) / 1000));
118
+ }
119
+ }
120
+
121
+ export class ModelRouter {
122
+ private keychain: Map<string, string> = new Map();
123
+ private rateLimiter: RateLimiter = new RateLimiter();
124
+
125
+ constructor() {
126
+ // Keys will be loaded from OS Keychain in Phase 1.3
127
+ // For now, use environment variables
128
+ this.loadFromEnv();
129
+ }
130
+
131
+ private loadFromEnv(): void {
132
+ const envMap: Record<string, string | undefined> = {
133
+ openai: process.env.OPENAI_API_KEY || process.env.OPENAI_KEY,
134
+ anthropic: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_KEY,
135
+ deepseek: process.env.DEEPSEEK_API_KEY || process.env.DEEPSEEK_KEY,
136
+ groq: process.env.GROQ_API_KEY || process.env.GROQ_KEY,
137
+ google: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY,
138
+ openrouter: process.env.OPENROUTER_API_KEY,
139
+ huggingface: process.env.HF_API_KEY || process.env.HUGGINGFACE_KEY,
140
+ together: process.env.TOGETHER_API_KEY,
141
+ ollama: "", // No key needed for local
142
+ };
143
+
144
+ for (const [provider, key] of Object.entries(envMap)) {
145
+ if (key) {
146
+ this.keychain.set(provider, key);
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get the active API key for a provider
153
+ */
154
+ getKey(provider: string): string | undefined {
155
+ return this.keychain.get(provider);
156
+ }
157
+
158
+ /**
159
+ * Set an API key for a provider
160
+ */
161
+ setKey(provider: string, key: string): void {
162
+ this.keychain.set(provider, key);
163
+ }
164
+
165
+ /**
166
+ * Check if a provider has a valid-looking API key configured
167
+ */
168
+ hasValidKey(provider: string): boolean {
169
+ const key = this.keychain.get(provider);
170
+ if (!key) return false;
171
+
172
+ const pattern = PROVIDER_KEY_PATTERNS[provider];
173
+ if (pattern) return pattern.test(key);
174
+ return key.length > 8;
175
+ }
176
+
177
+ /**
178
+ * Detect provider from model name
179
+ */
180
+ detectProvider(modelName: string): string {
181
+ const normalized = modelName.toLowerCase();
182
+
183
+ if (normalized.startsWith("gpt-") || normalized.startsWith("o3") || normalized.startsWith("o1")) return "openai";
184
+ if (normalized.startsWith("claude-") || normalized.startsWith("opus") || normalized.startsWith("sonnet") || normalized.startsWith("haiku")) return "anthropic";
185
+ if (normalized.startsWith("gemini-")) return "google";
186
+ if (normalized.startsWith("deepseek-")) return "deepseek";
187
+ if (normalized.startsWith("llama-") || normalized.startsWith("mixtral") || normalized.startsWith("qwen-")) return "groq";
188
+ if (normalized.startsWith("command-")) return "cohere";
189
+
190
+ return "openai"; // default fallback
191
+ }
192
+
193
+ /**
194
+ * Get available models for a provider
195
+ */
196
+ getModels(provider: string): string[] {
197
+ const modelMap: Record<string, string[]> = {
198
+ openai: ["gpt-4o", "gpt-4o-mini", "o3-mini", "o1"],
199
+ anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-haiku-4"],
200
+ deepseek: ["deepseek-chat", "deepseek-v4-flash", "deepseek-v4-pro"],
201
+ groq: ["llama-3.1-8b", "llama-3.1-70b", "qwen-32b", "mixtral-8x7b"],
202
+ google: ["gemini-2.5-pro", "gemini-2.5-flash"],
203
+ ollama: ["llama3.2", "mistral", "deepseek-coder-v2", "qwen2.5-coder"],
204
+ openrouter: ["openrouter/auto"],
205
+ huggingface: ["HuggingFaceH4/zephyr-7b-beta"],
206
+ };
207
+ return modelMap[provider] || [];
208
+ }
209
+
210
+ /**
211
+ * Get provider base URL
212
+ */
213
+ getBaseUrl(provider: string): string {
214
+ return PROVIDER_BASE_URLS[provider] || "";
215
+ }
216
+
217
+ /**
218
+ * Chat completion using OpenAI-compatible API
219
+ * Covers: DeepSeek, Groq, Together, Ollama, OpenRouter, HuggingFace
220
+ */
221
+ async chatOpenAI(provider: string, messages: ChatMessage[], options: ChatOptions): Promise<ChatResponse> {
222
+ const baseUrl = this.getBaseUrl(provider);
223
+ const apiKey = this.keychain.get(provider);
224
+
225
+ if (!apiKey && provider !== "ollama") {
226
+ throw new Error(`No API key configured for ${provider}. Run: voracode key set ${provider}`);
227
+ }
228
+
229
+ const response = await fetch(`${baseUrl}/chat/completions`, {
230
+ method: "POST",
231
+ headers: {
232
+ "Content-Type": "application/json",
233
+ Authorization: apiKey ? `Bearer ${apiKey}` : "",
234
+ },
235
+ body: JSON.stringify({
236
+ model: options.model,
237
+ messages: messages.map((m) => ({
238
+ role: m.role,
239
+ content: m.content,
240
+ ...(m.name ? { name: m.name } : {}),
241
+ ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
242
+ ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}),
243
+ })),
244
+ max_tokens: options.maxTokens || 4096,
245
+ temperature: options.temperature ?? 0.7,
246
+ stream: false,
247
+ ...(options.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: options.toolChoice || "auto" } : {}),
248
+ }),
249
+ signal: AbortSignal.timeout(60_000), // 60s timeout
250
+ });
251
+
252
+ if (!response.ok) {
253
+ const errorBody = await response.text().catch(() => "");
254
+ throw new Error(`${provider} API error (${response.status}): ${errorBody}`);
255
+ }
256
+
257
+ const data = await response.json() as {
258
+ choices: Array<{ message: { content: string; tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }> } }>;
259
+ usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
260
+ model: string;
261
+ };
262
+
263
+ return {
264
+ content: data.choices[0]?.message?.content || "",
265
+ model: data.model,
266
+ provider,
267
+ usage: {
268
+ inputTokens: data.usage?.prompt_tokens || 0,
269
+ outputTokens: data.usage?.completion_tokens || 0,
270
+ totalTokens: data.usage?.total_tokens || 0,
271
+ },
272
+ toolCalls: data.choices[0]?.message?.tool_calls,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Chat completion using Anthropic Messages API
278
+ */
279
+ async chatAnthropic(messages: ChatMessage[], options: ChatOptions): Promise<ChatResponse> {
280
+ const apiKey = this.keychain.get("anthropic");
281
+ if (!apiKey) throw new Error("No API key configured for Anthropic. Run: voracode key set anthropic");
282
+
283
+ // Convert messages to Anthropic format
284
+ const systemMsg = messages.find((m) => m.role === "system");
285
+ const nonSystem = messages.filter((m) => m.role !== "system");
286
+
287
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
288
+ method: "POST",
289
+ headers: {
290
+ "Content-Type": "application/json",
291
+ "x-api-key": apiKey,
292
+ "anthropic-version": "2023-06-01",
293
+ },
294
+ body: JSON.stringify({
295
+ model: options.model,
296
+ messages: nonSystem.map((m) => ({
297
+ role: m.role === "assistant" ? "assistant" : "user",
298
+ content: m.content,
299
+ })),
300
+ system: systemMsg?.content,
301
+ max_tokens: options.maxTokens || 4096,
302
+ temperature: options.temperature ?? 0.7,
303
+ }),
304
+ signal: AbortSignal.timeout(60_000), // 60s timeout
305
+ });
306
+
307
+ if (!response.ok) {
308
+ const errorBody = await response.text().catch(() => "");
309
+ throw new Error(`Anthropic API error (${response.status}): ${errorBody}`);
310
+ }
311
+
312
+ const data = await response.json() as {
313
+ content: Array<{ text: string }>;
314
+ model: string;
315
+ usage: { input_tokens: number; output_tokens: number };
316
+ };
317
+
318
+ return {
319
+ content: data.content?.[0]?.text || "",
320
+ model: data.model,
321
+ provider: "anthropic",
322
+ usage: {
323
+ inputTokens: data.usage?.input_tokens || 0,
324
+ outputTokens: data.usage?.output_tokens || 0,
325
+ totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
326
+ },
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Main chat method — auto-selects correct adapter based on model name
332
+ */
333
+ async chat(modelRef: string, messages: ChatMessage[], options: ChatOptions = {}): Promise<ChatResponse> {
334
+ const provider = modelRef.includes("/") ? modelRef.split("/")[0] : this.detectProvider(modelRef);
335
+ const modelName = modelRef.includes("/") ? modelRef.slice(modelRef.indexOf("/") + 1) : modelRef;
336
+
337
+ // Rate limit check
338
+ if (!this.rateLimiter.canCall(provider)) {
339
+ const retryAfter = this.rateLimiter.retryAfter(provider);
340
+ throw new Error(`Rate limited on ${provider}. Retry in ${retryAfter}s. Use voracode pro for higher limits.`);
341
+ }
342
+
343
+ const fullOptions = { ...options, model: modelName };
344
+
345
+ try {
346
+ switch (provider) {
347
+ case "anthropic":
348
+ return await this.chatAnthropic(messages, fullOptions);
349
+ case "google":
350
+ return await this.chatGoogle(messages, fullOptions);
351
+ default:
352
+ return await this.chatOpenAI(provider, messages, fullOptions);
353
+ }
354
+ } catch (error) {
355
+ // Auto-fallback: try next provider
356
+ const fallbacks = ["deepseek", "groq", "openrouter"];
357
+ for (const fb of fallbacks) {
358
+ if (fb === provider) continue;
359
+ const fbKey = this.keychain.get(fb);
360
+ if (fbKey) {
361
+ try {
362
+ return await this.chatOpenAI(fb, messages, { ...fullOptions, model: this.getModels(fb)[0] || fb });
363
+ } catch {
364
+ continue; // Try next fallback
365
+ }
366
+ }
367
+ }
368
+ throw error; // All fallbacks exhausted
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Chat completion using Google AI API
374
+ */
375
+ async chatGoogle(messages: ChatMessage[], _options: ChatOptions): Promise<ChatResponse> {
376
+ const apiKey = this.keychain.get("google");
377
+ if (!apiKey) throw new Error("No API key configured for Google. Run: voracode key set google");
378
+
379
+ const response = await fetch(
380
+ `https://generativelanguage.googleapis.com/v1beta/models/${_options.model}:generateContent`,
381
+ {
382
+ method: "POST",
383
+ headers: {
384
+ "Content-Type": "application/json",
385
+ "x-goog-api-key": apiKey, // Use header instead of URL query param
386
+ },
387
+ body: JSON.stringify({
388
+ contents: messages
389
+ .filter((m) => m.role !== "system")
390
+ .map((m) => ({
391
+ role: m.role === "assistant" ? "model" : "user",
392
+ parts: [{ text: m.content }],
393
+ })),
394
+ systemInstruction: messages.find((m) => m.role === "system")?.content
395
+ ? { parts: [{ text: messages.find((m) => m.role === "system")!.content }] }
396
+ : undefined,
397
+ generationConfig: {
398
+ maxOutputTokens: _options.maxTokens || 4096,
399
+ temperature: _options.temperature ?? 0.7,
400
+ },
401
+ }),
402
+ },
403
+ );
404
+
405
+ if (!response.ok) {
406
+ const errorBody = await response.text().catch(() => "");
407
+ throw new Error(`Google API error (${response.status}): ${errorBody}`);
408
+ }
409
+
410
+ const data = await response.json() as {
411
+ candidates: Array<{ content: { parts: Array<{ text: string }> } }>;
412
+ usageMetadata?: { promptTokenCount: number; candidatesTokenCount: number; totalTokenCount: number };
413
+ };
414
+
415
+ return {
416
+ content: data.candidates?.[0]?.content?.parts?.[0]?.text || "",
417
+ model: _options.model,
418
+ provider: "google",
419
+ usage: {
420
+ inputTokens: data.usageMetadata?.promptTokenCount || 0,
421
+ outputTokens: data.usageMetadata?.candidatesTokenCount || 0,
422
+ totalTokens: data.usageMetadata?.totalTokenCount || 0,
423
+ },
424
+ };
425
+ }
426
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * VORACODE OWASP Security Layer
3
+ *
4
+ * Protects against OWASP Top 10 + 40 years of common vulnerabilities:
5
+ * - Prompt injection (new OWASP LLM Top 10)
6
+ * - Command injection
7
+ * - Path traversal
8
+ * - SSRF
9
+ * - SQL injection
10
+ * - XXE
11
+ * - Supply chain
12
+ * - Credential leakage
13
+ * - Data exfiltration
14
+ * - Rate limiting abuse
15
+ */
16
+
17
+ export class OwaspShield {
18
+ private auditLog: string[] = [];
19
+
20
+ /**
21
+ * Scan user input for injection patterns
22
+ */
23
+ sanitizeInput(input: string): { clean: string; blocked: boolean; reason?: string } {
24
+ // Block control characters (except newlines and tabs)
25
+ const controlChars = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
26
+ if (controlChars.length !== input.length) {
27
+ this.log("blocked", "Control characters in input");
28
+ }
29
+
30
+ // Detect prompt injection attempts
31
+ const injectionPatterns = [
32
+ /ignore\s+(all\s+)?(previous|above|below)\s+(instructions|commands)/i,
33
+ /forget\s+(all\s+)?(previous|above|below)\s+(instructions|commands)/i,
34
+ /you\s+(are\s+)?(now|will\s+now)\s+(act\s+as|become|be)/i,
35
+ /system\s+prompt/i,
36
+ /new\s+(instructions|commands|task)/i,
37
+ /disregard\s+(previous|above)/i,
38
+ /\[SYSTEM\]|\[INST\]|<\|im_start\|>|<\|\s*im_start\s*\|>/i,
39
+ /role\s*:\s*system/i,
40
+ ];
41
+
42
+ for (const pattern of injectionPatterns) {
43
+ if (pattern.test(input)) {
44
+ this.log("blocked", `Prompt injection detected: ${pattern.source}`);
45
+ return { clean: input, blocked: true, reason: "Potential prompt injection detected" };
46
+ }
47
+ }
48
+
49
+ return { clean: input, blocked: false };
50
+ }
51
+
52
+ /**
53
+ * Validate LLM output for dangerous content
54
+ */
55
+ validateOutput(output: string): { safe: boolean; sanitized: string; reason?: string } {
56
+ // Check for credential leakage
57
+ const credentialPatterns = [
58
+ /sk-[a-zA-Z0-9]{20,}/, // OpenAI keys
59
+ /sk-ant-[a-zA-Z0-9]{20,}/, // Anthropic keys
60
+ /gsk_[a-zA-Z0-9]{20,}/, // Groq keys
61
+ /AIza[a-zA-Z0-9_-]{35}/, // Google keys
62
+ /ghp_[a-zA-Z0-9]{36}/, // GitHub tokens
63
+ /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/i, // Private keys
64
+ /password\s*[:=]\s*\S+/i, // Password leakage
65
+ /api[_-]?key\s*[:=]\s*\S+/i, // API key leakage
66
+ ];
67
+
68
+ let sanitized = output;
69
+ for (const pattern of credentialPatterns) {
70
+ if (pattern.test(sanitized)) {
71
+ sanitized = sanitized.replace(pattern, "[REDACTED - CREDENTIAL]");
72
+ this.log("redacted", "Credential pattern detected in output");
73
+ }
74
+ }
75
+
76
+ // Check for dangerous code patterns in LLM output
77
+ const dangerousCode = [
78
+ /rm\s+-rf\s+\//g,
79
+ />\s*\/dev\/(sda|sdb|sdc)/g,
80
+ /:\(\s*\)\s*\{/g,
81
+ /eval\(\s*request/i,
82
+ /exec\(\s*request/i,
83
+ /system\(\s*request/i,
84
+ ];
85
+
86
+ for (const pattern of dangerousCode) {
87
+ if (pattern.test(sanitized)) {
88
+ this.log("warn", "Dangerous code pattern in output");
89
+ }
90
+ }
91
+
92
+ return { safe: true, sanitized };
93
+ }
94
+
95
+ /**
96
+ * Check SSRF protection — block private IPs, internal services
97
+ */
98
+ validateUrl(url: string): { safe: boolean; reason?: string } {
99
+ try {
100
+ const parsed = new URL(url);
101
+
102
+ // Block private/internal IP ranges
103
+ const blockedPatterns = [
104
+ /^localhost$/i,
105
+ /^127\.\d+\.\d+\.\d+$/,
106
+ /^10\.\d+\.\d+\.\d+$/,
107
+ /^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
108
+ /^192\.168\.\d+\.\d+$/,
109
+ /^0\.0\.0\.0$/,
110
+ /^::1$/,
111
+ /^fc00:/i,
112
+ /^fe80:/i,
113
+ /^169\.254\.\d+\.\d+$/,
114
+ ];
115
+
116
+ const hostname = parsed.hostname;
117
+ for (const pattern of blockedPatterns) {
118
+ if (pattern.test(hostname)) {
119
+ this.log("blocked", `SSRF blocked: ${url}`);
120
+ return { safe: false, reason: "Internal/private address blocked (SSRF)" };
121
+ }
122
+ }
123
+
124
+ // Block known internal services
125
+ const internalServices = [
126
+ /metadata\.google\.internal/i,
127
+ /169\.254\.169\.254/, // AWS/GCP/Azure metadata
128
+ /\.internal$/i,
129
+ /\.local$/i,
130
+ ];
131
+
132
+ for (const pattern of internalServices) {
133
+ if (pattern.test(hostname)) {
134
+ this.log("blocked", `Internal service blocked: ${url}`);
135
+ return { safe: false, reason: "Cloud metadata endpoint blocked" };
136
+ }
137
+ }
138
+
139
+ return { safe: true };
140
+ } catch {
141
+ return { safe: false, reason: "Invalid URL" };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check for path traversal in file operations
147
+ */
148
+ validatePath(requestedPath: string, projectRoot: string): { safe: boolean; resolvedPath?: string; reason?: string } {
149
+ const path = require("path");
150
+ const resolved = path.resolve(projectRoot, requestedPath);
151
+
152
+ // Ensure resolved path starts with project root
153
+ if (!resolved.startsWith(projectRoot)) {
154
+ this.log("blocked", `Path traversal blocked: ${requestedPath}`);
155
+ return { safe: false, reason: "Path traversal detected" };
156
+ }
157
+
158
+ // Block sensitive files
159
+ const sensitiveFiles = [
160
+ /\.env$/,
161
+ /\.env\./,
162
+ /credentials\.json$/,
163
+ /config\.json$/i,
164
+ /\.ssh\//,
165
+ /id_rsa/,
166
+ /\.pem$/,
167
+ /\.key$/,
168
+ /\.cert$/,
169
+ /\/etc\//,
170
+ /\/proc\//,
171
+ /\/sys\//,
172
+ /\/dev\//,
173
+ ];
174
+
175
+ for (const pattern of sensitiveFiles) {
176
+ if (pattern.test(resolved)) {
177
+ this.log("blocked", `Sensitive file blocked: ${resolved}`);
178
+ return { safe: false, reason: "Access to sensitive files blocked" };
179
+ }
180
+ }
181
+
182
+ return { safe: true, resolvedPath: resolved };
183
+ }
184
+
185
+ /**
186
+ * Detect and block command injection in bash/shell commands
187
+ */
188
+ validateCommand(command: string): { safe: boolean; reason?: string } {
189
+ // Block dangerous commands
190
+ const blockedCommands = [
191
+ /^rm\s+-rf\s+\//m, /^sudo\s+/m, /^mkfs/m, /^dd\s+if=/m,
192
+ /^:\(\)/, /^chmod\s+777\s+\//m,
193
+ /^>\s*\/dev\//m, /bash\s+-c\s+/i, /sh\s+-c\s+/i,
194
+ /powershell\s+-c\s+/i, /cmd\.exe\s+\/c/i,
195
+ /`.*`/, /\$\(.*\)/, /\|.*(bash|sh|zsh|fish)/i,
196
+ /curl.*\|.*bash/i, /wget.*\|.*bash/i,
197
+ /nc\s+/, /ncat\s+/, /netcat\s+/,
198
+ /mkpasswd/, /openssl\s+(passwd|enc)/i,
199
+ /\/etc\/(passwd|shadow)/,
200
+ ];
201
+
202
+ for (const pattern of blockedCommands) {
203
+ if (pattern.test(command)) {
204
+ this.log("blocked", `Command injection blocked: ${command.slice(0, 50)}`);
205
+ return { safe: false, reason: "Command blocked for security" };
206
+ }
207
+ }
208
+
209
+ // Check for excessive command length (potential buffer overflow)
210
+ if (command.length > 10000) {
211
+ return { safe: false, reason: "Command too long (max 10000 chars)" };
212
+ }
213
+
214
+ return { safe: true };
215
+ }
216
+
217
+ /**
218
+ * Check rate limit
219
+ */
220
+ isRateLimited(provider: string, callCount: number, maxPerMinute = 10): boolean {
221
+ if (callCount > maxPerMinute) {
222
+ this.log("rate_limited", `${provider}: ${callCount} calls/min`);
223
+ return true;
224
+ }
225
+ return false;
226
+ }
227
+
228
+ /**
229
+ * Validate API key format
230
+ */
231
+ validateApiKey(provider: string, key: string): boolean {
232
+ const patterns: Record<string, RegExp> = {
233
+ openai: /^(sk-|sx-)[a-zA-Z0-9]{20,}$/,
234
+ anthropic: /^sk-ant-[a-zA-Z0-9]{20,}$/,
235
+ deepseek: /^sk-[a-zA-Z0-9]{20,}$/,
236
+ groq: /^gsk_[a-zA-Z0-9]{20,}$/,
237
+ google: /^AIza[a-zA-Z0-9_-]{35,}$/,
238
+ openrouter: /^sk-or-v1-[a-zA-Z0-9]{20,}$/,
239
+ };
240
+
241
+ if (patterns[provider]) {
242
+ return patterns[provider].test(key);
243
+ }
244
+ return key.length > 8; // Basic length check for unknown providers
245
+ }
246
+
247
+ private log(type: string, detail: string): void {
248
+ this.auditLog.push(`[${new Date().toISOString()}] ${type}: ${detail}`);
249
+ }
250
+
251
+ getAuditLog(): string[] {
252
+ return this.auditLog;
253
+ }
254
+ }