feat: add Security Blacklist for agent runtime (#10325)

This commit is contained in:
Arvin Xu
2025-11-20 17:57:45 +08:00
committed by GitHub
parent a41230ea11
commit deab4d0386
10 changed files with 1036 additions and 50 deletions

View File

@@ -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

View File

@@ -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
*

View File

@@ -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');
});
});
});

View 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',
},
},
},
];

View File

@@ -1,3 +1,4 @@
export * from './defaultSecurityBlacklist';
export * from './InterventionChecker';
export * from './runtime';
export * from './UsageCounter';

View File

@@ -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.

View File

@@ -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(),
});

View File

@@ -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.');
});
});

View File

@@ -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];
}
});
};
};

View File

@@ -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,
});