@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.
- package/.github/workflows/issue-auto-comments.yml +0 -19
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/InterventionChecker.ts +85 -0
- package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +492 -22
- package/packages/agent-runtime/src/core/defaultSecurityBlacklist.ts +335 -0
- package/packages/agent-runtime/src/core/index.ts +1 -0
- package/packages/agent-runtime/src/types/state.ts +10 -1
- package/packages/types/src/tool/intervention.ts +38 -0
- package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +25 -0
- package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +28 -0
- package/src/store/chat/agents/GeneralChatAgent.ts +22 -8
|
@@ -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
|
+
[](#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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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({
|
|
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(
|
|
15
|
-
|
|
16
|
-
|
|
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({
|
|
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({
|
|
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(
|
|
233
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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,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
|
});
|
package/src/features/Conversation/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts
CHANGED
|
@@ -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
|
|