@xagent-ai/cli 1.2.0 → 1.2.2

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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/README_CN.md +1 -1
  3. package/dist/agents.js +164 -164
  4. package/dist/agents.js.map +1 -1
  5. package/dist/ai-client.d.ts +4 -6
  6. package/dist/ai-client.d.ts.map +1 -1
  7. package/dist/ai-client.js +137 -115
  8. package/dist/ai-client.js.map +1 -1
  9. package/dist/auth.js +4 -4
  10. package/dist/auth.js.map +1 -1
  11. package/dist/cli.js +184 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.js +3 -3
  14. package/dist/config.js.map +1 -1
  15. package/dist/context-compressor.d.ts.map +1 -1
  16. package/dist/context-compressor.js +65 -81
  17. package/dist/context-compressor.js.map +1 -1
  18. package/dist/conversation.d.ts +1 -1
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +5 -31
  21. package/dist/conversation.js.map +1 -1
  22. package/dist/memory.d.ts +5 -1
  23. package/dist/memory.d.ts.map +1 -1
  24. package/dist/memory.js +77 -37
  25. package/dist/memory.js.map +1 -1
  26. package/dist/remote-ai-client.d.ts +1 -8
  27. package/dist/remote-ai-client.d.ts.map +1 -1
  28. package/dist/remote-ai-client.js +55 -65
  29. package/dist/remote-ai-client.js.map +1 -1
  30. package/dist/retry.d.ts +35 -0
  31. package/dist/retry.d.ts.map +1 -0
  32. package/dist/retry.js +166 -0
  33. package/dist/retry.js.map +1 -0
  34. package/dist/session.d.ts +0 -5
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +243 -312
  37. package/dist/session.js.map +1 -1
  38. package/dist/slash-commands.d.ts +1 -0
  39. package/dist/slash-commands.d.ts.map +1 -1
  40. package/dist/slash-commands.js +91 -9
  41. package/dist/slash-commands.js.map +1 -1
  42. package/dist/smart-approval.d.ts.map +1 -1
  43. package/dist/smart-approval.js +18 -17
  44. package/dist/smart-approval.js.map +1 -1
  45. package/dist/system-prompt-generator.d.ts.map +1 -1
  46. package/dist/system-prompt-generator.js +149 -139
  47. package/dist/system-prompt-generator.js.map +1 -1
  48. package/dist/theme.d.ts +48 -0
  49. package/dist/theme.d.ts.map +1 -1
  50. package/dist/theme.js +254 -0
  51. package/dist/theme.js.map +1 -1
  52. package/dist/tools/edit-diff.d.ts +32 -0
  53. package/dist/tools/edit-diff.d.ts.map +1 -0
  54. package/dist/tools/edit-diff.js +185 -0
  55. package/dist/tools/edit-diff.js.map +1 -0
  56. package/dist/tools/edit.d.ts +11 -0
  57. package/dist/tools/edit.d.ts.map +1 -0
  58. package/dist/tools/edit.js +129 -0
  59. package/dist/tools/edit.js.map +1 -0
  60. package/dist/tools.d.ts +19 -5
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +979 -631
  63. package/dist/tools.js.map +1 -1
  64. package/dist/types.d.ts +6 -31
  65. package/dist/types.d.ts.map +1 -1
  66. package/package.json +3 -2
  67. package/src/agents.ts +504 -504
  68. package/src/ai-client.ts +1559 -1458
  69. package/src/auth.ts +4 -4
  70. package/src/cli.ts +195 -1
  71. package/src/config.ts +3 -3
  72. package/src/memory.ts +55 -14
  73. package/src/remote-ai-client.ts +663 -683
  74. package/src/retry.ts +217 -0
  75. package/src/session.ts +1736 -1840
  76. package/src/slash-commands.ts +98 -9
  77. package/src/smart-approval.ts +626 -625
  78. package/src/system-prompt-generator.ts +853 -843
  79. package/src/theme.ts +284 -0
  80. package/src/tools.ts +390 -70
@@ -1,626 +1,627 @@
1
- import inquirer from 'inquirer';
2
- import { AIClient, Message } from './ai-client.js';
3
- import { getConfigManager } from './config.js';
4
- import { AuthType } from './types.js';
5
- import { getLogger } from './logger.js';
6
- import { colors, icons } from './theme.js';
7
-
8
- const logger = getLogger();
9
-
10
- /**
11
- * Approval result type
12
- */
13
- export enum ApprovalDecision {
14
- APPROVED = 'approved',
15
- REJECTED = 'rejected',
16
- REQUIRES_CONFIRMATION = 'requires_confirmation',
17
- AI_REVIEW = 'ai_review'
18
- }
19
-
20
- /**
21
- * Risk level
22
- */
23
- export enum RiskLevel {
24
- LOW = 'LOW',
25
- MEDIUM = 'MEDIUM',
26
- HIGH = 'HIGH',
27
- CRITICAL = 'CRITICAL'
28
- }
29
-
30
- /**
31
- * Approval result
32
- */
33
- export interface ApprovalResult {
34
- decision: ApprovalDecision;
35
- riskLevel: RiskLevel;
36
- detectionMethod: 'whitelist' | 'blacklist' | 'ai_review' | 'manual';
37
- description: string;
38
- latency: number;
39
- aiAnalysis?: string;
40
- }
41
-
42
- /**
43
- * Tool call context
44
- */
45
- export interface ToolCallContext {
46
- toolName: string;
47
- params: any;
48
- timestamp: number;
49
- }
50
-
51
- /**
52
- * Whitelist checker
53
- */
54
- export class WhitelistChecker {
55
- private static readonly WHITELISTED_TOOLS: Set<string> = new Set([
56
- // Information reading tools
57
- 'Read',
58
- 'ListDirectory',
59
- 'SearchCodebase',
60
- 'Grep',
61
- 'image_read',
62
-
63
- // Task management tools
64
- 'todo_write',
65
- 'todo_read',
66
- 'task',
67
- 'exit_plan_mode',
68
- 'web_search',
69
-
70
- // File editing tools
71
- 'replace',
72
- 'Write',
73
-
74
- // Other safe tools
75
- 'web_fetch',
76
- 'ask_user_question',
77
- 'save_memory',
78
- 'xml_escape',
79
- 'Skill'
80
- ]);
81
-
82
- /**
83
- * Check if tool is in whitelist
84
- */
85
- check(toolName: string): boolean {
86
- return WhitelistChecker.WHITELISTED_TOOLS.has(toolName);
87
- }
88
-
89
- /**
90
- * Get list of whitelisted tools
91
- */
92
- getWhitelistedTools(): string[] {
93
- return Array.from(WhitelistChecker.WHITELISTED_TOOLS);
94
- }
95
- }
96
-
97
- /**
98
- * Blacklist rules
99
- */
100
- interface BlacklistRule {
101
- pattern: RegExp;
102
- category: string;
103
- riskLevel: RiskLevel;
104
- description: string;
105
- }
106
-
107
- /**
108
- * Blacklist checker
109
- */
110
- export class BlacklistChecker {
111
- private static readonly RULES: BlacklistRule[] = [
112
- // System destruction
113
- {
114
- pattern: /rm\s+-rf\s+\/$/,
115
- category: 'System destruction',
116
- riskLevel: RiskLevel.CRITICAL,
117
- description: 'Delete root directory'
118
- },
119
- {
120
- pattern: /rm\s+-rf\s+(\/etc|\/usr|\/bin|\/sbin|\/lib|\/lib64)/,
121
- category: 'System destruction',
122
- riskLevel: RiskLevel.CRITICAL,
123
- description: 'Delete system directories'
124
- },
125
- {
126
- pattern: /rm\s+-rf\s+.*\*/,
127
- category: 'System destruction',
128
- riskLevel: RiskLevel.HIGH,
129
- description: 'Batch delete files'
130
- },
131
- {
132
- pattern: /(mkfs|format)\s+/,
133
- category: 'System destruction',
134
- riskLevel: RiskLevel.CRITICAL,
135
- description: 'Format disk'
136
- },
137
- {
138
- pattern: /dd\s+.*of=\/dev\/(sd[a-z]|nvme[0-9]n[0-9])/,
139
- category: 'System destruction',
140
- riskLevel: RiskLevel.CRITICAL,
141
- description: 'Overwrite disk data'
142
- },
143
-
144
- // Privilege escalation
145
- {
146
- pattern: /chmod\s+777\s+/,
147
- category: 'Privilege escalation',
148
- riskLevel: RiskLevel.HIGH,
149
- description: 'Set file permissions to 777'
150
- },
151
- {
152
- pattern: /chmod\s+[45][0-9]{3}\s+/,
153
- category: 'Privilege escalation',
154
- riskLevel: RiskLevel.HIGH,
155
- description: 'Set SUID/SGID permissions'
156
- },
157
- {
158
- pattern: /vi\s+\/etc\/sudoers/,
159
- category: 'Privilege escalation',
160
- riskLevel: RiskLevel.CRITICAL,
161
- description: 'Modify sudo permissions'
162
- },
163
- {
164
- pattern: /echo.*>>.*\/etc\/sudoers/,
165
- category: 'Privilege escalation',
166
- riskLevel: RiskLevel.CRITICAL,
167
- description: 'Modify sudo permissions'
168
- },
169
-
170
- // Data theft
171
- {
172
- pattern: /cat\s+\/etc\/passwd/,
173
- category: 'Data theft',
174
- riskLevel: RiskLevel.HIGH,
175
- description: 'Read password file'
176
- },
177
- {
178
- pattern: /cat\s+\/etc\/shadow/,
179
- category: 'Data theft',
180
- riskLevel: RiskLevel.CRITICAL,
181
- description: 'Read shadow file'
182
- },
183
- {
184
- pattern: /cat\s+.*\/\.ssh\/id_rsa/,
185
- category: 'Data theft',
186
- riskLevel: RiskLevel.CRITICAL,
187
- description: 'Read SSH private key'
188
- },
189
- {
190
- pattern: /grep\s+-[rRi].*password/,
191
- category: 'Data theft',
192
- riskLevel: RiskLevel.HIGH,
193
- description: 'Search for password information'
194
- },
195
- {
196
- pattern: /(curl|wget).*\|(sh|bash|python|perl)/,
197
- category: 'Data theft',
198
- riskLevel: RiskLevel.CRITICAL,
199
- description: 'Remote code execution'
200
- },
201
-
202
- // Network attacks
203
- {
204
- pattern: /nmap\s+-[sS].*/,
205
- category: 'Network attacks',
206
- riskLevel: RiskLevel.MEDIUM,
207
- description: 'Network scanning'
208
- },
209
- {
210
- pattern: /nc\s+.*-l/,
211
- category: 'Network attacks',
212
- riskLevel: RiskLevel.HIGH,
213
- description: 'Create network listener'
214
- },
215
- {
216
- pattern: /iptables\s+-F/,
217
- category: 'Network attacks',
218
- riskLevel: RiskLevel.HIGH,
219
- description: 'Clear firewall rules'
220
- },
221
-
222
- // Resource exhaustion
223
- {
224
- pattern: /:\)\s*{\s*:\s*\|\s*:&\s*};/,
225
- category: 'Resource exhaustion',
226
- riskLevel: RiskLevel.CRITICAL,
227
- description: 'Fork bomb'
228
- },
229
- {
230
- pattern: /while\s+true\s*;\s*do\s+.*done/,
231
- category: 'Resource exhaustion',
232
- riskLevel: RiskLevel.HIGH,
233
- description: 'Infinite loop'
234
- }
235
- ];
236
-
237
- /**
238
- * Check if tool call matches blacklist rules
239
- */
240
- check(context: ToolCallContext): { matched: boolean; rule?: BlacklistRule } {
241
- const { toolName, params } = context;
242
-
243
- // For Bash tool, check command content
244
- if (toolName === 'Bash' && params.command) {
245
- const command = params.command as string;
246
-
247
- for (const rule of BlacklistChecker.RULES) {
248
- if (rule.pattern.test(command)) {
249
- return { matched: true, rule };
250
- }
251
- }
252
- }
253
-
254
- // For file operation tools, check path
255
- if (['Write', 'DeleteFile', 'replace'].includes(toolName)) {
256
- const filePath = params.filePath || params.file_path || '';
257
- if (this.isSystemPath(filePath)) {
258
- return {
259
- matched: true,
260
- rule: {
261
- pattern: /system-path/,
262
- category: 'System destruction',
263
- riskLevel: RiskLevel.HIGH,
264
- description: 'Modify system files'
265
- }
266
- };
267
- }
268
- }
269
-
270
- return { matched: false };
271
- }
272
-
273
- /**
274
- * Check if it's a system path
275
- */
276
- private isSystemPath(filePath: string): boolean {
277
- const systemPaths = [
278
- '/etc',
279
- '/usr',
280
- '/bin',
281
- '/sbin',
282
- '/lib',
283
- '/lib64',
284
- '/boot',
285
- '/sys',
286
- '/proc',
287
- '/dev'
288
- ];
289
-
290
- const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/');
291
- return systemPaths.some(sysPath => normalizedPath.startsWith(sysPath));
292
- }
293
-
294
- /**
295
- * Get all blacklist rules
296
- */
297
- getRules(): BlacklistRule[] {
298
- return [...BlacklistChecker.RULES];
299
- }
300
- }
301
-
302
- /**
303
- * AI approval checker
304
- */
305
- export class AIApprovalChecker {
306
- private aiClient: AIClient | null = null;
307
- private isRemoteMode: boolean = false;
308
-
309
- constructor() {
310
- this.initializeAIClient();
311
- }
312
-
313
- /**
314
- * Initialize AI client(s)
315
- */
316
- private async initializeAIClient(): Promise<void> {
317
- try {
318
- const configManager = getConfigManager();
319
- const authConfig = configManager.getAuthConfig();
320
-
321
- // Check if Remote mode (OAuth XAGENT)
322
- this.isRemoteMode = authConfig.type === AuthType.OAUTH_XAGENT;
323
-
324
- // Remote mode: AI review handled by remote LLM, no local AIClient needed
325
- // Local mode: use local AIClient
326
- if (!this.isRemoteMode && authConfig.apiKey) {
327
- this.aiClient = new AIClient(authConfig);
328
- }
329
- } catch (error) {
330
- logger.error('Failed to initialize AI approval checker', error instanceof Error ? error.message : String(error));
331
- }
332
- }
333
-
334
- /**
335
- * Use AI for intelligent review
336
- */
337
- async check(context: ToolCallContext): Promise<{ approved: boolean; analysis: string; riskLevel: RiskLevel }> {
338
- // In Remote mode, the remote LLM has already approved the tool_calls
339
- // Local AI review approves directly, no need to repeat
340
- if (this.isRemoteMode) {
341
- return {
342
- approved: true,
343
- analysis: 'Remote mode: tool approval handled by remote LLM',
344
- riskLevel: RiskLevel.LOW
345
- };
346
- }
347
-
348
- if (!this.aiClient) {
349
- // If AI client is not initialized, default to medium risk, requires user confirmation
350
- return {
351
- approved: false,
352
- analysis: 'AI review not available (no local LLM configured), requires manual user confirmation',
353
- riskLevel: RiskLevel.MEDIUM
354
- };
355
- }
356
-
357
- const prompt = this.buildApprovalPrompt(context);
358
-
359
- try {
360
- const messages: Message[] = [
361
- {
362
- role: 'system',
363
- content: `You are a security review expert. Your task is to evaluate the security of tool calls.
364
-
365
- Please analyze from the following dimensions:
366
- 1. Whether the operation has malicious intent
367
- 2. Whether it may cause data leakage
368
- 3. Whether it may compromise system integrity
369
- 4. Whether it follows best practices
370
-
371
- Please return results in JSON format:
372
- {
373
- "approved": boolean,
374
- "riskLevel": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
375
- "analysis": "Detailed analysis description"
376
- }`
377
- },
378
- {
379
- role: 'user',
380
- content: prompt
381
- }
382
- ];
383
-
384
- const response = await this.aiClient.chatCompletion(messages, {
385
- temperature: 0.3,
386
- // maxTokens: 500
387
- });
388
-
389
- const content = typeof response.choices[0].message.content === 'string'
390
- ? response.choices[0].message.content
391
- : '{}';
392
-
393
- // Parse AI response
394
- const jsonMatch = content.match(/\{[\s\S]*\}/);
395
- if (jsonMatch) {
396
- const result = JSON.parse(jsonMatch[0]);
397
- return {
398
- approved: result.approved || false,
399
- analysis: result.analysis || 'No detailed analysis',
400
- riskLevel: result.riskLevel || RiskLevel.MEDIUM
401
- };
402
- }
403
-
404
- // If unable to parse, return medium risk
405
- return {
406
- approved: false,
407
- analysis: 'Unable to parse AI response, requires manual confirmation',
408
- riskLevel: RiskLevel.MEDIUM
409
- };
410
- } catch (error: any) {
411
- logger.error('AI approval check failed', error instanceof Error ? error.message : String(error));
412
-
413
- // In Remote mode, remote LLM already approved, local failure means auto-approve
414
- const configManager = getConfigManager();
415
- const authConfig = configManager.getAuthConfig();
416
- const isRemoteMode = authConfig.type === AuthType.OAUTH_XAGENT;
417
-
418
- if (isRemoteMode) {
419
- return {
420
- approved: true,
421
- analysis: 'Remote mode: approved (remote LLM handled approval)',
422
- riskLevel: RiskLevel.LOW
423
- };
424
- }
425
-
426
- return {
427
- approved: false,
428
- analysis: `AI review failed: ${error.message}, requires manual confirmation`,
429
- riskLevel: RiskLevel.MEDIUM
430
- };
431
- }
432
- }
433
-
434
- /**
435
- * Build review prompt
436
- */
437
- private buildApprovalPrompt(context: ToolCallContext): string {
438
- const { toolName, params } = context;
439
-
440
- let prompt = `Tool name: ${toolName}\n`;
441
- prompt += `Parameters: ${JSON.stringify(params, null, 2)}\n\n`;
442
-
443
- // Add specific analysis guidance based on tool type
444
- if (toolName === 'Bash') {
445
- prompt += `This is a Shell command execution request. Please check if the command contains:\n- Dangerous system operations (such as deletion, formatting)\n- Privilege escalation operations\n- Data theft operations\n- Remote code execution\n- Resource exhaustion attacks`;
446
- } else if (['Write', 'replace', 'DeleteFile'].includes(toolName)) {
447
- prompt += `This is a file operation request. Please check:\n- Whether the target path is a system path\n- Whether the operation may damage system files\n- Whether it involves sensitive configuration files`;
448
- } else if (toolName === 'web_fetch' || toolName === 'web_search') {
449
- prompt += `This is a network request. Please check:\n- Whether the URL is a malicious website\n- Whether it may leak sensitive information\n- Whether it may execute remote code`;
450
- }
451
-
452
- return prompt;
453
- }
454
- }
455
-
456
- /**
457
- * Smart approval engine
458
- */
459
- export class SmartApprovalEngine {
460
- private whitelistChecker: WhitelistChecker;
461
- private blacklistChecker: BlacklistChecker;
462
- private aiChecker: AIApprovalChecker;
463
- private debugMode: boolean;
464
-
465
- constructor(debugMode: boolean = false) {
466
- this.whitelistChecker = new WhitelistChecker();
467
- this.blacklistChecker = new BlacklistChecker();
468
- this.aiChecker = new AIApprovalChecker();
469
- this.debugMode = debugMode;
470
- }
471
-
472
- /**
473
- * Evaluate tool call
474
- */
475
- async evaluate(context: ToolCallContext): Promise<ApprovalResult> {
476
- const startTime = Date.now();
477
-
478
- if (this.debugMode) {
479
- logger.debug(`[SmartApprovalEngine] Evaluating tool call: ${context.toolName}`);
480
- }
481
-
482
- // First layer: Whitelist check
483
- const whitelistCheck = this.whitelistChecker.check(context.toolName);
484
- if (whitelistCheck) {
485
- const latency = Date.now() - startTime;
486
- if (this.debugMode) {
487
- logger.debug(`[WhitelistChecker] Tool '${context.toolName}' in whitelist, latency: ${latency}ms`);
488
- }
489
-
490
- return {
491
- decision: ApprovalDecision.APPROVED,
492
- riskLevel: RiskLevel.LOW,
493
- detectionMethod: 'whitelist',
494
- description: `Tool '${context.toolName}' is in the whitelist, executing directly`,
495
- latency
496
- };
497
- }
498
-
499
- if (this.debugMode) {
500
- logger.debug(`[WhitelistChecker] Tool '${context.toolName}' not in whitelist`);
501
- }
502
-
503
- // Second layer: Blacklist check
504
- const blacklistCheck = this.blacklistChecker.check(context);
505
- if (blacklistCheck.matched && blacklistCheck.rule) {
506
- const latency = Date.now() - startTime;
507
- if (this.debugMode) {
508
- logger.debug(`[BlacklistChecker] Matched rule: ${blacklistCheck.rule.description}, Risk: ${blacklistCheck.rule.riskLevel}, latency: ${latency}ms`);
509
- }
510
-
511
- return {
512
- decision: ApprovalDecision.REQUIRES_CONFIRMATION,
513
- riskLevel: blacklistCheck.rule.riskLevel,
514
- detectionMethod: 'blacklist',
515
- description: `Detected potentially risky operation: ${blacklistCheck.rule.description}`,
516
- latency
517
- };
518
- }
519
-
520
- if (this.debugMode) {
521
- logger.debug(`[BlacklistChecker] No blacklist rule matched`);
522
- }
523
-
524
- // Third layer: AI intelligent review
525
- const aiCheck = await this.aiChecker.check(context);
526
- const latency = Date.now() - startTime;
527
-
528
- if (this.debugMode) {
529
- logger.debug(`[AIApprovalChecker] AI review result: approved=${aiCheck.approved}, risk=${aiCheck.riskLevel}, latency: ${latency}ms`);
530
- }
531
-
532
- return {
533
- decision: aiCheck.approved ? ApprovalDecision.APPROVED : ApprovalDecision.REQUIRES_CONFIRMATION,
534
- riskLevel: aiCheck.riskLevel,
535
- detectionMethod: 'ai_review',
536
- description: aiCheck.analysis,
537
- latency,
538
- aiAnalysis: aiCheck.analysis
539
- };
540
- }
541
-
542
- /**
543
- * Request user confirmation
544
- */
545
- async requestConfirmation(result: ApprovalResult): Promise<boolean> {
546
- const separator = icons.separator.repeat(40);
547
- console.log('');
548
- console.log(colors.warning(`${icons.warning} [Smart Mode] Detected potentially risky operation`));
549
- console.log(colors.border(separator));
550
- console.log('');
551
- console.log(colors.textMuted(`📊 Risk Level: ${this.getRiskLevelDisplay(result.riskLevel)}`));
552
- console.log(colors.textMuted(`🔍 Detection Method: ${this.getDetectionMethodDisplay(result.detectionMethod)}`));
553
- console.log('');
554
-
555
- if (result.aiAnalysis) {
556
- console.log(colors.textMuted(`🤖 AI Analysis:`));
557
- console.log(colors.textDim(` ${result.aiAnalysis}`));
558
- console.log('');
559
- }
560
-
561
- console.log(colors.textMuted(`⚠️ Risk Description: ${result.description}`));
562
- console.log('');
563
- console.log(colors.warning('Potentially risky operation detected, continue execution?'));
564
-
565
- try {
566
- const { confirmed } = await inquirer.prompt([
567
- {
568
- type: 'confirm',
569
- name: 'confirmed',
570
- message: 'Continue execution?',
571
- default: false
572
- }
573
- ]);
574
-
575
- return confirmed;
576
- } catch (error) {
577
- logger.error('Failed to get user confirmation', error instanceof Error ? error.message : String(error));
578
- return false;
579
- }
580
- }
581
-
582
- /**
583
- * Get risk level display
584
- */
585
- private getRiskLevelDisplay(riskLevel: RiskLevel): string {
586
- const displays = {
587
- [RiskLevel.LOW]: colors.success('LOW'),
588
- [RiskLevel.MEDIUM]: colors.warning('MEDIUM'),
589
- [RiskLevel.HIGH]: colors.error('HIGH'),
590
- [RiskLevel.CRITICAL]: colors.error('CRITICAL')
591
- };
592
- return displays[riskLevel];
593
- }
594
-
595
- /**
596
- * Get detection method display
597
- */
598
- private getDetectionMethodDisplay(method: string): string {
599
- const displays = {
600
- whitelist: 'Whitelist rules',
601
- blacklist: 'Blacklist rules',
602
- ai_review: 'AI intelligent review',
603
- manual: 'Manual review'
604
- };
605
- return displays[method as keyof typeof displays] || method;
606
- }
607
-
608
- /**
609
- * Set debug mode
610
- */
611
- setDebugMode(enabled: boolean): void {
612
- this.debugMode = enabled;
613
- }
614
- }
615
-
616
- /**
617
- * Get smart approval engine instance
618
- */
619
- let smartApprovalEngineInstance: SmartApprovalEngine | null = null;
620
-
621
- export function getSmartApprovalEngine(debugMode: boolean = false): SmartApprovalEngine {
622
- if (!smartApprovalEngineInstance) {
623
- smartApprovalEngineInstance = new SmartApprovalEngine(debugMode);
624
- }
625
- return smartApprovalEngineInstance;
1
+ import inquirer from 'inquirer';
2
+ import { AIClient, Message } from './ai-client.js';
3
+ import { getConfigManager } from './config.js';
4
+ import { AuthType } from './types.js';
5
+ import { getLogger } from './logger.js';
6
+ import { colors, icons } from './theme.js';
7
+
8
+ const logger = getLogger();
9
+
10
+ /**
11
+ * Approval result type
12
+ */
13
+ export enum ApprovalDecision {
14
+ APPROVED = 'approved',
15
+ REJECTED = 'rejected',
16
+ REQUIRES_CONFIRMATION = 'requires_confirmation',
17
+ AI_REVIEW = 'ai_review'
18
+ }
19
+
20
+ /**
21
+ * Risk level
22
+ */
23
+ export enum RiskLevel {
24
+ LOW = 'LOW',
25
+ MEDIUM = 'MEDIUM',
26
+ HIGH = 'HIGH',
27
+ CRITICAL = 'CRITICAL'
28
+ }
29
+
30
+ /**
31
+ * Approval result
32
+ */
33
+ export interface ApprovalResult {
34
+ decision: ApprovalDecision;
35
+ riskLevel: RiskLevel;
36
+ detectionMethod: 'whitelist' | 'blacklist' | 'ai_review' | 'manual';
37
+ description: string;
38
+ latency: number;
39
+ aiAnalysis?: string;
40
+ }
41
+
42
+ /**
43
+ * Tool call context
44
+ */
45
+ export interface ToolCallContext {
46
+ toolName: string;
47
+ params: any;
48
+ timestamp: number;
49
+ }
50
+
51
+ /**
52
+ * Whitelist checker
53
+ */
54
+ export class WhitelistChecker {
55
+ private static readonly WHITELISTED_TOOLS: Set<string> = new Set([
56
+ // Information reading tools
57
+ 'Read',
58
+ 'ListDirectory',
59
+ 'SearchFiles',
60
+ 'Grep',
61
+ 'image_read',
62
+
63
+ // Task management tools
64
+ 'todo_write',
65
+ 'todo_read',
66
+ 'task',
67
+ 'exit_plan_mode',
68
+ 'web_search',
69
+
70
+ // File editing tools
71
+ 'Edit',
72
+ 'Write',
73
+ 'DeleteFile',
74
+
75
+ // Other safe tools
76
+ 'web_fetch',
77
+ 'ask_user_question',
78
+ 'save_memory',
79
+ 'xml_escape',
80
+ 'Skill'
81
+ ]);
82
+
83
+ /**
84
+ * Check if tool is in whitelist
85
+ */
86
+ check(toolName: string): boolean {
87
+ return WhitelistChecker.WHITELISTED_TOOLS.has(toolName);
88
+ }
89
+
90
+ /**
91
+ * Get list of whitelisted tools
92
+ */
93
+ getWhitelistedTools(): string[] {
94
+ return Array.from(WhitelistChecker.WHITELISTED_TOOLS);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Blacklist rules
100
+ */
101
+ interface BlacklistRule {
102
+ pattern: RegExp;
103
+ category: string;
104
+ riskLevel: RiskLevel;
105
+ description: string;
106
+ }
107
+
108
+ /**
109
+ * Blacklist checker
110
+ */
111
+ export class BlacklistChecker {
112
+ private static readonly RULES: BlacklistRule[] = [
113
+ // System destruction
114
+ {
115
+ pattern: /rm\s+-rf\s+\/$/,
116
+ category: 'System destruction',
117
+ riskLevel: RiskLevel.CRITICAL,
118
+ description: 'Delete root directory'
119
+ },
120
+ {
121
+ pattern: /rm\s+-rf\s+(\/etc|\/usr|\/bin|\/sbin|\/lib|\/lib64)/,
122
+ category: 'System destruction',
123
+ riskLevel: RiskLevel.CRITICAL,
124
+ description: 'Delete system directories'
125
+ },
126
+ {
127
+ pattern: /rm\s+-rf\s+.*\*/,
128
+ category: 'System destruction',
129
+ riskLevel: RiskLevel.HIGH,
130
+ description: 'Batch delete files'
131
+ },
132
+ {
133
+ pattern: /(mkfs|format)\s+/,
134
+ category: 'System destruction',
135
+ riskLevel: RiskLevel.CRITICAL,
136
+ description: 'Format disk'
137
+ },
138
+ {
139
+ pattern: /dd\s+.*of=\/dev\/(sd[a-z]|nvme[0-9]n[0-9])/,
140
+ category: 'System destruction',
141
+ riskLevel: RiskLevel.CRITICAL,
142
+ description: 'Overwrite disk data'
143
+ },
144
+
145
+ // Privilege escalation
146
+ {
147
+ pattern: /chmod\s+777\s+/,
148
+ category: 'Privilege escalation',
149
+ riskLevel: RiskLevel.HIGH,
150
+ description: 'Set file permissions to 777'
151
+ },
152
+ {
153
+ pattern: /chmod\s+[45][0-9]{3}\s+/,
154
+ category: 'Privilege escalation',
155
+ riskLevel: RiskLevel.HIGH,
156
+ description: 'Set SUID/SGID permissions'
157
+ },
158
+ {
159
+ pattern: /vi\s+\/etc\/sudoers/,
160
+ category: 'Privilege escalation',
161
+ riskLevel: RiskLevel.CRITICAL,
162
+ description: 'Modify sudo permissions'
163
+ },
164
+ {
165
+ pattern: /echo.*>>.*\/etc\/sudoers/,
166
+ category: 'Privilege escalation',
167
+ riskLevel: RiskLevel.CRITICAL,
168
+ description: 'Modify sudo permissions'
169
+ },
170
+
171
+ // Data theft
172
+ {
173
+ pattern: /cat\s+\/etc\/passwd/,
174
+ category: 'Data theft',
175
+ riskLevel: RiskLevel.HIGH,
176
+ description: 'Read password file'
177
+ },
178
+ {
179
+ pattern: /cat\s+\/etc\/shadow/,
180
+ category: 'Data theft',
181
+ riskLevel: RiskLevel.CRITICAL,
182
+ description: 'Read shadow file'
183
+ },
184
+ {
185
+ pattern: /cat\s+.*\/\.ssh\/id_rsa/,
186
+ category: 'Data theft',
187
+ riskLevel: RiskLevel.CRITICAL,
188
+ description: 'Read SSH private key'
189
+ },
190
+ {
191
+ pattern: /grep\s+-[rRi].*password/,
192
+ category: 'Data theft',
193
+ riskLevel: RiskLevel.HIGH,
194
+ description: 'Search for password information'
195
+ },
196
+ {
197
+ pattern: /(curl|wget).*\|(sh|bash|python|perl)/,
198
+ category: 'Data theft',
199
+ riskLevel: RiskLevel.CRITICAL,
200
+ description: 'Remote code execution'
201
+ },
202
+
203
+ // Network attacks
204
+ {
205
+ pattern: /nmap\s+-[sS].*/,
206
+ category: 'Network attacks',
207
+ riskLevel: RiskLevel.MEDIUM,
208
+ description: 'Network scanning'
209
+ },
210
+ {
211
+ pattern: /nc\s+.*-l/,
212
+ category: 'Network attacks',
213
+ riskLevel: RiskLevel.HIGH,
214
+ description: 'Create network listener'
215
+ },
216
+ {
217
+ pattern: /iptables\s+-F/,
218
+ category: 'Network attacks',
219
+ riskLevel: RiskLevel.HIGH,
220
+ description: 'Clear firewall rules'
221
+ },
222
+
223
+ // Resource exhaustion
224
+ {
225
+ pattern: /:\)\s*{\s*:\s*\|\s*:&\s*};/,
226
+ category: 'Resource exhaustion',
227
+ riskLevel: RiskLevel.CRITICAL,
228
+ description: 'Fork bomb'
229
+ },
230
+ {
231
+ pattern: /while\s+true\s*;\s*do\s+.*done/,
232
+ category: 'Resource exhaustion',
233
+ riskLevel: RiskLevel.HIGH,
234
+ description: 'Infinite loop'
235
+ }
236
+ ];
237
+
238
+ /**
239
+ * Check if tool call matches blacklist rules
240
+ */
241
+ check(context: ToolCallContext): { matched: boolean; rule?: BlacklistRule } {
242
+ const { toolName, params } = context;
243
+
244
+ // For Bash tool, check command content
245
+ if (toolName === 'Bash' && params.command) {
246
+ const command = params.command as string;
247
+
248
+ for (const rule of BlacklistChecker.RULES) {
249
+ if (rule.pattern.test(command)) {
250
+ return { matched: true, rule };
251
+ }
252
+ }
253
+ }
254
+
255
+ // For file operation tools, check path
256
+ if (['Write', 'DeleteFile', 'Edit'].includes(toolName)) {
257
+ const filePath = params.filePath || params.file_path || '';
258
+ if (this.isSystemPath(filePath)) {
259
+ return {
260
+ matched: true,
261
+ rule: {
262
+ pattern: /system-path/,
263
+ category: 'System destruction',
264
+ riskLevel: RiskLevel.HIGH,
265
+ description: 'Modify system files'
266
+ }
267
+ };
268
+ }
269
+ }
270
+
271
+ return { matched: false };
272
+ }
273
+
274
+ /**
275
+ * Check if it's a system path
276
+ */
277
+ private isSystemPath(filePath: string): boolean {
278
+ const systemPaths = [
279
+ '/etc',
280
+ '/usr',
281
+ '/bin',
282
+ '/sbin',
283
+ '/lib',
284
+ '/lib64',
285
+ '/boot',
286
+ '/sys',
287
+ '/proc',
288
+ '/dev'
289
+ ];
290
+
291
+ const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/');
292
+ return systemPaths.some(sysPath => normalizedPath.startsWith(sysPath));
293
+ }
294
+
295
+ /**
296
+ * Get all blacklist rules
297
+ */
298
+ getRules(): BlacklistRule[] {
299
+ return [...BlacklistChecker.RULES];
300
+ }
301
+ }
302
+
303
+ /**
304
+ * AI approval checker
305
+ */
306
+ export class AIApprovalChecker {
307
+ private aiClient: AIClient | null = null;
308
+ private isRemoteMode: boolean = false;
309
+
310
+ constructor() {
311
+ this.initializeAIClient();
312
+ }
313
+
314
+ /**
315
+ * Initialize AI client(s)
316
+ */
317
+ private async initializeAIClient(): Promise<void> {
318
+ try {
319
+ const configManager = getConfigManager();
320
+ const authConfig = configManager.getAuthConfig();
321
+
322
+ // Check if Remote mode (OAuth XAGENT)
323
+ this.isRemoteMode = authConfig.type === AuthType.OAUTH_XAGENT;
324
+
325
+ // Remote mode: AI review handled by remote LLM, no local AIClient needed
326
+ // Local mode: use local AIClient
327
+ if (!this.isRemoteMode && authConfig.apiKey) {
328
+ this.aiClient = new AIClient(authConfig);
329
+ }
330
+ } catch (error) {
331
+ logger.error('Failed to initialize AI approval checker', error instanceof Error ? error.message : String(error));
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Use AI for intelligent review
337
+ */
338
+ async check(context: ToolCallContext): Promise<{ approved: boolean; analysis: string; riskLevel: RiskLevel }> {
339
+ // In Remote mode, the remote LLM has already approved the tool_calls
340
+ // Local AI review approves directly, no need to repeat
341
+ if (this.isRemoteMode) {
342
+ return {
343
+ approved: true,
344
+ analysis: 'Remote mode: tool approval handled by remote LLM',
345
+ riskLevel: RiskLevel.LOW
346
+ };
347
+ }
348
+
349
+ if (!this.aiClient) {
350
+ // If AI client is not initialized, default to medium risk, requires user confirmation
351
+ return {
352
+ approved: false,
353
+ analysis: 'AI review not available (no local LLM configured), requires manual user confirmation',
354
+ riskLevel: RiskLevel.MEDIUM
355
+ };
356
+ }
357
+
358
+ const prompt = this.buildApprovalPrompt(context);
359
+
360
+ try {
361
+ const messages: Message[] = [
362
+ {
363
+ role: 'system',
364
+ content: `You are a security review expert. Your task is to evaluate the security of tool calls.
365
+
366
+ Please analyze from the following dimensions:
367
+ 1. Whether the operation has malicious intent
368
+ 2. Whether it may cause data leakage
369
+ 3. Whether it may compromise system integrity
370
+ 4. Whether it follows best practices
371
+
372
+ Please return results in JSON format:
373
+ {
374
+ "approved": boolean,
375
+ "riskLevel": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
376
+ "analysis": "Detailed analysis description"
377
+ }`
378
+ },
379
+ {
380
+ role: 'user',
381
+ content: prompt
382
+ }
383
+ ];
384
+
385
+ const response = await this.aiClient.chatCompletion(messages, {
386
+ temperature: 0.3,
387
+ // maxTokens: 500
388
+ });
389
+
390
+ const content = typeof response.choices[0].message.content === 'string'
391
+ ? response.choices[0].message.content
392
+ : '{}';
393
+
394
+ // Parse AI response
395
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
396
+ if (jsonMatch) {
397
+ const result = JSON.parse(jsonMatch[0]);
398
+ return {
399
+ approved: result.approved || false,
400
+ analysis: result.analysis || 'No detailed analysis',
401
+ riskLevel: result.riskLevel || RiskLevel.MEDIUM
402
+ };
403
+ }
404
+
405
+ // If unable to parse, return medium risk
406
+ return {
407
+ approved: false,
408
+ analysis: 'Unable to parse AI response, requires manual confirmation',
409
+ riskLevel: RiskLevel.MEDIUM
410
+ };
411
+ } catch (error: any) {
412
+ logger.error('AI approval check failed', error instanceof Error ? error.message : String(error));
413
+
414
+ // In Remote mode, remote LLM already approved, local failure means auto-approve
415
+ const configManager = getConfigManager();
416
+ const authConfig = configManager.getAuthConfig();
417
+ const isRemoteMode = authConfig.type === AuthType.OAUTH_XAGENT;
418
+
419
+ if (isRemoteMode) {
420
+ return {
421
+ approved: true,
422
+ analysis: 'Remote mode: approved (remote LLM handled approval)',
423
+ riskLevel: RiskLevel.LOW
424
+ };
425
+ }
426
+
427
+ return {
428
+ approved: false,
429
+ analysis: `AI review failed: ${error.message}, requires manual confirmation`,
430
+ riskLevel: RiskLevel.MEDIUM
431
+ };
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Build review prompt
437
+ */
438
+ private buildApprovalPrompt(context: ToolCallContext): string {
439
+ const { toolName, params } = context;
440
+
441
+ let prompt = `Tool name: ${toolName}\n`;
442
+ prompt += `Parameters: ${JSON.stringify(params, null, 2)}\n\n`;
443
+
444
+ // Add specific analysis guidance based on tool type
445
+ if (toolName === 'Bash') {
446
+ prompt += `This is a Shell command execution request. Please check if the command contains:\n- Dangerous system operations (such as deletion, formatting)\n- Privilege escalation operations\n- Data theft operations\n- Remote code execution\n- Resource exhaustion attacks`;
447
+ } else if (['Write', 'Edit', 'DeleteFile'].includes(toolName)) {
448
+ prompt += `This is a file operation request. Please check:\n- Whether the target path is a system path\n- Whether the operation may damage system files\n- Whether it involves sensitive configuration files`;
449
+ } else if (toolName === 'web_fetch' || toolName === 'web_search') {
450
+ prompt += `This is a network request. Please check:\n- Whether the URL is a malicious website\n- Whether it may leak sensitive information\n- Whether it may execute remote code`;
451
+ }
452
+
453
+ return prompt;
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Smart approval engine
459
+ */
460
+ export class SmartApprovalEngine {
461
+ private whitelistChecker: WhitelistChecker;
462
+ private blacklistChecker: BlacklistChecker;
463
+ private aiChecker: AIApprovalChecker;
464
+ private debugMode: boolean;
465
+
466
+ constructor(debugMode: boolean = false) {
467
+ this.whitelistChecker = new WhitelistChecker();
468
+ this.blacklistChecker = new BlacklistChecker();
469
+ this.aiChecker = new AIApprovalChecker();
470
+ this.debugMode = debugMode;
471
+ }
472
+
473
+ /**
474
+ * Evaluate tool call
475
+ */
476
+ async evaluate(context: ToolCallContext): Promise<ApprovalResult> {
477
+ const startTime = Date.now();
478
+
479
+ if (this.debugMode) {
480
+ logger.debug(`[SmartApprovalEngine] Evaluating tool call: ${context.toolName}`);
481
+ }
482
+
483
+ // First layer: Whitelist check
484
+ const whitelistCheck = this.whitelistChecker.check(context.toolName);
485
+ if (whitelistCheck) {
486
+ const latency = Date.now() - startTime;
487
+ if (this.debugMode) {
488
+ logger.debug(`[WhitelistChecker] Tool '${context.toolName}' in whitelist, latency: ${latency}ms`);
489
+ }
490
+
491
+ return {
492
+ decision: ApprovalDecision.APPROVED,
493
+ riskLevel: RiskLevel.LOW,
494
+ detectionMethod: 'whitelist',
495
+ description: `Tool '${context.toolName}' is in the whitelist, executing directly`,
496
+ latency
497
+ };
498
+ }
499
+
500
+ if (this.debugMode) {
501
+ logger.debug(`[WhitelistChecker] Tool '${context.toolName}' not in whitelist`);
502
+ }
503
+
504
+ // Second layer: Blacklist check
505
+ const blacklistCheck = this.blacklistChecker.check(context);
506
+ if (blacklistCheck.matched && blacklistCheck.rule) {
507
+ const latency = Date.now() - startTime;
508
+ if (this.debugMode) {
509
+ logger.debug(`[BlacklistChecker] Matched rule: ${blacklistCheck.rule.description}, Risk: ${blacklistCheck.rule.riskLevel}, latency: ${latency}ms`);
510
+ }
511
+
512
+ return {
513
+ decision: ApprovalDecision.REQUIRES_CONFIRMATION,
514
+ riskLevel: blacklistCheck.rule.riskLevel,
515
+ detectionMethod: 'blacklist',
516
+ description: `Detected potentially risky operation: ${blacklistCheck.rule.description}`,
517
+ latency
518
+ };
519
+ }
520
+
521
+ if (this.debugMode) {
522
+ logger.debug(`[BlacklistChecker] No blacklist rule matched`);
523
+ }
524
+
525
+ // Third layer: AI intelligent review
526
+ const aiCheck = await this.aiChecker.check(context);
527
+ const latency = Date.now() - startTime;
528
+
529
+ if (this.debugMode) {
530
+ logger.debug(`[AIApprovalChecker] AI review result: approved=${aiCheck.approved}, risk=${aiCheck.riskLevel}, latency: ${latency}ms`);
531
+ }
532
+
533
+ return {
534
+ decision: aiCheck.approved ? ApprovalDecision.APPROVED : ApprovalDecision.REQUIRES_CONFIRMATION,
535
+ riskLevel: aiCheck.riskLevel,
536
+ detectionMethod: 'ai_review',
537
+ description: aiCheck.analysis,
538
+ latency,
539
+ aiAnalysis: aiCheck.analysis
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Request user confirmation
545
+ */
546
+ async requestConfirmation(result: ApprovalResult): Promise<boolean> {
547
+ const separator = icons.separator.repeat(40);
548
+ console.log('');
549
+ console.log(colors.warning(`${icons.warning} [Smart Mode] Detected potentially risky operation`));
550
+ console.log(colors.border(separator));
551
+ console.log('');
552
+ console.log(colors.textMuted(`📊 Risk Level: ${this.getRiskLevelDisplay(result.riskLevel)}`));
553
+ console.log(colors.textMuted(`🔍 Detection Method: ${this.getDetectionMethodDisplay(result.detectionMethod)}`));
554
+ console.log('');
555
+
556
+ if (result.aiAnalysis) {
557
+ console.log(colors.textMuted(`🤖 AI Analysis:`));
558
+ console.log(colors.textDim(` ${result.aiAnalysis}`));
559
+ console.log('');
560
+ }
561
+
562
+ console.log(colors.textMuted(`⚠️ Risk Description: ${result.description}`));
563
+ console.log('');
564
+ console.log(colors.warning('Potentially risky operation detected, continue execution?'));
565
+
566
+ try {
567
+ const { confirmed } = await inquirer.prompt([
568
+ {
569
+ type: 'confirm',
570
+ name: 'confirmed',
571
+ message: 'Continue execution?',
572
+ default: false
573
+ }
574
+ ]);
575
+
576
+ return confirmed;
577
+ } catch (error) {
578
+ logger.error('Failed to get user confirmation', error instanceof Error ? error.message : String(error));
579
+ return false;
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Get risk level display
585
+ */
586
+ private getRiskLevelDisplay(riskLevel: RiskLevel): string {
587
+ const displays = {
588
+ [RiskLevel.LOW]: colors.success('LOW'),
589
+ [RiskLevel.MEDIUM]: colors.warning('MEDIUM'),
590
+ [RiskLevel.HIGH]: colors.error('HIGH'),
591
+ [RiskLevel.CRITICAL]: colors.error('CRITICAL')
592
+ };
593
+ return displays[riskLevel];
594
+ }
595
+
596
+ /**
597
+ * Get detection method display
598
+ */
599
+ private getDetectionMethodDisplay(method: string): string {
600
+ const displays = {
601
+ whitelist: 'Whitelist rules',
602
+ blacklist: 'Blacklist rules',
603
+ ai_review: 'AI intelligent review',
604
+ manual: 'Manual review'
605
+ };
606
+ return displays[method as keyof typeof displays] || method;
607
+ }
608
+
609
+ /**
610
+ * Set debug mode
611
+ */
612
+ setDebugMode(enabled: boolean): void {
613
+ this.debugMode = enabled;
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Get smart approval engine instance
619
+ */
620
+ let smartApprovalEngineInstance: SmartApprovalEngine | null = null;
621
+
622
+ export function getSmartApprovalEngine(debugMode: boolean = false): SmartApprovalEngine {
623
+ if (!smartApprovalEngineInstance) {
624
+ smartApprovalEngineInstance = new SmartApprovalEngine(debugMode);
625
+ }
626
+ return smartApprovalEngineInstance;
626
627
  }