mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-30 13:59:22 +07:00
✨ feat: add Security Blacklist for agent runtime (#10325)
This commit is contained in:
19
.github/workflows/issue-auto-comments.yml
vendored
19
.github/workflows/issue-auto-comments.yml
vendored
@@ -20,15 +20,6 @@ jobs:
|
||||
pull-requests: write # for actions-cool/issues-helper to update PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto Comment on Issues Opened
|
||||
uses: wow-actions/auto-comment@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN}}
|
||||
issuesOpened: |
|
||||
👀 @{{ author }}
|
||||
|
||||
Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible.
|
||||
Please make sure you have given us as much context as possible.
|
||||
- name: Auto Comment on Issues Closed
|
||||
uses: wow-actions/auto-comment@v1
|
||||
with:
|
||||
@@ -37,16 +28,6 @@ jobs:
|
||||
✅ @{{ author }}
|
||||
|
||||
This issue is closed, If you have any questions, you can comment and reply.
|
||||
- name: Auto Comment on Pull Request Opened
|
||||
uses: wow-actions/auto-comment@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN}}
|
||||
pullRequestOpened: |
|
||||
👍 @{{ author }}
|
||||
|
||||
Thank you for raising your pull request and contributing to our Community
|
||||
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
|
||||
If you encounter any problems, please feel free to connect with us.
|
||||
- name: Auto Comment on Pull Request Merged
|
||||
uses: actions-cool/pr-welcome@main
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
@@ -2,14 +2,56 @@ import type {
|
||||
ArgumentMatcher,
|
||||
HumanInterventionPolicy,
|
||||
HumanInterventionRule,
|
||||
SecurityBlacklistRule,
|
||||
ShouldInterveneParams,
|
||||
} from '@lobechat/types';
|
||||
|
||||
import { DEFAULT_SECURITY_BLACKLIST } from './defaultSecurityBlacklist';
|
||||
|
||||
/**
|
||||
* Result of security blacklist check
|
||||
*/
|
||||
export interface SecurityCheckResult {
|
||||
/**
|
||||
* Whether the operation is blocked by security rules
|
||||
*/
|
||||
blocked: boolean;
|
||||
|
||||
/**
|
||||
* Reason for blocking (if blocked)
|
||||
*/
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intervention Checker
|
||||
* Determines whether a tool call requires human intervention
|
||||
*/
|
||||
export class InterventionChecker {
|
||||
/**
|
||||
* Check if tool call is blocked by security blacklist
|
||||
* This check runs BEFORE all other intervention checks
|
||||
*
|
||||
* @param securityBlacklist - Security blacklist rules
|
||||
* @param toolArgs - Tool call arguments
|
||||
* @returns Security check result
|
||||
*/
|
||||
static checkSecurityBlacklist(
|
||||
securityBlacklist: SecurityBlacklistRule[] = [],
|
||||
toolArgs: Record<string, any> = {},
|
||||
): SecurityCheckResult {
|
||||
for (const rule of securityBlacklist) {
|
||||
if (this.matchesSecurityRule(rule, toolArgs)) {
|
||||
return {
|
||||
blocked: true,
|
||||
reason: rule.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { blocked: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool call requires intervention
|
||||
*
|
||||
@@ -19,6 +61,19 @@ export class InterventionChecker {
|
||||
static shouldIntervene(params: ShouldInterveneParams): HumanInterventionPolicy {
|
||||
const { config, toolArgs = {} } = params;
|
||||
|
||||
// Use default blacklist if not provided
|
||||
const securityBlacklist =
|
||||
params.securityBlacklist !== undefined
|
||||
? params.securityBlacklist
|
||||
: DEFAULT_SECURITY_BLACKLIST;
|
||||
|
||||
// CRITICAL: Check security blacklist first - this overrides ALL other settings
|
||||
const securityCheck = this.checkSecurityBlacklist(securityBlacklist, toolArgs);
|
||||
if (securityCheck.blocked) {
|
||||
// Security blacklist always requires intervention, even in auto-run mode
|
||||
return 'required';
|
||||
}
|
||||
|
||||
// No config means never intervene (auto-execute)
|
||||
if (!config) return 'never';
|
||||
|
||||
@@ -38,6 +93,36 @@ export class InterventionChecker {
|
||||
return 'required';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tool arguments match a security blacklist rule
|
||||
*
|
||||
* @param rule - Security rule to check
|
||||
* @param toolArgs - Tool call arguments
|
||||
* @returns true if matches (should be blocked)
|
||||
*/
|
||||
private static matchesSecurityRule(
|
||||
rule: SecurityBlacklistRule,
|
||||
toolArgs: Record<string, any>,
|
||||
): boolean {
|
||||
// Security rules must have match criteria
|
||||
if (!rule.match) return false;
|
||||
|
||||
// All matchers must match (AND logic)
|
||||
for (const [paramName, matcher] of Object.entries(rule.match)) {
|
||||
const paramValue = toolArgs[paramName];
|
||||
|
||||
// Parameter not present in args - rule doesn't match
|
||||
if (paramValue === undefined) return false;
|
||||
|
||||
// Check if value matches
|
||||
if (!this.matchesArgument(matcher, paramValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tool arguments match a rule
|
||||
*
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import type { HumanInterventionConfig } from '@lobechat/types';
|
||||
import type { HumanInterventionConfig, SecurityBlacklistConfig } from '@lobechat/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { InterventionChecker } from '../InterventionChecker';
|
||||
import { DEFAULT_SECURITY_BLACKLIST } from '../defaultSecurityBlacklist';
|
||||
|
||||
describe('InterventionChecker', () => {
|
||||
describe('shouldIntervene', () => {
|
||||
it('should return never when config is undefined', () => {
|
||||
const result = InterventionChecker.shouldIntervene({ config: undefined, toolArgs: {} });
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: undefined,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: {},
|
||||
});
|
||||
expect(result).toBe('never');
|
||||
});
|
||||
|
||||
it('should return the policy when config is a simple string', () => {
|
||||
expect(InterventionChecker.shouldIntervene({ config: 'never', toolArgs: {} })).toBe('never');
|
||||
expect(InterventionChecker.shouldIntervene({ config: 'required', toolArgs: {} })).toBe(
|
||||
'required',
|
||||
);
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: {},
|
||||
}),
|
||||
).toBe('never');
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'required',
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: {},
|
||||
}),
|
||||
).toBe('required');
|
||||
});
|
||||
|
||||
it('should match rules in order and return first match', () => {
|
||||
@@ -24,14 +39,26 @@ describe('InterventionChecker', () => {
|
||||
{ policy: 'required' }, // Default rule
|
||||
];
|
||||
|
||||
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
|
||||
'never',
|
||||
);
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'ls:' },
|
||||
}),
|
||||
).toBe('never');
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'git commit:' },
|
||||
}),
|
||||
).toBe('required');
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm -rf /' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'rm -rf /' },
|
||||
}),
|
||||
).toBe('required');
|
||||
});
|
||||
|
||||
@@ -40,6 +67,7 @@ describe('InterventionChecker', () => {
|
||||
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'rm -rf /' },
|
||||
});
|
||||
expect(result).toBe('required');
|
||||
@@ -61,6 +89,7 @@ describe('InterventionChecker', () => {
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: {
|
||||
command: 'git add:.',
|
||||
path: '/Users/project/file.ts',
|
||||
@@ -72,6 +101,7 @@ describe('InterventionChecker', () => {
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: {
|
||||
command: 'git add:.',
|
||||
path: '/tmp/file.ts',
|
||||
@@ -88,6 +118,7 @@ describe('InterventionChecker', () => {
|
||||
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'anything' },
|
||||
});
|
||||
expect(result).toBe('required');
|
||||
@@ -218,6 +249,393 @@ describe('InterventionChecker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSecurityBlacklist', () => {
|
||||
it('should return not blocked when blacklist is empty', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist([], { command: 'rm -rf /' });
|
||||
expect(result.blocked).toBe(false);
|
||||
expect(result.reason).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('with DEFAULT_SECURITY_BLACKLIST', () => {
|
||||
it('should block dangerous rm -rf ~/ command', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf ~/',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
|
||||
});
|
||||
|
||||
it('should block rm -rf on macOS home directory', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf /Users/alice',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
|
||||
});
|
||||
|
||||
it('should block rm -rf on Linux home directory', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf /home/alice',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
|
||||
});
|
||||
|
||||
it('should block rm -rf with $HOME variable', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf $HOME',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Recursive deletion of home directory is extremely dangerous');
|
||||
});
|
||||
|
||||
it('should block rm -rf / command', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf /',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Recursive deletion of root directory will destroy the system');
|
||||
});
|
||||
|
||||
it('should allow safe rm commands', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'rm -rf /tmp/test-folder',
|
||||
});
|
||||
expect(result.blocked).toBe(false);
|
||||
});
|
||||
|
||||
it('should block fork bomb', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: ':(){ :|:& };:',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Fork bomb can crash the system');
|
||||
});
|
||||
|
||||
it('should block dangerous dd commands to disk devices', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'dd if=/dev/zero of=/dev/sda',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Writing random data to disk devices can destroy data');
|
||||
});
|
||||
|
||||
it('should block reading .env files via command', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat .env',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe(
|
||||
'Reading .env files may leak sensitive credentials and API keys',
|
||||
);
|
||||
});
|
||||
|
||||
it('should block reading .env files via path', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/project/.env.local',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe(
|
||||
'Reading .env files may leak sensitive credentials and API keys',
|
||||
);
|
||||
});
|
||||
|
||||
it('should block reading SSH private keys via command', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat ~/.ssh/id_rsa',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading SSH private keys can compromise system security');
|
||||
});
|
||||
|
||||
it('should block reading SSH private keys via path', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/home/user/.ssh/id_ed25519',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading SSH private keys can compromise system security');
|
||||
});
|
||||
|
||||
it('should allow reading SSH public keys', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat ~/.ssh/id_rsa.pub',
|
||||
});
|
||||
expect(result.blocked).toBe(false);
|
||||
});
|
||||
|
||||
it('should block reading AWS credentials via command', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat ~/.aws/credentials',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Accessing AWS credentials can leak cloud access keys');
|
||||
});
|
||||
|
||||
it('should block reading AWS credentials via path', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/home/user/.aws/credentials',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Accessing AWS credentials can leak cloud access keys');
|
||||
});
|
||||
|
||||
it('should block reading Docker config', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'less ~/.docker/config.json',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading Docker config may expose registry credentials');
|
||||
});
|
||||
|
||||
it('should block reading Kubernetes config', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/home/user/.kube/config',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading Kubernetes config may expose cluster credentials');
|
||||
});
|
||||
|
||||
it('should block reading Git credentials', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat ~/.git-credentials',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading Git credentials file may leak access tokens');
|
||||
});
|
||||
|
||||
it('should block reading npm token file', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/home/user/.npmrc',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe(
|
||||
'Reading npm token file may expose package registry credentials',
|
||||
);
|
||||
});
|
||||
|
||||
it('should block reading shell history files', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
command: 'cat ~/.bash_history',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe(
|
||||
'Reading history files may expose sensitive commands and credentials',
|
||||
);
|
||||
});
|
||||
|
||||
it('should block reading GCP credentials', () => {
|
||||
const result = InterventionChecker.checkSecurityBlacklist(DEFAULT_SECURITY_BLACKLIST, {
|
||||
path: '/home/user/.config/gcloud/application_default_credentials.json',
|
||||
});
|
||||
expect(result.blocked).toBe(true);
|
||||
expect(result.reason).toBe('Reading GCP credentials may leak cloud service account keys');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with custom blacklist', () => {
|
||||
it('should work with multiple parameter matching', () => {
|
||||
const blacklist: SecurityBlacklistConfig = [
|
||||
{
|
||||
description: 'Dangerous operation on system files',
|
||||
match: {
|
||||
command: { pattern: 'rm.*', type: 'regex' },
|
||||
path: '/etc/*',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Both match - should block
|
||||
expect(
|
||||
InterventionChecker.checkSecurityBlacklist(blacklist, {
|
||||
command: 'rm -rf',
|
||||
path: '/etc/passwd',
|
||||
}).blocked,
|
||||
).toBe(true);
|
||||
|
||||
// Only command matches - should not block
|
||||
expect(
|
||||
InterventionChecker.checkSecurityBlacklist(blacklist, {
|
||||
command: 'rm -rf',
|
||||
path: '/tmp/file',
|
||||
}).blocked,
|
||||
).toBe(false);
|
||||
|
||||
// Only path matches - should not block
|
||||
expect(
|
||||
InterventionChecker.checkSecurityBlacklist(blacklist, {
|
||||
command: 'cat',
|
||||
path: '/etc/passwd',
|
||||
}).blocked,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldIntervene with security blacklist', () => {
|
||||
describe('with default blacklist behavior', () => {
|
||||
it('should block dangerous commands even in auto-run mode', () => {
|
||||
// Even with config set to 'never', default blacklist should override
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
// Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
|
||||
toolArgs: { command: 'rm -rf ~/' },
|
||||
});
|
||||
|
||||
expect(result).toBe('required');
|
||||
});
|
||||
|
||||
it('should block dangerous commands even with no config', () => {
|
||||
// Even with no config (which normally means 'never'), default blacklist should override
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: undefined,
|
||||
// Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
|
||||
toolArgs: { command: 'rm -rf /' },
|
||||
});
|
||||
|
||||
expect(result).toBe('required');
|
||||
});
|
||||
|
||||
it('should allow safe commands to follow normal intervention rules', () => {
|
||||
// Safe command should follow normal config
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
// Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
|
||||
toolArgs: { command: 'ls -la' },
|
||||
});
|
||||
|
||||
expect(result).toBe('never');
|
||||
});
|
||||
|
||||
it('should block reading sensitive files', () => {
|
||||
// Test with actual default blacklist for sensitive file reading
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
// Not passing securityBlacklist - should use DEFAULT_SECURITY_BLACKLIST
|
||||
toolArgs: { command: 'cat .env' },
|
||||
});
|
||||
|
||||
expect(result).toBe('required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with custom blacklist replacement', () => {
|
||||
it('should use custom blacklist instead of default when provided', () => {
|
||||
const customBlacklist: SecurityBlacklistConfig = [
|
||||
{
|
||||
description: 'Block all npm commands in production',
|
||||
match: {
|
||||
command: { pattern: 'npm.*', type: 'regex' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Custom blacklist blocks npm but not rm
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: customBlacklist,
|
||||
toolArgs: { command: 'npm install' },
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// rm is not in custom blacklist, should follow config
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: customBlacklist,
|
||||
toolArgs: { command: 'rm -rf ~/' },
|
||||
}),
|
||||
).toBe('never');
|
||||
});
|
||||
|
||||
it('should support extending default blacklist with custom rules', () => {
|
||||
const extendedBlacklist: SecurityBlacklistConfig = [
|
||||
...DEFAULT_SECURITY_BLACKLIST,
|
||||
{
|
||||
description: 'Block access to production database',
|
||||
match: {
|
||||
command: { pattern: '.*psql.*production.*', type: 'regex' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Default rule still works
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: extendedBlacklist,
|
||||
toolArgs: { command: 'rm -rf ~/' },
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// Custom rule works
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: extendedBlacklist,
|
||||
toolArgs: { command: 'psql -h production.db' },
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// Safe commands pass
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: extendedBlacklist,
|
||||
toolArgs: { command: 'psql -h localhost' },
|
||||
}),
|
||||
).toBe('never');
|
||||
});
|
||||
|
||||
it('should allow disabling security blacklist by passing empty array', () => {
|
||||
// Dangerous command should not be blocked when blacklist is empty
|
||||
const result = InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: [], // Explicitly disable blacklist
|
||||
toolArgs: { command: 'rm -rf ~/' },
|
||||
});
|
||||
|
||||
expect(result).toBe('never');
|
||||
});
|
||||
|
||||
it('should support project-specific blacklist rules', () => {
|
||||
const projectBlacklist: SecurityBlacklistConfig = [
|
||||
{
|
||||
description: 'Block modifying package.json in CI',
|
||||
match: {
|
||||
path: { pattern: '.*/package\\.json$', type: 'regex' },
|
||||
command: { pattern: '(vim|nano|vi|emacs|code|sed).*', type: 'regex' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Should block editing package.json
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: projectBlacklist,
|
||||
toolArgs: {
|
||||
command: 'vim package.json',
|
||||
path: '/project/package.json',
|
||||
},
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// Should allow reading package.json
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config: 'never',
|
||||
securityBlacklist: projectBlacklist,
|
||||
toolArgs: {
|
||||
command: 'cat package.json',
|
||||
path: '/project/package.json',
|
||||
},
|
||||
}),
|
||||
).toBe('never');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration scenarios', () => {
|
||||
it('should handle Bash tool scenario', () => {
|
||||
const config: HumanInterventionConfig = [
|
||||
@@ -229,24 +647,44 @@ describe('InterventionChecker', () => {
|
||||
];
|
||||
|
||||
// Safe commands - never
|
||||
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
|
||||
'never',
|
||||
);
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'ls:' },
|
||||
}),
|
||||
).toBe('never');
|
||||
|
||||
// Git commands - require
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git add:.' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'git add:.' },
|
||||
}),
|
||||
).toBe('required');
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:-m' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'git commit:-m' },
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// Dangerous commands - require
|
||||
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm:-rf' } })).toBe(
|
||||
'required',
|
||||
);
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'npm install' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'rm:-rf' },
|
||||
}),
|
||||
).toBe('required');
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { command: 'npm install' },
|
||||
}),
|
||||
).toBe('required');
|
||||
});
|
||||
|
||||
@@ -260,13 +698,18 @@ describe('InterventionChecker', () => {
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { path: '/Users/project/file.ts' },
|
||||
}),
|
||||
).toBe('never');
|
||||
|
||||
// Outside project - require
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { path: '/tmp/file.ts' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { path: '/tmp/file.ts' },
|
||||
}),
|
||||
).toBe('required');
|
||||
});
|
||||
|
||||
@@ -274,8 +717,35 @@ describe('InterventionChecker', () => {
|
||||
const config: HumanInterventionConfig = 'required';
|
||||
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({ config, toolArgs: { url: 'https://example.com' } }),
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: [], // Disable blacklist for this test
|
||||
toolArgs: { url: 'https://example.com' },
|
||||
}),
|
||||
).toBe('required');
|
||||
});
|
||||
|
||||
it('should handle security blacklist overriding user config', () => {
|
||||
const config: HumanInterventionConfig = 'never';
|
||||
const blacklist: SecurityBlacklistConfig = DEFAULT_SECURITY_BLACKLIST;
|
||||
|
||||
// Dangerous command blocked even with 'never' config
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: blacklist,
|
||||
toolArgs: { command: 'rm -rf /' },
|
||||
}),
|
||||
).toBe('required');
|
||||
|
||||
// Safe command follows config
|
||||
expect(
|
||||
InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist: blacklist,
|
||||
toolArgs: { command: 'ls -la' },
|
||||
}),
|
||||
).toBe('never');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
335
packages/agent-runtime/src/core/defaultSecurityBlacklist.ts
Normal file
335
packages/agent-runtime/src/core/defaultSecurityBlacklist.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import type { SecurityBlacklistConfig } from '@lobechat/types';
|
||||
|
||||
/**
|
||||
* Default Security Blacklist
|
||||
* These rules will ALWAYS block execution and require human intervention,
|
||||
* regardless of user settings (even in auto-run mode)
|
||||
*
|
||||
* This is the last line of defense against dangerous operations
|
||||
*/
|
||||
export const DEFAULT_SECURITY_BLACKLIST: SecurityBlacklistConfig = [
|
||||
// ==================== File System Dangers ====================
|
||||
{
|
||||
description: 'Recursive deletion of home directory is extremely dangerous',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'rm.*-r.*(~|\\$HOME|/Users/[^/]+|/home/[^/]+)/?\\s*$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Recursive deletion of root directory will destroy the system',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'rm.*-r.*/\\s*$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Force recursive deletion without specific target is too dangerous',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'rm\\s+-rf\\s+[~./]\\s*$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== System Configuration Dangers ====================
|
||||
{
|
||||
description: 'Modifying /etc/passwd could lock you out of the system',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '.*(/etc/passwd|/etc/shadow).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Modifying sudoers file without proper validation is dangerous',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '.*/etc/sudoers.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Dangerous Commands ====================
|
||||
{
|
||||
description: 'Fork bomb can crash the system',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '.*:\\(\\).*\\{.*\\|.*&.*\\};.*:.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Writing random data to disk devices can destroy data',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'dd.*of=/dev/(sd|hd|nvme).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Formatting system partitions will destroy data',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(mkfs|fdisk|parted).*(/dev/(sd|hd|nvme)|/)',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Network & Remote Access Dangers ====================
|
||||
{
|
||||
description: 'Disabling firewall exposes system to attacks',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(ufw\\s+disable|iptables\\s+-F|systemctl\\s+stop\\s+firewalld)',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Changing SSH configuration could lock you out',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '.*(/etc/ssh/sshd_config).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Package Manager Dangers ====================
|
||||
{
|
||||
description: 'Removing essential system packages can break the system',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(apt|yum|dnf|pacman)\\s+(remove|purge|erase).*(systemd|kernel|glibc|bash|sudo)',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Kernel & System Core Dangers ====================
|
||||
{
|
||||
description: 'Modifying kernel parameters without understanding can crash the system',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'echo.*>/proc/sys/.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Direct memory access is extremely dangerous',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '.*(/dev/(mem|kmem|port)).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Privilege Escalation Dangers ====================
|
||||
{
|
||||
description: 'Changing file ownership of system directories is dangerous',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'chown.*-R.*(/(etc|bin|sbin|usr|var|sys|proc)|~).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Setting SUID on shells or interpreters is a security risk',
|
||||
match: {
|
||||
command: {
|
||||
pattern: 'chmod.*(4755|u\\+s).*(sh|bash|python|perl|ruby|node)',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ==================== Sensitive Information Leakage ====================
|
||||
{
|
||||
description: 'Reading .env files may leak sensitive credentials and API keys',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*\\.env.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading .env files may leak sensitive credentials and API keys',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*\\.env.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading SSH private keys can compromise system security',
|
||||
match: {
|
||||
command: {
|
||||
pattern:
|
||||
'(cat|less|more|head|tail|vim|nano|vi|emacs|code).*(id_rsa|id_ed25519|id_ecdsa)(?!\\.pub).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading SSH private keys can compromise system security',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.ssh/(id_rsa|id_ed25519|id_ecdsa)$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Accessing AWS credentials can leak cloud access keys',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.aws/credentials.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Accessing AWS credentials can leak cloud access keys',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.aws/credentials.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Docker config may expose registry credentials',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.docker/config\\.json.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Docker config may expose registry credentials',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.docker/config\\.json$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Kubernetes config may expose cluster credentials',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.kube/config.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Kubernetes config may expose cluster credentials',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.kube/config$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Git credentials file may leak access tokens',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.git-credentials.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading Git credentials file may leak access tokens',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.git-credentials$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading npm token file may expose package registry credentials',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.npmrc.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading npm token file may expose package registry credentials',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.npmrc$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading history files may expose sensitive commands and credentials',
|
||||
match: {
|
||||
command: {
|
||||
pattern:
|
||||
'(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.(bash_history|zsh_history|history).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading history files may expose sensitive commands and credentials',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.(bash_history|zsh_history|history)$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Accessing browser credential storage may leak passwords',
|
||||
match: {
|
||||
command: {
|
||||
pattern:
|
||||
'(cat|less|more|head|tail|vim|nano|vi|emacs|code).*(Cookies|Login Data|Web Data).*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading GCP credentials may leak cloud service account keys',
|
||||
match: {
|
||||
command: {
|
||||
pattern: '(cat|less|more|head|tail|vim|nano|vi|emacs|code).*/\\.config/gcloud/.*\\.json.*',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Reading GCP credentials may leak cloud service account keys',
|
||||
match: {
|
||||
path: {
|
||||
pattern: '.*/\\.config/gcloud/.*\\.json$',
|
||||
type: 'regex',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './defaultSecurityBlacklist';
|
||||
export * from './InterventionChecker';
|
||||
export * from './runtime';
|
||||
export * from './UsageCounter';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
import { ChatToolPayload, UserInterventionConfig } from '@lobechat/types';
|
||||
import { ChatToolPayload, SecurityBlacklistConfig, UserInterventionConfig } from '@lobechat/types';
|
||||
|
||||
import type { Cost, CostLimit, Usage } from './usage';
|
||||
|
||||
@@ -23,6 +23,15 @@ export interface AgentState {
|
||||
* Controls how tools requiring approval are handled
|
||||
*/
|
||||
userInterventionConfig?: UserInterventionConfig;
|
||||
|
||||
/**
|
||||
* Security blacklist configuration
|
||||
* These rules will ALWAYS block execution and require human intervention,
|
||||
* regardless of user settings (even in auto-run mode).
|
||||
* If not provided, DEFAULT_SECURITY_BLACKLIST will be used.
|
||||
*/
|
||||
securityBlacklist?: SecurityBlacklistConfig;
|
||||
|
||||
// --- Execution Tracking ---
|
||||
/**
|
||||
* Number of execution steps in this session.
|
||||
|
||||
@@ -146,6 +146,36 @@ export const UserInterventionConfigSchema = z.object({
|
||||
approvalMode: z.enum(['auto-run', 'allow-list', 'manual']),
|
||||
});
|
||||
|
||||
/**
|
||||
* Security Blacklist Rule
|
||||
* Used to forcefully block dangerous operations regardless of user settings
|
||||
*/
|
||||
export interface SecurityBlacklistRule {
|
||||
/**
|
||||
* Description of why this rule exists (for error messages)
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Parameter filter - matches against tool call arguments
|
||||
* Same format as HumanInterventionRule.match
|
||||
*/
|
||||
match: Record<string, ArgumentMatcher>;
|
||||
}
|
||||
|
||||
export const SecurityBlacklistRuleSchema = z.object({
|
||||
description: z.string(),
|
||||
match: z.record(z.string(), ArgumentMatcherSchema),
|
||||
});
|
||||
|
||||
/**
|
||||
* Security Blacklist Configuration
|
||||
* A list of rules that will always block execution and require intervention
|
||||
*/
|
||||
export type SecurityBlacklistConfig = SecurityBlacklistRule[];
|
||||
|
||||
export const SecurityBlacklistConfigSchema = z.array(SecurityBlacklistRuleSchema);
|
||||
|
||||
/**
|
||||
* Parameters for shouldIntervene method
|
||||
*/
|
||||
@@ -161,6 +191,13 @@ export interface ShouldInterveneParams {
|
||||
*/
|
||||
confirmedHistory?: string[];
|
||||
|
||||
/**
|
||||
* Security blacklist rules that will be checked first
|
||||
* These rules override all other settings including auto-run mode
|
||||
* @default []
|
||||
*/
|
||||
securityBlacklist?: SecurityBlacklistConfig;
|
||||
|
||||
/**
|
||||
* Tool call arguments to check against rules
|
||||
* @default {}
|
||||
@@ -177,6 +214,7 @@ export interface ShouldInterveneParams {
|
||||
export const ShouldInterveneParamsSchema = z.object({
|
||||
config: HumanInterventionConfigSchema.optional(),
|
||||
confirmedHistory: z.array(z.string()).optional(),
|
||||
securityBlacklist: SecurityBlacklistConfigSchema.optional(),
|
||||
toolArgs: z.record(z.string(), z.any()).optional(),
|
||||
toolKey: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -249,4 +249,29 @@ describe('createRemarkSelfClosingTagPlugin', () => {
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle tags wrapped in backticks (code)', () => {
|
||||
const markdown = `Use this file: \`<${tagName} name="config.json" path="/app/config.json" />\` in your code.`;
|
||||
const tree = processMarkdown(markdown, tagName);
|
||||
|
||||
expect(tree.children).toHaveLength(1);
|
||||
expect(tree.children[0].type).toBe('paragraph');
|
||||
|
||||
const paragraphChildren = tree.children[0].children;
|
||||
expect(paragraphChildren).toHaveLength(3);
|
||||
|
||||
expect(paragraphChildren[0].type).toBe('text');
|
||||
expect(paragraphChildren[0].value).toBe('Use this file: ');
|
||||
|
||||
// The tag should be parsed even inside backticks
|
||||
const tagNode = paragraphChildren[1];
|
||||
expect(tagNode.type).toBe(tagName);
|
||||
expect(tagNode.data?.hProperties).toEqual({
|
||||
name: 'config.json',
|
||||
path: '/app/config.json',
|
||||
});
|
||||
|
||||
expect(paragraphChildren[2].type).toBe('text');
|
||||
expect(paragraphChildren[2].value).toBe(' in your code.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,5 +130,33 @@ export const createRemarkSelfClosingTagPlugin =
|
||||
return [SKIP, index + newChildren.length]; // Skip new nodes
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Visit inlineCode nodes (backtick-wrapped tags like `<localFile ... />`)
|
||||
// @ts-ignore
|
||||
visit(tree, 'inlineCode', (node: any, index: number, parent) => {
|
||||
log('>>> Visiting inlineCode node: "%s"', node.value);
|
||||
|
||||
if (!parent || typeof index !== 'number' || !node.value?.includes(`<${tagName}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = node.value.match(exactTagRegex);
|
||||
if (match) {
|
||||
const [, attributesString] = match;
|
||||
const properties = attributesString ? parseAttributes(attributesString.trim()) : {};
|
||||
|
||||
const newNode = {
|
||||
data: {
|
||||
hName: tagName,
|
||||
hProperties: properties,
|
||||
},
|
||||
type: tagName,
|
||||
};
|
||||
|
||||
log('Replacing inlineCode node at index %d with %s node: %o', index, tagName, newNode);
|
||||
parent.children.splice(index, 1, newNode);
|
||||
return [SKIP, index + 1];
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
AgentInstruction,
|
||||
AgentRuntimeContext,
|
||||
AgentState,
|
||||
DEFAULT_SECURITY_BLACKLIST,
|
||||
GeneralAgentCallLLMInstructionPayload,
|
||||
GeneralAgentCallLLMResultPayload,
|
||||
GeneralAgentCallToolResultPayload,
|
||||
@@ -65,6 +66,9 @@ export class GeneralChatAgent implements Agent {
|
||||
const toolsNeedingIntervention: ChatToolPayload[] = [];
|
||||
const toolsToExecute: ChatToolPayload[] = [];
|
||||
|
||||
// Get security blacklist (use default if not provided)
|
||||
const securityBlacklist = state.securityBlacklist ?? DEFAULT_SECURITY_BLACKLIST;
|
||||
|
||||
// Get user config (default to 'manual' mode)
|
||||
const userConfig = state.userInterventionConfig || { approvalMode: 'manual' };
|
||||
const { approvalMode, allowList = [] } = userConfig;
|
||||
@@ -73,6 +77,23 @@ export class GeneralChatAgent implements Agent {
|
||||
const { identifier, apiName } = toolCalling;
|
||||
const toolKey = `${identifier}/${apiName}`;
|
||||
|
||||
// Parse arguments for intervention checking
|
||||
let toolArgs: Record<string, any> = {};
|
||||
try {
|
||||
toolArgs = JSON.parse(toolCalling.arguments || '{}');
|
||||
} catch {
|
||||
// Invalid JSON, treat as empty args
|
||||
}
|
||||
|
||||
// Priority 0: CRITICAL - Check security blacklist FIRST
|
||||
// This overrides ALL other settings, including auto-run mode
|
||||
const securityCheck = InterventionChecker.checkSecurityBlacklist(securityBlacklist, toolArgs);
|
||||
if (securityCheck.blocked) {
|
||||
// Security blacklist always requires intervention
|
||||
toolsNeedingIntervention.push(toolCalling);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Priority 1: User config is 'auto-run', all tools execute directly
|
||||
if (approvalMode === 'auto-run') {
|
||||
toolsToExecute.push(toolCalling);
|
||||
@@ -92,16 +113,9 @@ export class GeneralChatAgent implements Agent {
|
||||
// Priority 3: User config is 'manual' (default), use tool's own config
|
||||
const config = this.getToolInterventionConfig(toolCalling, state);
|
||||
|
||||
// Parse arguments for intervention checking
|
||||
let toolArgs: Record<string, any> = {};
|
||||
try {
|
||||
toolArgs = JSON.parse(toolCalling.arguments || '{}');
|
||||
} catch {
|
||||
// Invalid JSON, treat as empty args
|
||||
}
|
||||
|
||||
const policy = InterventionChecker.shouldIntervene({
|
||||
config,
|
||||
securityBlacklist,
|
||||
toolArgs,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user