From bbfd2ea1fdf9e14eedd5d520b26c16b3ddada406 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Sat, 16 May 2026 01:09:43 +0800 Subject: [PATCH 1/3] fix: route OpenClaw actions through runtime protection --- src/adapters/openclaw-plugin.ts | 119 ++++++++++++++++++++++++++++++-- src/runtime/protect.ts | 6 +- src/tests/integration.test.ts | 74 ++++++++++++++++++++ src/tests/runtime-cloud.test.ts | 28 ++++++++ 4 files changed, 222 insertions(+), 5 deletions(-) diff --git a/src/adapters/openclaw-plugin.ts b/src/adapters/openclaw-plugin.ts index bfced9d..d1197b1 100644 --- a/src/adapters/openclaw-plugin.ts +++ b/src/adapters/openclaw-plugin.ts @@ -26,12 +26,20 @@ import { homedir } from 'node:os'; import * as path from 'node:path'; import { OpenClawAdapter } from './openclaw.js'; import { evaluateHook } from './engine.js'; -import { loadConfig, writeAuditLog } from './common.js'; +import { writeAuditLog } from './common.js'; import type { AgentGuardInstance } from './types.js'; +import { loadConfig as loadAgentGuardConfig } from '../config.js'; import { SkillScanner } from '../scanner/index.js'; import { SkillRegistry } from '../registry/index.js'; import { ActionScanner } from '../action/index.js'; import { DEFAULT_CAPABILITY } from '../types/skill.js'; +import { + protectAction, + type ProtectOptions, + type ProtectResult, +} from '../runtime/protect.js'; +import type { RuntimeActionType } from '../runtime/types.js'; +import type { AgentGuardConfig } from '../config.js'; // --------------------------------------------------------------------------- // OpenClaw Types (subset we use) @@ -169,11 +177,17 @@ async function autoScanSkillDirs( */ export interface OpenClawPluginOptions { /** Protection level (strict/balanced/permissive) */ - level?: string; + level?: AgentGuardConfig['level']; + /** Enable runtime policy protection via AgentGuard Cloud/cache/default policy (default: true) */ + runtimeProtection?: boolean; + /** Runtime policy decision mode: local-first fetches Cloud policy then evaluates locally; cloud delegates the decision to Cloud */ + decisionMode?: 'local-first' | 'cloud'; /** Enable auto-scanning of plugins (default: false — opt-in) */ skipAutoScan?: boolean; /** Custom AgentGuard instance factory */ agentguardFactory?: () => AgentGuardInstance; + /** Custom runtime protection function, primarily for tests */ + protectAction?: (options: ProtectOptions) => Promise; /** Custom scanner instance */ scanner?: SkillScanner; /** Custom registry instance */ @@ -331,9 +345,12 @@ export function registerOpenClawPlugin( options: OpenClawPluginOptions = {} ): void { const adapter = new OpenClawAdapter(); - const config = options.level ? { level: options.level } : loadConfig(); + const runtimeConfig = loadAgentGuardConfig(); + const config = options.level ? { ...runtimeConfig, level: options.level } : runtimeConfig; const scanner = options.scanner ?? new SkillScanner({ useExternalScanner: false }); const trustRegistry = options.registry ?? new SkillRegistry(); + const runProtectAction = options.protectAction ?? protectAction; + const runtimeProtectionEnabled = options.runtimeProtection !== false; // Simple logger const logger = (msg: string) => console.log(msg); @@ -392,7 +409,7 @@ export function registerOpenClawPlugin( } // before_tool_call → evaluate and optionally block - api.on('before_tool_call', async (event: unknown) => { + api.on('before_tool_call', async (event: unknown, ctx?: unknown) => { try { // Try to infer plugin from tool name const toolEvent = event as { toolName?: string }; @@ -409,6 +426,26 @@ export function registerOpenClawPlugin( } } + if (runtimeProtectionEnabled) { + try { + const runtimeResult = await runProtectAction({ + config, + rawInput: event, + agentHost: 'openclaw', + actionType: mapOpenClawToolToRuntimeAction(toolEvent.toolName), + toolName: toolEvent.toolName, + sessionId: readOpenClawSessionId(event, ctx), + decisionMode: options.decisionMode ?? 'local-first', + }); + const hookDecision = runtimeResultToBeforeToolCallResult(runtimeResult); + if (hookDecision) { + return hookDecision; + } + } catch (err) { + logger(`[AgentGuard] Runtime protection failed; falling back to local hook policy: ${String(err)}`); + } + } + const result = await evaluateHook(adapter, event, { config, agentguard: getAgentGuard(), @@ -451,6 +488,80 @@ export function registerOpenClawPlugin( logger(`[AgentGuard] Registered with OpenClaw (protection level: ${config.level || 'balanced'})`); } +function mapOpenClawToolToRuntimeAction(toolName: string | undefined): RuntimeActionType | undefined { + const normalized = (toolName || '').toLowerCase(); + if (!normalized) return undefined; + if (normalized === 'exec' || normalized === 'bash' || normalized.includes('shell')) { + return 'shell'; + } + if (normalized === 'read' || normalized.includes('read')) { + return 'file_read'; + } + if ( + normalized === 'write' || + normalized === 'edit' || + normalized === 'apply_patch' || + normalized.includes('write') + ) { + return 'file_write'; + } + if (normalized.includes('web') || normalized.includes('browser')) { + return 'network'; + } + return undefined; +} + +function readOpenClawSessionId(event: unknown, ctx: unknown): string | undefined { + const eventRecord = isRecord(event) ? event : undefined; + const ctxRecord = isRecord(ctx) ? ctx : undefined; + const sessionId = ctxRecord?.sessionId ?? eventRecord?.sessionId; + return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : undefined; +} + +function runtimeResultToBeforeToolCallResult( + result: ProtectResult | null +): { block: true; blockReason: string } | undefined { + if (!result) return undefined; + + const decision = result.decision.decision; + if (decision !== 'block' && decision !== 'require_approval') { + return undefined; + } + if (decision === 'require_approval' && !shouldSurfaceRuntimeApproval(result)) { + return undefined; + } + + const reasonSummary = result.decision.reasons + .map((reason) => reason.title) + .filter(Boolean) + .slice(0, 3) + .join(', '); + const approval = result.approvalId ? ` Approval: ${result.approvalId}.` : ''; + const action = decision === 'require_approval' ? 'requires approval' : 'blocked'; + + return { + block: true, + blockReason: + `GoPlus AgentGuard: runtime policy ${action} this OpenClaw tool call` + + ` (risk ${result.decision.riskScore}/100, ${result.decision.riskLevel}; policy ${result.decision.policyVersion}).` + + (reasonSummary ? ` Reasons: ${reasonSummary}.` : '') + + approval, + }; +} + +function shouldSurfaceRuntimeApproval(result: ProtectResult): boolean { + return ( + result.policySource === 'cloud-decision' || + Boolean(result.approvalId) || + result.decision.riskScore > 0 || + result.decision.reasons.length > 0 + ); +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + /** * Default export for OpenClaw plugin registration * diff --git a/src/runtime/protect.ts b/src/runtime/protect.ts index 6af06e0..ed338ba 100644 --- a/src/runtime/protect.ts +++ b/src/runtime/protect.ts @@ -62,7 +62,11 @@ export async function protectAction(options: ProtectOptions): Promise { assert.equal(result, undefined, 'Ordinary OpenClaw exec command should be allowed'); }); + it('should run runtime protection for OpenClaw tool calls', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + const calls: unknown[] = []; + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + registry: ctx.agentguard.registry as never, + protectAction: async (options) => { + calls.push(options); + return null; + }, + }); + + const result = await handlers['before_tool_call']( + { + toolName: 'exec', + params: { command: 'whoami' }, + }, + { sessionId: 'openclaw-session-1' }, + ); + + assert.equal(result, undefined, 'Allowed runtime protection result should continue'); + assert.equal(calls.length, 1); + const call = calls[0] as { + agentHost?: string; + actionType?: string; + toolName?: string; + sessionId?: string; + rawInput?: unknown; + }; + assert.equal(call.agentHost, 'openclaw'); + assert.equal(call.actionType, 'shell'); + assert.equal(call.toolName, 'exec'); + assert.equal(call.sessionId, 'openclaw-session-1'); + }); + + it('should block when runtime policy blocks an OpenClaw tool call', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + registry: ctx.agentguard.registry as never, + protectAction: async () => ({ + policySource: 'cloud-decision', + approvalId: null, + event: {} as never, + decision: { + actionId: 'act_test', + decision: 'block', + riskScore: 95, + riskLevel: 'critical', + policyVersion: 'cloud-test', + reasons: [ + { + code: 'CUSTOM_BLOCKED_COMMAND', + severity: 'critical', + title: 'Custom blocked command', + description: 'Blocked by cloud policy.', + }, + ], + }, + }), + }); + + const result = await handlers['before_tool_call']({ + toolName: 'exec', + params: { command: 'echo hello' }, + }) as { block?: boolean; blockReason?: string } | undefined; + + assert.equal(result?.block, true); + assert.ok(result?.blockReason?.includes('runtime policy blocked')); + assert.ok(result?.blockReason?.includes('cloud-test')); + }); + it('should return { block: true } for rm -rf /', async () => { ctx = createTestContext(); const { api, handlers } = createMockApi(); diff --git a/src/tests/runtime-cloud.test.ts b/src/tests/runtime-cloud.test.ts index 629ab19..c7f7957 100644 --- a/src/tests/runtime-cloud.test.ts +++ b/src/tests/runtime-cloud.test.ts @@ -142,6 +142,34 @@ describe('Runtime Cloud bridge', () => { assert.ok(!audit.includes('secret-value')); }); + it('protectAction still returns policy decision when local audit write fails', async () => { + const dir = mkdtempSync(join(tmpdir(), 'agentguard-audit-fail-')); + const policy = getDefaultEffectiveRuntimePolicy(); + policy.blockedCommandPatterns = ['cached-danger']; + + const config: AgentGuardConfig = { + version: 1, + level: 'balanced', + cloudUrl: 'https://127.0.0.1:9', + apiKey: 'ag_live_test_key_123456', + policyCachePath: join(dir, 'policy.json'), + auditPath: dir, + eventSpoolPath: join(dir, 'spool.jsonl'), + }; + writeFileSync(config.policyCachePath, JSON.stringify(policy)); + + const result = await protectAction({ + config, + stdinText: JSON.stringify({ + tool_name: 'Bash', + tool_input: { command: 'cached-danger' }, + session_id: 'sess_test', + }), + }); + + assert.equal(result?.decision.decision, 'block'); + }); + it('syncs redacted audit events and creates Cloud approval on require_approval', async () => { const originalFetch = globalThis.fetch; const dir = mkdtempSync(join(tmpdir(), 'agentguard-cloud-ok-')); From 4d4d8eb076c6de657f7ee52352e473143cd42f65 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Sat, 16 May 2026 07:46:49 +0800 Subject: [PATCH 2/3] fix: align OpenClaw registry and plugin config handling --- src/adapters/openclaw-plugin.ts | 23 +++++++-- src/installers.ts | 1 - src/tests/installer.test.ts | 4 +- src/tests/integration.test.ts | 82 ++++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/src/adapters/openclaw-plugin.ts b/src/adapters/openclaw-plugin.ts index d1197b1..d314d93 100644 --- a/src/adapters/openclaw-plugin.ts +++ b/src/adapters/openclaw-plugin.ts @@ -72,6 +72,7 @@ interface OpenClawPluginApi { id: string; name: string; source: string; + pluginConfig?: Record; on(event: string, handler: (event: unknown, ctx?: unknown) => Promise): void; on(event: string, options: Record, handler: (event: unknown, ctx?: unknown) => Promise): void; } @@ -218,10 +219,16 @@ const pluginScanCache = new Map 0 ? sessionId : undefined; } +function readOpenClawConfigLevel( + pluginConfig: Record | undefined +): AgentGuardConfig['level'] | undefined { + const level = pluginConfig?.level; + return level === 'strict' || level === 'balanced' || level === 'permissive' + ? level + : undefined; +} + function runtimeResultToBeforeToolCallResult( result: ProtectResult | null ): { block: true; blockReason: string } | undefined { diff --git a/src/installers.ts b/src/installers.ts index 0b6335f..ebe11e7 100644 --- a/src/installers.ts +++ b/src/installers.ts @@ -142,7 +142,6 @@ function openClawPluginTemplate(): string { export default function setup(api) { registerOpenClawPlugin(api, { - level: 'balanced', skipAutoScan: false, }); } diff --git a/src/tests/installer.test.ts b/src/tests/installer.test.ts index 54a9464..e262325 100644 --- a/src/tests/installer.test.ts +++ b/src/tests/installer.test.ts @@ -27,6 +27,8 @@ describe('Agent template installers', () => { const dir = mkdtempSync(join(tmpdir(), 'agentguard-openclaw-')); installAgentTemplates('openclaw', { cwd: dir }); - assert.ok(readFileSync(join(dir, 'openclaw.agentguard.plugin.ts'), 'utf8').includes('registerOpenClawPlugin')); + const template = readFileSync(join(dir, 'openclaw.agentguard.plugin.ts'), 'utf8'); + assert.ok(template.includes('registerOpenClawPlugin')); + assert.ok(!template.includes("level: 'balanced'")); }); }); diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index b4ca11f..bb45eb5 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -93,8 +93,12 @@ describe('Integration: Claude Code evaluateHook', () => { describe('Integration: OpenClaw registerOpenClawPlugin', () => { let ctx: ReturnType; + const openClawRegistryState = Symbol.for('openclaw.pluginRegistryState'); - afterEach(() => ctx?.cleanup()); + afterEach(() => { + ctx?.cleanup(); + delete (globalThis as Record)[openClawRegistryState]; + }); function createMockApi() { const handlers: Record Promise> = {}; @@ -120,6 +124,82 @@ describe('Integration: OpenClaw registerOpenClawPlugin', () => { assert.ok(handlers['after_tool_call'], 'Should register after_tool_call'); }); + it('should auto-scan plugins from OpenClaw activeRegistry state', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + const scannedPaths: string[] = []; + (globalThis as Record)[openClawRegistryState] = { + activeRegistry: { + plugins: [ + { + id: 'risky-plugin', + name: 'Risky Plugin', + source: '/tmp/risky-plugin/index.ts', + status: 'loaded', + enabled: true, + toolNames: ['risky_exec'], + }, + { + id: 'test-plugin', + name: 'AgentGuard', + source: '/tmp/test-plugin/index.ts', + status: 'loaded', + enabled: true, + toolNames: ['agentguard_internal'], + }, + ], + }, + }; + registerOpenClawPlugin(api as never, { + skipAutoScan: false, + agentguardFactory: () => ctx.agentguard as never, + protectAction: async () => null, + scanner: { + quickScan: async (pluginPath: string) => { + scannedPaths.push(pluginPath); + return { + risk_level: 'critical', + risk_tags: ['TROJAN_DISTRIBUTION'], + summary: 'critical plugin', + }; + }, + } as never, + }); + + await new Promise((resolve) => setImmediate(resolve)); + + assert.deepEqual(scannedPaths, ['/tmp/risky-plugin']); + const result = await handlers['before_tool_call']({ + toolName: 'risky_exec', + params: { command: 'echo hello' }, + }) as { block?: boolean; blockReason?: string } | undefined; + assert.equal(result?.block, true); + assert.ok(result?.blockReason?.includes('risky-plugin')); + }); + + it('should use protection level from OpenClaw plugin config', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + const levels: unknown[] = []; + (api as { pluginConfig?: Record }).pluginConfig = { level: 'strict' }; + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + agentguardFactory: () => ctx.agentguard as never, + protectAction: async (options) => { + levels.push(options.config.level); + return null; + }, + }); + + const result = await handlers['before_tool_call']({ + toolName: 'exec', + params: { command: 'echo hello' }, + }) as { block?: boolean; blockReason?: string } | undefined; + + assert.equal(result, undefined); + assert.deepEqual(levels, ['strict']); + }); + it('should return undefined (allow) for safe command', async () => { ctx = createTestContext(); const { api, handlers } = createMockApi(); From 3f9fddb118b655387546484c0cdbf0c504489ed9 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Sat, 16 May 2026 07:54:58 +0800 Subject: [PATCH 3/3] fix: harden OpenClaw runtime protection fallback --- src/adapters/openclaw-plugin.ts | 100 +++++++++++++++++++++++++++++--- src/tests/integration.test.ts | 73 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 8 deletions(-) diff --git a/src/adapters/openclaw-plugin.ts b/src/adapters/openclaw-plugin.ts index d314d93..5d2a964 100644 --- a/src/adapters/openclaw-plugin.ts +++ b/src/adapters/openclaw-plugin.ts @@ -181,6 +181,8 @@ export interface OpenClawPluginOptions { level?: AgentGuardConfig['level']; /** Enable runtime policy protection via AgentGuard Cloud/cache/default policy (default: true) */ runtimeProtection?: boolean; + /** Runtime protection failure behavior (default: block security-sensitive actions) */ + runtimeFailureMode?: 'block' | 'fallback'; /** Runtime policy decision mode: local-first fetches Cloud policy then evaluates locally; cloud delegates the decision to Cloud */ decisionMode?: 'local-first' | 'cloud'; /** Enable auto-scanning of plugins (default: false — opt-in) */ @@ -435,12 +437,13 @@ export function registerOpenClawPlugin( } if (runtimeProtectionEnabled) { + const runtimeActionType = mapOpenClawToolToRuntimeAction(toolEvent.toolName, event); try { const runtimeResult = await runProtectAction({ config, rawInput: event, agentHost: 'openclaw', - actionType: mapOpenClawToolToRuntimeAction(toolEvent.toolName), + actionType: runtimeActionType, toolName: toolEvent.toolName, sessionId: readOpenClawSessionId(event, ctx), decisionMode: options.decisionMode ?? 'local-first', @@ -450,6 +453,17 @@ export function registerOpenClawPlugin( return hookDecision; } } catch (err) { + if ( + options.runtimeFailureMode !== 'fallback' && + isSecuritySensitiveRuntimeAction(runtimeActionType) + ) { + return { + block: true, + blockReason: + `GoPlus AgentGuard: runtime protection failed for this OpenClaw tool call` + + ` (${String(err)}). Blocking by default.`, + }; + } logger(`[AgentGuard] Runtime protection failed; falling back to local hook policy: ${String(err)}`); } } @@ -496,27 +510,97 @@ export function registerOpenClawPlugin( logger(`[AgentGuard] Registered with OpenClaw (protection level: ${config.level || 'balanced'})`); } -function mapOpenClawToolToRuntimeAction(toolName: string | undefined): RuntimeActionType | undefined { +function mapOpenClawToolToRuntimeAction( + toolName: string | undefined, + event?: unknown +): RuntimeActionType { const normalized = (toolName || '').toLowerCase(); - if (!normalized) return undefined; - if (normalized === 'exec' || normalized === 'bash' || normalized.includes('shell')) { + if ( + normalized === 'exec' || + normalized === 'bash' || + normalized === 'cmd' || + normalized === 'command' || + normalized === 'terminal' || + normalized === 'run' || + normalized.includes('shell') || + normalized.includes('terminal') || + normalized.includes('command') || + normalized.includes('process') || + normalized.includes('spawn') + ) { return 'shell'; } - if (normalized === 'read' || normalized.includes('read')) { + if (normalized === 'read' || normalized.includes('read') || normalized.includes('fetch_file')) { return 'file_read'; } if ( normalized === 'write' || normalized === 'edit' || normalized === 'apply_patch' || - normalized.includes('write') + normalized === 'patch' || + normalized === 'create' || + normalized === 'save' || + normalized === 'delete' || + normalized === 'remove' || + normalized === 'rename' || + normalized === 'scaffold' || + normalized.includes('write') || + normalized.includes('edit') || + normalized.includes('patch') || + normalized.includes('delete') || + normalized.includes('remove') || + normalized.includes('rename') || + normalized.includes('scaffold') ) { return 'file_write'; } - if (normalized.includes('web') || normalized.includes('browser')) { + if ( + normalized.includes('web') || + normalized.includes('browser') || + normalized.includes('http') || + normalized.includes('fetch') || + normalized.includes('request') + ) { return 'network'; } - return undefined; + + const params = readOpenClawParams(event); + if (typeof params?.command === 'string' || typeof params?.cmd === 'string') { + return 'shell'; + } + if ( + typeof params?.url === 'string' || + typeof params?.uri === 'string' || + typeof params?.query === 'string' + ) { + return 'network'; + } + if ( + typeof params?.content === 'string' || + typeof params?.newContent === 'string' || + typeof params?.patch === 'string' + ) { + return 'file_write'; + } + if ( + typeof params?.path === 'string' || + typeof params?.file_path === 'string' || + typeof params?.filePath === 'string' + ) { + return 'file_read'; + } + + return 'other'; +} + +function readOpenClawParams(event: unknown): Record | undefined { + const record = isRecord(event) ? event : undefined; + const params = record?.params ?? record?.toolInput ?? record?.tool_input; + return isRecord(params) ? params : undefined; +} + +function isSecuritySensitiveRuntimeAction(actionType: RuntimeActionType): boolean { + return actionType !== 'other'; } function readOpenClawSessionId(event: unknown, ctx: unknown): string | undefined { diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index bb45eb5..5c44443 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -266,6 +266,79 @@ describe('Integration: OpenClaw registerOpenClawPlugin', () => { assert.equal(call.sessionId, 'openclaw-session-1'); }); + it('should classify renamed OpenClaw shell and file tools before runtime protection', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + const calls: unknown[] = []; + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + registry: ctx.agentguard.registry as never, + protectAction: async (options) => { + calls.push({ toolName: options.toolName, actionType: options.actionType }); + return null; + }, + }); + + await handlers['before_tool_call']({ + toolName: 'terminal', + params: { command: 'whoami' }, + }); + await handlers['before_tool_call']({ + toolName: 'scaffold', + params: { path: 'src/generated.ts', content: 'export {};' }, + }); + await handlers['before_tool_call']({ + toolName: 'vendorTool', + params: { command: 'echo hello' }, + }); + + assert.deepEqual(calls, [ + { toolName: 'terminal', actionType: 'shell' }, + { toolName: 'scaffold', actionType: 'file_write' }, + { toolName: 'vendorTool', actionType: 'shell' }, + ]); + }); + + it('should fail closed for security-sensitive OpenClaw actions when runtime protection fails', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + registry: ctx.agentguard.registry as never, + protectAction: async () => { + throw new Error('runtime unavailable'); + }, + }); + + const result = await handlers['before_tool_call']({ + toolName: 'terminal', + params: { command: 'echo hello' }, + }) as { block?: boolean; blockReason?: string } | undefined; + + assert.equal(result?.block, true); + assert.ok(result?.blockReason?.includes('runtime protection failed')); + }); + + it('should allow explicit fallback when runtime protection fails', async () => { + ctx = createTestContext(); + const { api, handlers } = createMockApi(); + registerOpenClawPlugin(api as never, { + skipAutoScan: true, + registry: ctx.agentguard.registry as never, + runtimeFailureMode: 'fallback', + protectAction: async () => { + throw new Error('runtime unavailable'); + }, + }); + + const result = await handlers['before_tool_call']({ + toolName: 'terminal', + params: { command: 'echo hello' }, + }); + + assert.equal(result, undefined); + }); + it('should block when runtime policy blocks an OpenClaw tool call', async () => { ctx = createTestContext(); const { api, handlers } = createMockApi();