@lobehub/lobehub 2.0.0-next.94 → 2.0.0-next.95

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.
@@ -20,15 +20,6 @@ jobs:
20
20
  pull-requests: write # for actions-cool/issues-helper to update PRs
21
21
  runs-on: ubuntu-latest
22
22
  steps:
23
- - name: Auto Comment on Issues Opened
24
- uses: wow-actions/auto-comment@v1
25
- with:
26
- GITHUB_TOKEN: ${{ secrets.GH_TOKEN}}
27
- issuesOpened: |
28
- 👀 @{{ author }}
29
-
30
- Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible.
31
- Please make sure you have given us as much context as possible.
32
23
  - name: Auto Comment on Issues Closed
33
24
  uses: wow-actions/auto-comment@v1
34
25
  with:
@@ -37,16 +28,6 @@ jobs:
37
28
  ✅ @{{ author }}
38
29
 
39
30
  This issue is closed, If you have any questions, you can comment and reply.
40
- - name: Auto Comment on Pull Request Opened
41
- uses: wow-actions/auto-comment@v1
42
- with:
43
- GITHUB_TOKEN: ${{ secrets.GH_TOKEN}}
44
- pullRequestOpened: |
45
- 👍 @{{ author }}
46
-
47
- Thank you for raising your pull request and contributing to our Community
48
- Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
49
- If you encounter any problems, please feel free to connect with us.
50
31
  - name: Auto Comment on Pull Request Merged
51
32
  uses: actions-cool/pr-welcome@main
52
33
  if: github.event.pull_request.merged == true
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.95](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.94...v2.0.0-next.95)
6
+
7
+ <sup>Released on **2025-11-20**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add Security Blacklist for agent runtime.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add Security Blacklist for agent runtime, closes [#10325](https://github.com/lobehub/lobe-chat/issues/10325) ([deab4d0](https://github.com/lobehub/lobe-chat/commit/deab4d0))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.94](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.93...v2.0.0-next.94)
6
31
 
7
32
  <sup>Released on **2025-11-20**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Add Security Blacklist for agent runtime."
6
+ ]
7
+ },
8
+ "date": "2025-11-20",
9
+ "version": "2.0.0-next.95"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.94",
3
+ "version": "2.0.0-next.95",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -2,14 +2,56 @@ import type {
2
2
  ArgumentMatcher,
3
3
  HumanInterventionPolicy,
4
4
  HumanInterventionRule,
5
+ SecurityBlacklistRule,
5
6
  ShouldInterveneParams,
6
7
  } from '@lobechat/types';
7
8
 
9
+ import { DEFAULT_SECURITY_BLACKLIST } from './defaultSecurityBlacklist';
10
+
11
+ /**
12
+ * Result of security blacklist check
13
+ */
14
+ export interface SecurityCheckResult {
15
+ /**
16
+ * Whether the operation is blocked by security rules
17
+ */
18
+ blocked: boolean;
19
+
20
+ /**
21
+ * Reason for blocking (if blocked)
22
+ */
23
+ reason?: string;
24
+ }
25
+
8
26
  /**
9
27
  * Intervention Checker
10
28
  * Determines whether a tool call requires human intervention
11
29
  */
12
30
  export class InterventionChecker {
31
+ /**
32
+ * Check if tool call is blocked by security blacklist
33
+ * This check runs BEFORE all other intervention checks
34
+ *
35
+ * @param securityBlacklist - Security blacklist rules
36
+ * @param toolArgs - Tool call arguments
37
+ * @returns Security check result
38
+ */
39
+ static checkSecurityBlacklist(
40
+ securityBlacklist: SecurityBlacklistRule[] = [],
41
+ toolArgs: Record<string, any> = {},
42
+ ): SecurityCheckResult {
43
+ for (const rule of securityBlacklist) {
44
+ if (this.matchesSecurityRule(rule, toolArgs)) {
45
+ return {
46
+ blocked: true,
47
+ reason: rule.description,
48
+ };
49
+ }
50
+ }
51
+
52
+ return { blocked: false };
53
+ }
54
+
13
55
  /**
14
56
  * Check if a tool call requires intervention
15
57
  *
@@ -19,6 +61,19 @@ export class InterventionChecker {
19
61
  static shouldIntervene(params: ShouldInterveneParams): HumanInterventionPolicy {
20
62
  const { config, toolArgs = {} } = params;
21
63
 
64
+ // Use default blacklist if not provided
65
+ const securityBlacklist =
66
+ params.securityBlacklist !== undefined
67
+ ? params.securityBlacklist
68
+ : DEFAULT_SECURITY_BLACKLIST;
69
+
70
+ // CRITICAL: Check security blacklist first - this overrides ALL other settings
71
+ const securityCheck = this.checkSecurityBlacklist(securityBlacklist, toolArgs);
72
+ if (securityCheck.blocked) {
73
+ // Security blacklist always requires intervention, even in auto-run mode
74
+ return 'required';
75
+ }
76
+
22
77
  // No config means never intervene (auto-execute)
23
78
  if (!config) return 'never';
24
79
 
@@ -38,6 +93,36 @@ export class InterventionChecker {
38
93
  return 'required';
39
94
  }
40
95
 
96
+ /**
97
+ * Check if tool arguments match a security blacklist rule
98
+ *
99
+ * @param rule - Security rule to check
100
+ * @param toolArgs - Tool call arguments
101
+ * @returns true if matches (should be blocked)
102
+ */
103
+ private static matchesSecurityRule(
104
+ rule: SecurityBlacklistRule,
105
+ toolArgs: Record<string, any>,
106
+ ): boolean {
107
+ // Security rules must have match criteria
108
+ if (!rule.match) return false;
109
+
110
+ // All matchers must match (AND logic)
111
+ for (const [paramName, matcher] of Object.entries(rule.match)) {
112
+ const paramValue = toolArgs[paramName];
113
+
114
+ // Parameter not present in args - rule doesn't match
115
+ if (paramValue === undefined) return false;
116
+
117
+ // Check if value matches
118
+ if (!this.matchesArgument(matcher, paramValue)) {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ return true;
124
+ }
125
+
41
126
  /**
42
127
  * Check if tool arguments match a rule
43
128
  *
@@ -1,20 +1,35 @@
1
- import type { HumanInterventionConfig } from '@lobechat/types';
1
+ import type { HumanInterventionConfig, SecurityBlacklistConfig } from '@lobechat/types';
2
2
  import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import { InterventionChecker } from '../InterventionChecker';
5
+ import { DEFAULT_SECURITY_BLACKLIST } from '../defaultSecurityBlacklist';
5
6
 
6
7
  describe('InterventionChecker', () => {
7
8
  describe('shouldIntervene', () => {
8
9
  it('should return never when config is undefined', () => {
9
- const result = InterventionChecker.shouldIntervene({ config: undefined, toolArgs: {} });
10
+ const result = InterventionChecker.shouldIntervene({
11
+ config: undefined,
12
+ securityBlacklist: [], // Disable blacklist for this test
13
+ toolArgs: {},
14
+ });
10
15
  expect(result).toBe('never');
11
16
  });
12
17
 
13
18
  it('should return the policy when config is a simple string', () => {
14
- expect(InterventionChecker.shouldIntervene({ config: 'never', toolArgs: {} })).toBe('never');
15
- expect(InterventionChecker.shouldIntervene({ config: 'required', toolArgs: {} })).toBe(
16
- 'required',
17
- );
19
+ expect(
20
+ InterventionChecker.shouldIntervene({
21
+ config: 'never',
22
+ securityBlacklist: [], // Disable blacklist for this test
23
+ toolArgs: {},
24
+ }),
25
+ ).toBe('never');
26
+ expect(
27
+ InterventionChecker.shouldIntervene({
28
+ config: 'required',
29
+ securityBlacklist: [], // Disable blacklist for this test
30
+ toolArgs: {},
31
+ }),
32
+ ).toBe('required');
18
33
  });
19
34
 
20
35
  it('should match rules in order and return first match', () => {
@@ -24,14 +39,26 @@ describe('InterventionChecker', () => {
24
39
  { policy: 'required' }, // Default rule
25
40
  ];
26
41
 
27
- expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
28
- 'never',
29
- );
30
42
  expect(
31
- InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:' } }),
43
+ InterventionChecker.shouldIntervene({
44
+ config,
45
+ securityBlacklist: [], // Disable blacklist for this test
46
+ toolArgs: { command: 'ls:' },
47
+ }),
48
+ ).toBe('never');
49
+ expect(
50
+ InterventionChecker.shouldIntervene({
51
+ config,
52
+ securityBlacklist: [], // Disable blacklist for this test
53
+ toolArgs: { command: 'git commit:' },
54
+ }),
32
55
  ).toBe('required');
33
56
  expect(
34
- InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm -rf /' } }),
57
+ InterventionChecker.shouldIntervene({
58
+ config,
59
+ securityBlacklist: [], // Disable blacklist for this test
60
+ toolArgs: { command: 'rm -rf /' },
61
+ }),
35
62
  ).toBe('required');
36
63
  });
37
64
 
@@ -40,6 +67,7 @@ describe('InterventionChecker', () => {
40
67
 
41
68
  const result = InterventionChecker.shouldIntervene({
42
69
  config,
70
+ securityBlacklist: [], // Disable blacklist for this test
43
71
  toolArgs: { command: 'rm -rf /' },
44
72
  });
45
73
  expect(result).toBe('required');
@@ -61,6 +89,7 @@ describe('InterventionChecker', () => {
61
89
  expect(
62
90
  InterventionChecker.shouldIntervene({
63
91
  config,
92
+ securityBlacklist: [], // Disable blacklist for this test
64
93
  toolArgs: {
65
94
  command: 'git add:.',
66
95
  path: '/Users/project/file.ts',
@@ -72,6 +101,7 @@ describe('InterventionChecker', () => {
72
101
  expect(
73
102
  InterventionChecker.shouldIntervene({
74
103
  config,
104
+ securityBlacklist: [], // Disable blacklist for this test
75
105
  toolArgs: {
76
106
  command: 'git add:.',
77
107
  path: '/tmp/file.ts',
@@ -88,6 +118,7 @@ describe('InterventionChecker', () => {
88
118
 
89
119
  const result = InterventionChecker.shouldIntervene({
90
120
  config,
121
+ securityBlacklist: [], // Disable blacklist for this test
91
122
  toolArgs: { command: 'anything' },
92
123
  });
93
124
  expect(result).toBe('required');
@@ -218,6 +249,393 @@ describe('InterventionChecker', () => {
218
249
  });
219
250
  });
220
251
 
252
+ describe('checkSecurityBlacklist', () => {
253
+ it('should return not blocked when blacklist is empty', () => {
254
+ const result = InterventionChecker.checkSecurityBlacklist([], { command: 'rm -rf /' });
255
+ expect(result.blocked).toBe(false);
256
+ expect(result.reason).toBeUndefined();
257
+ });
258
+
259
+ describe('with DEFAULT_SECURITY_BLACKLIST', () => {
260
+ it('should block dangerous rm -rf ~/ command', () => {
261
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
262
+ command: 'rm -rf ~/',
263
+ });
264
+ expect(result.blocked).toBe(true);
265
+ expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
266
+ });
267
+
268
+ it('should block rm -rf on macOS home directory', () => {
269
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
270
+ command: 'rm -rf /Users/alice',
271
+ });
272
+ expect(result.blocked).toBe(true);
273
+ expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
274
+ });
275
+
276
+ it('should block rm -rf on Linux home directory', () => {
277
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
278
+ command: 'rm -rf /home/alice',
279
+ });
280
+ expect(result.blocked).toBe(true);
281
+ expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
282
+ });
283
+
284
+ it('should block rm -rf with $HOME variable', () => {
285
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
286
+ command: 'rm -rf $HOME',
287
+ });
288
+ expect(result.blocked).toBe(true);
289
+ expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
290
+ });
291
+
292
+ it('should block rm -rf / command', () => {
293
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
294
+ command: 'rm -rf /',
295
+ });
296
+ expect(result.blocked).toBe(true);
297
+ expect(result.reason).toBe('Recursive deletion of root directory will destroy the system');
298
+ });
299
+
300
+ it('should allow safe rm commands', () => {
301
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
302
+ command: 'rm -rf /tmp/test-folder',
303
+ });
304
+ expect(result.blocked).toBe(false);
305
+ });
306
+
307
+ it('should block fork bomb', () => {
308
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
309
+ command: ':(){ :|:& };:',
310
+ });
311
+ expect(result.blocked).toBe(true);
312
+ expect(result.reason).toBe('Fork bomb can crash the system');
313
+ });
314
+
315
+ it('should block dangerous dd commands to disk devices', () => {
316
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
317
+ command: 'dd if=/dev/zero of=/dev/sda',
318
+ });
319
+ expect(result.blocked).toBe(true);
320
+ expect(result.reason).toBe('Writing random data to disk devices can destroy data');
321
+ });
322
+
323
+ it('should block reading .env files via command', () => {
324
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
325
+ command: 'cat .env',
326
+ });
327
+ expect(result.blocked).toBe(true);
328
+ expect(result.reason).toBe(
329
+ 'Reading .env files may leak sensitive credentials and API keys',
330
+ );
331
+ });
332
+
333
+ it('should block reading .env files via path', () => {
334
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
335
+ path: '/project/.env.local',
336
+ });
337
+ expect(result.blocked).toBe(true);
338
+ expect(result.reason).toBe(
339
+ 'Reading .env files may leak sensitive credentials and API keys',
340
+ );
341
+ });
342
+
343
+ it('should block reading SSH private keys via command', () => {
344
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
345
+ command: 'cat ~/.ssh/id_rsa',
346
+ });
347
+ expect(result.blocked).toBe(true);
348
+ expect(result.reason).toBe('Reading SSH private keys can compromise system security');
349
+ });
350
+
351
+ it('should block reading SSH private keys via path', () => {
352
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
353
+ path: '/home/user/.ssh/id_ed25519',
354
+ });
355
+ expect(result.blocked).toBe(true);
356
+ expect(result.reason).toBe('Reading SSH private keys can compromise system security');
357
+ });
358
+
359
+ it('should allow reading SSH public keys', () => {
360
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
361
+ command: 'cat ~/.ssh/id_rsa.pub',
362
+ });
363
+ expect(result.blocked).toBe(false);
364
+ });
365
+
366
+ it('should block reading AWS credentials via command', () => {
367
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
368
+ command: 'cat ~/.aws/credentials',
369
+ });
370
+ expect(result.blocked).toBe(true);
371
+ expect(result.reason).toBe('Accessing AWS credentials can leak cloud access keys');
372
+ });
373
+
374
+ it('should block reading AWS credentials via path', () => {
375
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
376
+ path: '/home/user/.aws/credentials',
377
+ });
378
+ expect(result.blocked).toBe(true);
379
+ expect(result.reason).toBe('Accessing AWS credentials can leak cloud access keys');
380
+ });
381
+
382
+ it('should block reading Docker config', () => {
383
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
384
+ command: 'less ~/.docker/config.json',
385
+ });
386
+ expect(result.blocked).toBe(true);
387
+ expect(result.reason).toBe('Reading Docker config may expose registry credentials');
388
+ });
389
+
390
+ it('should block reading Kubernetes config', () => {
391
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
392
+ path: '/home/user/.kube/config',
393
+ });
394
+ expect(result.blocked).toBe(true);
395
+ expect(result.reason).toBe('Reading Kubernetes config may expose cluster credentials');
396
+ });
397
+
398
+ it('should block reading Git credentials', () => {
399
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
400
+ command: 'cat ~/.git-credentials',
401
+ });
402
+ expect(result.blocked).toBe(true);
403
+ expect(result.reason).toBe('Reading Git credentials file may leak access tokens');
404
+ });
405
+
406
+ it('should block reading npm token file', () => {
407
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
408
+ path: '/home/user/.npmrc',
409
+ });
410
+ expect(result.blocked).toBe(true);
411
+ expect(result.reason).toBe(
412
+ 'Reading npm token file may expose package registry credentials',
413
+ );
414
+ });
415
+
416
+ it('should block reading shell history files', () => {
417
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
418
+ command: 'cat ~/.bash_history',
419
+ });
420
+ expect(result.blocked).toBe(true);
421
+ expect(result.reason).toBe(
422
+ 'Reading history files may expose sensitive commands and credentials',
423
+ );
424
+ });
425
+
426
+ it('should block reading GCP credentials', () => {
427
+ const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
428
+ path: '/home/user/.config/gcloud/application_default_credentials.json',
429
+ });
430
+ expect(result.blocked).toBe(true);
431
+ expect(result.reason).toBe('Reading GCP credentials may leak cloud service account keys');
432
+ });
433
+ });
434
+
435
+ describe('with custom blacklist', () => {
436
+ it('should work with multiple parameter matching', () => {
437
+ const blacklist: SecurityBlacklistConfig = [
438
+ {
439
+ description: 'Dangerous operation on system files',
440
+ match: {
441
+ command: { pattern: 'rm.*', type: 'regex' },
442
+ path: '/etc/*',
443
+ },
444
+ },
445
+ ];
446
+
447
+ // Both match - should block
448
+ expect(
449
+ InterventionChecker.checkSecurityBlacklist(blacklist, {
450
+ command: 'rm -rf',
451
+ path: '/etc/passwd',
452
+ }).blocked,
453
+ ).toBe(true);
454
+
455
+ // Only command matches - should not block
456
+ expect(
457
+ InterventionChecker.checkSecurityBlacklist(blacklist, {
458
+ command: 'rm -rf',
459
+ path: '/tmp/file',
460
+ }).blocked,
461
+ ).toBe(false);
462
+
463
+ // Only path matches - should not block
464
+ expect(
465
+ InterventionChecker.checkSecurityBlacklist(blacklist, {
466
+ command: 'cat',
467
+ path: '/etc/passwd',
468
+ }).blocked,
469
+ ).toBe(false);
470
+ });
471
+ });
472
+ });
473
+
474
+ describe('shouldIntervene with security blacklist', () => {
475
+ describe('with default blacklist behavior', () => {
476
+ it('should block dangerous commands even in auto-run mode', () => {
477
+ // Even with config set to 'never', default blacklist should override
478
+ const result = InterventionChecker.shouldIntervene({
479
+ config: 'never',
480
+ // Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
481
+ toolArgs: { command: 'rm -rf ~/' },
482
+ });
483
+
484
+ expect(result).toBe('required');
485
+ });
486
+
487
+ it('should block dangerous commands even with no config', () => {
488
+ // Even with no config (which normally means 'never'), default blacklist should override
489
+ const result = InterventionChecker.shouldIntervene({
490
+ config: undefined,
491
+ // Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
492
+ toolArgs: { command: 'rm -rf /' },
493
+ });
494
+
495
+ expect(result).toBe('required');
496
+ });
497
+
498
+ it('should allow safe commands to follow normal intervention rules', () => {
499
+ // Safe command should follow normal config
500
+ const result = InterventionChecker.shouldIntervene({
501
+ config: 'never',
502
+ // Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
503
+ toolArgs: { command: 'ls -la' },
504
+ });
505
+
506
+ expect(result).toBe('never');
507
+ });
508
+
509
+ it('should block reading sensitive files', () => {
510
+ // Test with actual default blacklist for sensitive file reading
511
+ const result = InterventionChecker.shouldIntervene({
512
+ config: 'never',
513
+ // Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
514
+ toolArgs: { command: 'cat .env' },
515
+ });
516
+
517
+ expect(result).toBe('required');
518
+ });
519
+ });
520
+
521
+ describe('with custom blacklist replacement', () => {
522
+ it('should use custom blacklist instead of default when provided', () => {
523
+ const customBlacklist: SecurityBlacklistConfig = [
524
+ {
525
+ description: 'Block all npm commands in production',
526
+ match: {
527
+ command: { pattern: 'npm.*', type: 'regex' },
528
+ },
529
+ },
530
+ ];
531
+
532
+ // Custom blacklist blocks npm but not rm
533
+ expect(
534
+ InterventionChecker.shouldIntervene({
535
+ config: 'never',
536
+ securityBlacklist: customBlacklist,
537
+ toolArgs: { command: 'npm install' },
538
+ }),
539
+ ).toBe('required');
540
+
541
+ // rm is not in custom blacklist, should follow config
542
+ expect(
543
+ InterventionChecker.shouldIntervene({
544
+ config: 'never',
545
+ securityBlacklist: customBlacklist,
546
+ toolArgs: { command: 'rm -rf ~/' },
547
+ }),
548
+ ).toBe('never');
549
+ });
550
+
551
+ it('should support extending default blacklist with custom rules', () => {
552
+ const extendedBlacklist: SecurityBlacklistConfig = [
553
+ ...DEFAULT_SECURITY_BLACKLIST,
554
+ {
555
+ description: 'Block access to production database',
556
+ match: {
557
+ command: { pattern: '.*psql.*production.*', type: 'regex' },
558
+ },
559
+ },
560
+ ];
561
+
562
+ // Default rule still works
563
+ expect(
564
+ InterventionChecker.shouldIntervene({
565
+ config: 'never',
566
+ securityBlacklist: extendedBlacklist,
567
+ toolArgs: { command: 'rm -rf ~/' },
568
+ }),
569
+ ).toBe('required');
570
+
571
+ // Custom rule works
572
+ expect(
573
+ InterventionChecker.shouldIntervene({
574
+ config: 'never',
575
+ securityBlacklist: extendedBlacklist,
576
+ toolArgs: { command: 'psql -h production.db' },
577
+ }),
578
+ ).toBe('required');
579
+
580
+ // Safe commands pass
581
+ expect(
582
+ InterventionChecker.shouldIntervene({
583
+ config: 'never',
584
+ securityBlacklist: extendedBlacklist,
585
+ toolArgs: { command: 'psql -h localhost' },
586
+ }),
587
+ ).toBe('never');
588
+ });
589
+
590
+ it('should allow disabling security blacklist by passing empty array', () => {
591
+ // Dangerous command should not be blocked when blacklist is empty
592
+ const result = InterventionChecker.shouldIntervene({
593
+ config: 'never',
594
+ securityBlacklist: [], // Explicitly disable blacklist
595
+ toolArgs: { command: 'rm -rf ~/' },
596
+ });
597
+
598
+ expect(result).toBe('never');
599
+ });
600
+
601
+ it('should support project-specific blacklist rules', () => {
602
+ const projectBlacklist: SecurityBlacklistConfig = [
603
+ {
604
+ description: 'Block modifying package.json in CI',
605
+ match: {
606
+ path: { pattern: '.*/package\\.json$', type: 'regex' },
607
+ command: { pattern: '(vim|nano|vi|emacs|code|sed).*', type: 'regex' },
608
+ },
609
+ },
610
+ ];
611
+
612
+ // Should block editing package.json
613
+ expect(
614
+ InterventionChecker.shouldIntervene({
615
+ config: 'never',
616
+ securityBlacklist: projectBlacklist,
617
+ toolArgs: {
618
+ command: 'vim package.json',
619
+ path: '/project/package.json',
620
+ },
621
+ }),
622
+ ).toBe('required');
623
+
624
+ // Should allow reading package.json
625
+ expect(
626
+ InterventionChecker.shouldIntervene({
627
+ config: 'never',
628
+ securityBlacklist: projectBlacklist,
629
+ toolArgs: {
630
+ command: 'cat package.json',
631
+ path: '/project/package.json',
632
+ },
633
+ }),
634
+ ).toBe('never');
635
+ });
636
+ });
637
+ });
638
+
221
639
  describe('Integration scenarios', () => {
222
640
  it('should handle Bash tool scenario', () => {
223
641
  const config: HumanInterventionConfig = [
@@ -229,24 +647,44 @@ describe('InterventionChecker', () => {
229
647
  ];
230
648
 
231
649
  // Safe commands - never
232
- expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
233
- 'never',
234
- );
650
+ expect(
651
+ InterventionChecker.shouldIntervene({
652
+ config,
653
+ securityBlacklist: [], // Disable blacklist for this test
654
+ toolArgs: { command: 'ls:' },
655
+ }),
656
+ ).toBe('never');
235
657
 
236
658
  // Git commands - require
237
659
  expect(
238
- InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git add:.' } }),
660
+ InterventionChecker.shouldIntervene({
661
+ config,
662
+ securityBlacklist: [], // Disable blacklist for this test
663
+ toolArgs: { command: 'git add:.' },
664
+ }),
239
665
  ).toBe('required');
240
666
  expect(
241
- InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:-m' } }),
667
+ InterventionChecker.shouldIntervene({
668
+ config,
669
+ securityBlacklist: [], // Disable blacklist for this test
670
+ toolArgs: { command: 'git commit:-m' },
671
+ }),
242
672
  ).toBe('required');
243
673
 
244
674
  // Dangerous commands - require
245
- expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm:-rf' } })).toBe(
246
- 'required',
247
- );
248
675
  expect(
249
- InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'npm install' } }),
676
+ InterventionChecker.shouldIntervene({
677
+ config,
678
+ securityBlacklist: [], // Disable blacklist for this test
679
+ toolArgs: { command: 'rm:-rf' },
680
+ }),
681
+ ).toBe('required');
682
+ expect(
683
+ InterventionChecker.shouldIntervene({
684
+ config,
685
+ securityBlacklist: [], // Disable blacklist for this test
686
+ toolArgs: { command: 'npm install' },
687
+ }),
250
688
  ).toBe('required');
251
689
  });
252
690
 
@@ -260,13 +698,18 @@ describe('InterventionChecker', () => {
260
698
  expect(
261
699
  InterventionChecker.shouldIntervene({
262
700
  config,
701
+ securityBlacklist: [], // Disable blacklist for this test
263
702
  toolArgs: { path: '/Users/project/file.ts' },
264
703
  }),
265
704
  ).toBe('never');
266
705
 
267
706
  // Outside project - require
268
707
  expect(
269
- InterventionChecker.shouldIntervene({ config, toolArgs: { path: '/tmp/file.ts' } }),
708
+ InterventionChecker.shouldIntervene({
709
+ config,
710
+ securityBlacklist: [], // Disable blacklist for this test
711
+ toolArgs: { path: '/tmp/file.ts' },
712
+ }),
270
713
  ).toBe('required');
271
714
  });
272
715
 
@@ -274,8 +717,35 @@ describe('InterventionChecker', () => {
274
717
  const config: HumanInterventionConfig = 'required';
275
718
 
276
719
  expect(
277
- InterventionChecker.shouldIntervene({ config, toolArgs: { url: 'https://example.com' } }),
720
+ InterventionChecker.shouldIntervene({
721
+ config,
722
+ securityBlacklist: [], // Disable blacklist for this test
723
+ toolArgs: { url: 'https://example.com' },
724
+ }),
278
725
  ).toBe('required');
279
726
  });
727
+
728
+ it('should handle security blacklist overriding user config', () => {
729
+ const config: HumanInterventionConfig = 'never';
730
+ const blacklist: SecurityBlacklistConfig = DEFAULT_SECURITY_BLACKLIST;
731
+
732
+ // Dangerous command blocked even with 'never' config
733
+ expect(
734
+ InterventionChecker.shouldIntervene({
735
+ config,
736
+ securityBlacklist: blacklist,
737
+ toolArgs: { command: 'rm -rf /' },
738
+ }),
739
+ ).toBe('required');
740
+
741
+ // Safe command follows config
742
+ expect(
743
+ InterventionChecker.shouldIntervene({
744
+ config,
745
+ securityBlacklist: blacklist,
746
+ toolArgs: { command: 'ls -la' },
747
+ }),
748
+ ).toBe('never');
749
+ });
280
750
  });
281
751
  });
@@ -0,0 +1,335 @@
1
+ import type { SecurityBlacklistConfig } from '@lobechat/types';
2
+
3
+ /**
4
+ * Default Security Blacklist
5
+ * These rules will ALWAYS block execution and require human intervention,
6
+ * regardless of user settings (even in auto-run mode)
7
+ *
8
+ * This is the last line of defense against dangerous operations
9
+ */
10
+ export const DEFAULT_SECURITY_BLACKLIST: SecurityBlacklistConfig = [
11
+ // ==================== File System Dangers ====================
12
+ {
13
+ description: 'Recursive deletion of home directory is extremely dangerous',
14
+ match: {
15
+ command: {
16
+ pattern: 'rm.*-r.*(~|\\$HOME|/Users/[^/]+|/home/[^/]+)/?\\s*$',
17
+ type: 'regex',
18
+ },
19
+ },
20
+ },
21
+ {
22
+ description: 'Recursive deletion of root directory will destroy the system',
23
+ match: {
24
+ command: {
25
+ pattern: 'rm.*-r.*/\\s*$',
26
+ type: 'regex',
27
+ },
28
+ },
29
+ },
30
+ {
31
+ description: 'Force recursive deletion without specific target is too dangerous',
32
+ match: {
33
+ command: {
34
+ pattern: 'rm\\s+-rf\\s+[~./]\\s*$',
35
+ type: 'regex',
36
+ },
37
+ },
38
+ },
39
+
40
+ // ==================== System Configuration Dangers ====================
41
+ {
42
+ description: 'Modifying /etc/passwd could lock you out of the system',
43
+ match: {
44
+ command: {
45
+ pattern: '.*(/etc/passwd|/etc/shadow).*',
46
+ type: 'regex',
47
+ },
48
+ },
49
+ },
50
+ {
51
+ description: 'Modifying sudoers file without proper validation is dangerous',
52
+ match: {
53
+ command: {
54
+ pattern: '.*/etc/sudoers.*',
55
+ type: 'regex',
56
+ },
57
+ },
58
+ },
59
+
60
+ // ==================== Dangerous Commands ====================
61
+ {
62
+ description: 'Fork bomb can crash the system',
63
+ match: {
64
+ command: {
65
+ pattern: '.*:\\(\\).*\\{.*\\|.*&.*\\};.*:.*',
66
+ type: 'regex',
67
+ },
68
+ },
69
+ },
70
+ {
71
+ description: 'Writing random data to disk devices can destroy data',
72
+ match: {
73
+ command: {
74
+ pattern: 'dd.*of=/dev/(sd|hd|nvme).*',
75
+ type: 'regex',
76
+ },
77
+ },
78
+ },
79
+ {
80
+ description: 'Formatting system partitions will destroy data',
81
+ match: {
82
+ command: {
83
+ pattern: '(mkfs|fdisk|parted).*(/dev/(sd|hd|nvme)|/)',
84
+ type: 'regex',
85
+ },
86
+ },
87
+ },
88
+
89
+ // ==================== Network & Remote Access Dangers ====================
90
+ {
91
+ description: 'Disabling firewall exposes system to attacks',
92
+ match: {
93
+ command: {
94
+ pattern: '(ufw\\s+disable|iptables\\s+-F|systemctl\\s+stop\\s+firewalld)',
95
+ type: 'regex',
96
+ },
97
+ },
98
+ },
99
+ {
100
+ description: 'Changing SSH configuration could lock you out',
101
+ match: {
102
+ command: {
103
+ pattern: '.*(/etc/ssh/sshd_config).*',
104
+ type: 'regex',
105
+ },
106
+ },
107
+ },
108
+
109
+ // ==================== Package Manager Dangers ====================
110
+ {
111
+ description: 'Removing essential system packages can break the system',
112
+ match: {
113
+ command: {
114
+ pattern: '(apt|yum|dnf|pacman)\\s+(remove|purge|erase).*(systemd|kernel|glibc|bash|sudo)',
115
+ type: 'regex',
116
+ },
117
+ },
118
+ },
119
+
120
+ // ==================== Kernel & System Core Dangers ====================
121
+ {
122
+ description: 'Modifying kernel parameters without understanding can crash the system',
123
+ match: {
124
+ command: {
125
+ pattern: 'echo.*>/proc/sys/.*',
126
+ type: 'regex',
127
+ },
128
+ },
129
+ },
130
+ {
131
+ description: 'Direct memory access is extremely dangerous',
132
+ match: {
133
+ command: {
134
+ pattern: '.*(/dev/(mem|kmem|port)).*',
135
+ type: 'regex',
136
+ },
137
+ },
138
+ },
139
+
140
+ // ==================== Privilege Escalation Dangers ====================
141
+ {
142
+ description: 'Changing file ownership of system directories is dangerous',
143
+ match: {
144
+ command: {
145
+ pattern: 'chown.*-R.*(/(etc|bin|sbin|usr|var|sys|proc)|~).*',
146
+ type: 'regex',
147
+ },
148
+ },
149
+ },
150
+ {
151
+ description: 'Setting SUID on shells or interpreters is a security risk',
152
+ match: {
153
+ command: {
154
+ pattern: 'chmod.*(4755|u\\+s).*(sh|bash|python|perl|ruby|node)',
155
+ type: 'regex',
156
+ },
157
+ },
158
+ },
159
+
160
+ // ==================== Sensitive Information Leakage ====================
161
+ {
162
+ description: 'Reading .env files may leak sensitive credentials and API keys',
163
+ match: {
164
+ command: {
165
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*\\.env.*',
166
+ type: 'regex',
167
+ },
168
+ },
169
+ },
170
+ {
171
+ description: 'Reading .env files may leak sensitive credentials and API keys',
172
+ match: {
173
+ path: {
174
+ pattern: '.*\\.env.*',
175
+ type: 'regex',
176
+ },
177
+ },
178
+ },
179
+ {
180
+ description: 'Reading SSH private keys can compromise system security',
181
+ match: {
182
+ command: {
183
+ pattern:
184
+ '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*(id_rsa|id_ed25519|id_ecdsa)(?!\\.pub).*',
185
+ type: 'regex',
186
+ },
187
+ },
188
+ },
189
+ {
190
+ description: 'Reading SSH private keys can compromise system security',
191
+ match: {
192
+ path: {
193
+ pattern: '.*/\\.ssh/(id_rsa|id_ed25519|id_ecdsa)$',
194
+ type: 'regex',
195
+ },
196
+ },
197
+ },
198
+ {
199
+ description: 'Accessing AWS credentials can leak cloud access keys',
200
+ match: {
201
+ command: {
202
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.aws/credentials.*',
203
+ type: 'regex',
204
+ },
205
+ },
206
+ },
207
+ {
208
+ description: 'Accessing AWS credentials can leak cloud access keys',
209
+ match: {
210
+ path: {
211
+ pattern: '.*/\\.aws/credentials.*',
212
+ type: 'regex',
213
+ },
214
+ },
215
+ },
216
+ {
217
+ description: 'Reading Docker config may expose registry credentials',
218
+ match: {
219
+ command: {
220
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.docker/config\\.json.*',
221
+ type: 'regex',
222
+ },
223
+ },
224
+ },
225
+ {
226
+ description: 'Reading Docker config may expose registry credentials',
227
+ match: {
228
+ path: {
229
+ pattern: '.*/\\.docker/config\\.json$',
230
+ type: 'regex',
231
+ },
232
+ },
233
+ },
234
+ {
235
+ description: 'Reading Kubernetes config may expose cluster credentials',
236
+ match: {
237
+ command: {
238
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.kube/config.*',
239
+ type: 'regex',
240
+ },
241
+ },
242
+ },
243
+ {
244
+ description: 'Reading Kubernetes config may expose cluster credentials',
245
+ match: {
246
+ path: {
247
+ pattern: '.*/\\.kube/config$',
248
+ type: 'regex',
249
+ },
250
+ },
251
+ },
252
+ {
253
+ description: 'Reading Git credentials file may leak access tokens',
254
+ match: {
255
+ command: {
256
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.git-credentials.*',
257
+ type: 'regex',
258
+ },
259
+ },
260
+ },
261
+ {
262
+ description: 'Reading Git credentials file may leak access tokens',
263
+ match: {
264
+ path: {
265
+ pattern: '.*/\\.git-credentials$',
266
+ type: 'regex',
267
+ },
268
+ },
269
+ },
270
+ {
271
+ description: 'Reading npm token file may expose package registry credentials',
272
+ match: {
273
+ command: {
274
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.npmrc.*',
275
+ type: 'regex',
276
+ },
277
+ },
278
+ },
279
+ {
280
+ description: 'Reading npm token file may expose package registry credentials',
281
+ match: {
282
+ path: {
283
+ pattern: '.*/\\.npmrc$',
284
+ type: 'regex',
285
+ },
286
+ },
287
+ },
288
+ {
289
+ description: 'Reading history files may expose sensitive commands and credentials',
290
+ match: {
291
+ command: {
292
+ pattern:
293
+ '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.(bash_history|zsh_history|history).*',
294
+ type: 'regex',
295
+ },
296
+ },
297
+ },
298
+ {
299
+ description: 'Reading history files may expose sensitive commands and credentials',
300
+ match: {
301
+ path: {
302
+ pattern: '.*/\\.(bash_history|zsh_history|history)$',
303
+ type: 'regex',
304
+ },
305
+ },
306
+ },
307
+ {
308
+ description: 'Accessing browser credential storage may leak passwords',
309
+ match: {
310
+ command: {
311
+ pattern:
312
+ '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*(Cookies|Login Data|Web Data).*',
313
+ type: 'regex',
314
+ },
315
+ },
316
+ },
317
+ {
318
+ description: 'Reading GCP credentials may leak cloud service account keys',
319
+ match: {
320
+ command: {
321
+ pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.config/gcloud/.*\\.json.*',
322
+ type: 'regex',
323
+ },
324
+ },
325
+ },
326
+ {
327
+ description: 'Reading GCP credentials may leak cloud service account keys',
328
+ match: {
329
+ path: {
330
+ pattern: '.*/\\.config/gcloud/.*\\.json$',
331
+ type: 'regex',
332
+ },
333
+ },
334
+ },
335
+ ];
@@ -1,3 +1,4 @@
1
+ export * from './defaultSecurityBlacklist';
1
2
  export * from './InterventionChecker';
2
3
  export * from './runtime';
3
4
  export * from './UsageCounter';
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
- import { ChatToolPayload, UserInterventionConfig } from '@lobechat/types';
2
+ import { ChatToolPayload, SecurityBlacklistConfig, UserInterventionConfig } from '@lobechat/types';
3
3
 
4
4
  import type { Cost, CostLimit, Usage } from './usage';
5
5
 
@@ -23,6 +23,15 @@ export interface AgentState {
23
23
  * Controls how tools requiring approval are handled
24
24
  */
25
25
  userInterventionConfig?: UserInterventionConfig;
26
+
27
+ /**
28
+ * Security blacklist configuration
29
+ * These rules will ALWAYS block execution and require human intervention,
30
+ * regardless of user settings (even in auto-run mode).
31
+ * If not provided, DEFAULT_SECURITY_BLACKLIST will be used.
32
+ */
33
+ securityBlacklist?: SecurityBlacklistConfig;
34
+
26
35
  // --- Execution Tracking ---
27
36
  /**
28
37
  * Number of execution steps in this session.
@@ -146,6 +146,36 @@ export const UserInterventionConfigSchema = z.object({
146
146
  approvalMode: z.enum(['auto-run', 'allow-list', 'manual']),
147
147
  });
148
148
 
149
+ /**
150
+ * Security Blacklist Rule
151
+ * Used to forcefully block dangerous operations regardless of user settings
152
+ */
153
+ export interface SecurityBlacklistRule {
154
+ /**
155
+ * Description of why this rule exists (for error messages)
156
+ */
157
+ description: string;
158
+
159
+ /**
160
+ * Parameter filter - matches against tool call arguments
161
+ * Same format as HumanInterventionRule.match
162
+ */
163
+ match: Record<string, ArgumentMatcher>;
164
+ }
165
+
166
+ export const SecurityBlacklistRuleSchema = z.object({
167
+ description: z.string(),
168
+ match: z.record(z.string(), ArgumentMatcherSchema),
169
+ });
170
+
171
+ /**
172
+ * Security Blacklist Configuration
173
+ * A list of rules that will always block execution and require intervention
174
+ */
175
+ export type SecurityBlacklistConfig = SecurityBlacklistRule[];
176
+
177
+ export const SecurityBlacklistConfigSchema = z.array(SecurityBlacklistRuleSchema);
178
+
149
179
  /**
150
180
  * Parameters for shouldIntervene method
151
181
  */
@@ -161,6 +191,13 @@ export interface ShouldInterveneParams {
161
191
  */
162
192
  confirmedHistory?: string[];
163
193
 
194
+ /**
195
+ * Security blacklist rules that will be checked first
196
+ * These rules override all other settings including auto-run mode
197
+ * @default []
198
+ */
199
+ securityBlacklist?: SecurityBlacklistConfig;
200
+
164
201
  /**
165
202
  * Tool call arguments to check against rules
166
203
  * @default {}
@@ -177,6 +214,7 @@ export interface ShouldInterveneParams {
177
214
  export const ShouldInterveneParamsSchema = z.object({
178
215
  config: HumanInterventionConfigSchema.optional(),
179
216
  confirmedHistory: z.array(z.string()).optional(),
217
+ securityBlacklist: SecurityBlacklistConfigSchema.optional(),
180
218
  toolArgs: z.record(z.string(), z.any()).optional(),
181
219
  toolKey: z.string().optional(),
182
220
  });
@@ -249,4 +249,29 @@ describe('createRemarkSelfClosingTagPlugin', () => {
249
249
 
250
250
  expect(tree).toMatchSnapshot();
251
251
  });
252
+
253
+ it('should handle tags wrapped in backticks (code)', () => {
254
+ const markdown = `Use this file: \`<${tagName} name="config.json" path="/app/config.json" />\` in your code.`;
255
+ const tree = processMarkdown(markdown, tagName);
256
+
257
+ expect(tree.children).toHaveLength(1);
258
+ expect(tree.children[0].type).toBe('paragraph');
259
+
260
+ const paragraphChildren = tree.children[0].children;
261
+ expect(paragraphChildren).toHaveLength(3);
262
+
263
+ expect(paragraphChildren[0].type).toBe('text');
264
+ expect(paragraphChildren[0].value).toBe('Use this file: ');
265
+
266
+ // The tag should be parsed even inside backticks
267
+ const tagNode = paragraphChildren[1];
268
+ expect(tagNode.type).toBe(tagName);
269
+ expect(tagNode.data?.hProperties).toEqual({
270
+ name: 'config.json',
271
+ path: '/app/config.json',
272
+ });
273
+
274
+ expect(paragraphChildren[2].type).toBe('text');
275
+ expect(paragraphChildren[2].value).toBe(' in your code.');
276
+ });
252
277
  });
@@ -130,5 +130,33 @@ export const createRemarkSelfClosingTagPlugin =
130
130
  return [SKIP, index + newChildren.length]; // Skip new nodes
131
131
  }
132
132
  });
133
+
134
+ // 3. Visit inlineCode nodes (backtick-wrapped tags like `<localFile ... />`)
135
+ // @ts-ignore
136
+ visit(tree, 'inlineCode', (node: any, index: number, parent) => {
137
+ log('>>> Visiting inlineCode node: "%s"', node.value);
138
+
139
+ if (!parent || typeof index !== 'number' || !node.value?.includes(`<${tagName}`)) {
140
+ return;
141
+ }
142
+
143
+ const match = node.value.match(exactTagRegex);
144
+ if (match) {
145
+ const [, attributesString] = match;
146
+ const properties = attributesString ? parseAttributes(attributesString.trim()) : {};
147
+
148
+ const newNode = {
149
+ data: {
150
+ hName: tagName,
151
+ hProperties: properties,
152
+ },
153
+ type: tagName,
154
+ };
155
+
156
+ log('Replacing inlineCode node at index %d with %s node: %o', index, tagName, newNode);
157
+ parent.children.splice(index, 1, newNode);
158
+ return [SKIP, index + 1];
159
+ }
160
+ });
133
161
  };
134
162
  };
@@ -3,6 +3,7 @@ import {
3
3
  AgentInstruction,
4
4
  AgentRuntimeContext,
5
5
  AgentState,
6
+ DEFAULT_SECURITY_BLACKLIST,
6
7
  GeneralAgentCallLLMInstructionPayload,
7
8
  GeneralAgentCallLLMResultPayload,
8
9
  GeneralAgentCallToolResultPayload,
@@ -65,6 +66,9 @@ export class GeneralChatAgent implements Agent {
65
66
  const toolsNeedingIntervention: ChatToolPayload[] = [];
66
67
  const toolsToExecute: ChatToolPayload[] = [];
67
68
 
69
+ // Get security blacklist (use default if not provided)
70
+ const securityBlacklist = state.securityBlacklist ?? DEFAULT_SECURITY_BLACKLIST;
71
+
68
72
  // Get user config (default to 'manual' mode)
69
73
  const userConfig = state.userInterventionConfig || { approvalMode: 'manual' };
70
74
  const { approvalMode, allowList = [] } = userConfig;
@@ -73,6 +77,23 @@ export class GeneralChatAgent implements Agent {
73
77
  const { identifier, apiName } = toolCalling;
74
78
  const toolKey = `${identifier}/${apiName}`;
75
79
 
80
+ // Parse arguments for intervention checking
81
+ let toolArgs: Record<string, any> = {};
82
+ try {
83
+ toolArgs = JSON.parse(toolCalling.arguments || '{}');
84
+ } catch {
85
+ // Invalid JSON, treat as empty args
86
+ }
87
+
88
+ // Priority 0: CRITICAL - Check security blacklist FIRST
89
+ // This overrides ALL other settings, including auto-run mode
90
+ const securityCheck = InterventionChecker.checkSecurityBlacklist(securityBlacklist, toolArgs);
91
+ if (securityCheck.blocked) {
92
+ // Security blacklist always requires intervention
93
+ toolsNeedingIntervention.push(toolCalling);
94
+ continue;
95
+ }
96
+
76
97
  // Priority 1: User config is 'auto-run', all tools execute directly
77
98
  if (approvalMode === 'auto-run') {
78
99
  toolsToExecute.push(toolCalling);
@@ -92,16 +113,9 @@ export class GeneralChatAgent implements Agent {
92
113
  // Priority 3: User config is 'manual' (default), use tool's own config
93
114
  const config = this.getToolInterventionConfig(toolCalling, state);
94
115
 
95
- // Parse arguments for intervention checking
96
- let toolArgs: Record<string, any> = {};
97
- try {
98
- toolArgs = JSON.parse(toolCalling.arguments || '{}');
99
- } catch {
100
- // Invalid JSON, treat as empty args
101
- }
102
-
103
116
  const policy = InterventionChecker.shouldIntervene({
104
117
  config,
118
+ securityBlacklist,
105
119
  toolArgs,
106
120
  });
107
121