@drakon-systems/shieldcortex-realtime 4.42.3 → 4.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -1
- package/dist/interceptor.js +113 -1
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +8 -1
- package/interceptor.ts +148 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -675,6 +675,7 @@ export default {
|
|
|
675
675
|
enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
|
|
676
676
|
severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
|
|
677
677
|
failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
|
|
678
|
+
actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
|
|
678
679
|
} : {}),
|
|
679
680
|
logger: { info: api.logger?.info ?? console.log, warn: api.logger?.warn ?? console.warn },
|
|
680
681
|
};
|
|
@@ -692,13 +693,19 @@ export default {
|
|
|
692
693
|
if (typeof defenceMod.runDefencePipeline !== 'function')
|
|
693
694
|
return null;
|
|
694
695
|
interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline, {
|
|
696
|
+
evaluateToolCall: typeof defenceMod.evaluateToolCall === 'function'
|
|
697
|
+
? defenceMod.evaluateToolCall
|
|
698
|
+
: undefined,
|
|
695
699
|
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
696
700
|
cloudApiKey: scConfig.cloudApiKey ?? '',
|
|
697
701
|
cloudBaseUrl: scConfig.cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
|
698
702
|
cloudEnabled: scConfig.cloudEnabled ?? false,
|
|
699
703
|
}),
|
|
700
704
|
});
|
|
701
|
-
|
|
705
|
+
const guardState = interceptorConfig.actionGuard?.enabled
|
|
706
|
+
? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
|
|
707
|
+
: 'Action Guard: off';
|
|
708
|
+
api.logger?.info?.(`[shieldcortex] Interceptor active — memory writes + ${guardState} (shell/file/network/git)`);
|
|
702
709
|
return interceptorReady;
|
|
703
710
|
}
|
|
704
711
|
catch (err) {
|
package/dist/interceptor.js
CHANGED
|
@@ -26,6 +26,12 @@ const DEFAULT_CONFIG = {
|
|
|
26
26
|
high: 'deny',
|
|
27
27
|
critical: 'deny',
|
|
28
28
|
},
|
|
29
|
+
// Action Guard on by default: catastrophic ops are blocked out of the box;
|
|
30
|
+
// dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
|
|
31
|
+
actionGuard: {
|
|
32
|
+
enabled: true,
|
|
33
|
+
enforce: false,
|
|
34
|
+
},
|
|
29
35
|
};
|
|
30
36
|
export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
|
|
31
37
|
export function extractContent(toolName, args) {
|
|
@@ -133,6 +139,33 @@ export function formatApprovalPrompt(input) {
|
|
|
133
139
|
'[Approve] [Deny]',
|
|
134
140
|
].join('\n');
|
|
135
141
|
}
|
|
142
|
+
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
143
|
+
export function summariseToolArgs(args) {
|
|
144
|
+
if (!args)
|
|
145
|
+
return '';
|
|
146
|
+
const parts = [];
|
|
147
|
+
for (const [k, val] of Object.entries(args)) {
|
|
148
|
+
if (typeof val === 'string')
|
|
149
|
+
parts.push(`${k}=${val.slice(0, 80)}`);
|
|
150
|
+
else if (typeof val === 'number' || typeof val === 'boolean')
|
|
151
|
+
parts.push(`${k}=${val}`);
|
|
152
|
+
}
|
|
153
|
+
return parts.join(' ').slice(0, 160);
|
|
154
|
+
}
|
|
155
|
+
/** Operator-facing approval prompt for a gated action (not a memory write). */
|
|
156
|
+
export function formatActionGuardPrompt(toolName, v) {
|
|
157
|
+
return [
|
|
158
|
+
'🛡️ ShieldCortex — Action Intercepted',
|
|
159
|
+
'',
|
|
160
|
+
`Tool: ${toolName}`,
|
|
161
|
+
`Action: ${v.action}`,
|
|
162
|
+
`Risk: ${v.severity}`,
|
|
163
|
+
`Signals: ${v.signals.join(', ') || 'none'}`,
|
|
164
|
+
`Reason: ${v.reason}`,
|
|
165
|
+
'',
|
|
166
|
+
'[Approve] [Deny]',
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
136
169
|
// --- Audit Logging (local JSONL) ---
|
|
137
170
|
const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
|
|
138
171
|
function writeAuditEntry(entry) {
|
|
@@ -194,13 +227,92 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
194
227
|
const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
|
|
195
228
|
const log = config.logger ?? { info: console.log, warn: console.warn };
|
|
196
229
|
const onAuditEntry = options?.onAuditEntry;
|
|
230
|
+
const actionGuardCfg = config.actionGuard ?? { enabled: true, enforce: false };
|
|
231
|
+
const evaluateToolCall = options?.evaluateToolCall;
|
|
197
232
|
function emitAudit(entry) {
|
|
198
233
|
writeAuditEntry(entry);
|
|
199
234
|
onAuditEntry?.(entry);
|
|
200
235
|
}
|
|
236
|
+
function guardAuditBase(toolName, v, preview) {
|
|
237
|
+
return {
|
|
238
|
+
type: 'intercept', tool: toolName,
|
|
239
|
+
severity: v.severity === 'catastrophic' ? 'critical' : 'high',
|
|
240
|
+
firewallResult: 'ACTION_GUARD', threats: v.signals,
|
|
241
|
+
anomalyScore: v.decision === 'block' ? 1 : 0.6,
|
|
242
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
243
|
+
preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
|
247
|
+
// This is what makes "Iron Dome protects what the agent DOES" true at runtime.
|
|
248
|
+
async function runActionGuard(context) {
|
|
249
|
+
if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function')
|
|
250
|
+
return;
|
|
251
|
+
let v;
|
|
252
|
+
try {
|
|
253
|
+
v = evaluateToolCall(context.toolName, context.arguments || {});
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
// A guard error must never break the agent — log and allow. (The memory
|
|
257
|
+
// pipeline is the hard-fail path; the action guard is best-effort.)
|
|
258
|
+
log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (v.decision === 'allow')
|
|
262
|
+
return;
|
|
263
|
+
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
264
|
+
const base = guardAuditBase(context.toolName, v, preview);
|
|
265
|
+
const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
|
|
266
|
+
// Catastrophic / exfil — hard block, always enforced when the guard is enabled.
|
|
267
|
+
if (v.decision === 'block') {
|
|
268
|
+
emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
|
|
269
|
+
throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
|
|
270
|
+
}
|
|
271
|
+
// require_approval — warn-only by default (never nags), prompt when enforcing.
|
|
272
|
+
if (!actionGuardCfg.enforce) {
|
|
273
|
+
log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
|
|
274
|
+
emitAudit({ ...base, action: 'warn', outcome: 'warned' });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (typeof context.requireApproval !== 'function') {
|
|
278
|
+
const failAction = config.failurePolicy[severity];
|
|
279
|
+
emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
280
|
+
if (failAction === 'deny') {
|
|
281
|
+
throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!rateLimiter.shouldAllow()) {
|
|
286
|
+
emitAudit({ ...base, action: 'rate_limit', outcome: 'auto_denied' });
|
|
287
|
+
throw new Error('ShieldCortex: tool call auto-denied (approval rate limit exceeded)');
|
|
288
|
+
}
|
|
289
|
+
let approved;
|
|
290
|
+
try {
|
|
291
|
+
approved = await context.requireApproval(formatActionGuardPrompt(context.toolName, v));
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
const failAction = config.failurePolicy[severity];
|
|
295
|
+
log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
|
|
296
|
+
emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
297
|
+
if (failAction === 'deny') {
|
|
298
|
+
throw new Error('ShieldCortex: tool call blocked — approval error, failure policy: deny');
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (approved) {
|
|
303
|
+
emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
emitAudit({ ...base, action: 'require_approval', outcome: 'denied' });
|
|
307
|
+
throw new Error('ShieldCortex: tool call denied by user');
|
|
308
|
+
}
|
|
201
309
|
async function handleToolCall(context) {
|
|
202
|
-
|
|
310
|
+
// Non-memory tools go through the Action Guard (what the agent DOES); the
|
|
311
|
+
// memory-write tools continue through the content defence pipeline below.
|
|
312
|
+
if (!WATCHED_TOOLS.includes(context.toolName)) {
|
|
313
|
+
await runActionGuard(context);
|
|
203
314
|
return;
|
|
315
|
+
}
|
|
204
316
|
const { title, content } = extractContent(context.toolName, context.arguments);
|
|
205
317
|
const fullContent = [title, content].filter(Boolean).join(' ');
|
|
206
318
|
if (!fullContent.trim())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.43.0",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/index.ts
CHANGED
|
@@ -794,6 +794,7 @@ export default {
|
|
|
794
794
|
enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
|
|
795
795
|
severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
|
|
796
796
|
failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
|
|
797
|
+
actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
|
|
797
798
|
} : {}),
|
|
798
799
|
logger: { info: api.logger?.info ?? console.log, warn: (api.logger as any)?.warn ?? console.warn },
|
|
799
800
|
};
|
|
@@ -812,13 +813,19 @@ export default {
|
|
|
812
813
|
if (typeof defenceMod.runDefencePipeline !== 'function') return null;
|
|
813
814
|
|
|
814
815
|
interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline as Parameters<typeof createInterceptor>[1], {
|
|
816
|
+
evaluateToolCall: typeof (defenceMod as any).evaluateToolCall === 'function'
|
|
817
|
+
? ((defenceMod as any).evaluateToolCall as Parameters<typeof createInterceptor>[2] extends { evaluateToolCall?: infer E } ? E : never)
|
|
818
|
+
: undefined,
|
|
815
819
|
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
816
820
|
cloudApiKey: (scConfig as any).cloudApiKey ?? '',
|
|
817
821
|
cloudBaseUrl: (scConfig as any).cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
|
818
822
|
cloudEnabled: (scConfig as any).cloudEnabled ?? false,
|
|
819
823
|
}),
|
|
820
824
|
});
|
|
821
|
-
|
|
825
|
+
const guardState = interceptorConfig.actionGuard?.enabled
|
|
826
|
+
? (interceptorConfig.actionGuard.enforce ? 'Action Guard: enforce' : 'Action Guard: warn')
|
|
827
|
+
: 'Action Guard: off';
|
|
828
|
+
api.logger?.info?.(`[shieldcortex] Interceptor active — memory writes + ${guardState} (shell/file/network/git)`);
|
|
822
829
|
return interceptorReady;
|
|
823
830
|
} catch (err) {
|
|
824
831
|
(api.logger as any)?.warn?.(`[shieldcortex] Interceptor init failed: ${err instanceof Error ? err.message : err}`);
|
package/interceptor.ts
CHANGED
|
@@ -7,10 +7,37 @@ export type Severity = 'low' | 'medium' | 'high' | 'critical';
|
|
|
7
7
|
export type InterceptAction = 'log' | 'warn' | 'require_approval';
|
|
8
8
|
export type FailureAction = 'allow' | 'deny';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Action Guard config — gates what the agent DOES (shell/file/network/git),
|
|
12
|
+
* not just what it remembers. Catastrophic operations (rm -rf /, fork bombs,
|
|
13
|
+
* disk wipes, secret exfil) are always blocked when `enabled`; recognised
|
|
14
|
+
* dangerous ops require approval when `enforce` is set, otherwise they are
|
|
15
|
+
* surfaced (warn + audit) but allowed through — so the guard never nags on
|
|
16
|
+
* routine work by default.
|
|
17
|
+
*/
|
|
18
|
+
export interface ActionGuardConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
enforce: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Structural shape of a Tool Action Guard verdict (kept local to avoid a
|
|
24
|
+
* compile-time dependency on the main package across the plugin build boundary;
|
|
25
|
+
* the real `evaluateToolCall` from `shieldcortex/defence` is compatible). */
|
|
26
|
+
export interface ToolGuardVerdictLike {
|
|
27
|
+
decision: 'allow' | 'require_approval' | 'block';
|
|
28
|
+
severity: 'benign' | 'sensitive' | 'dangerous' | 'catastrophic' | string;
|
|
29
|
+
family: string;
|
|
30
|
+
action: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
signals: string[];
|
|
33
|
+
}
|
|
34
|
+
export type ToolGuardEvaluator = (toolName: string, args: Record<string, unknown>) => ToolGuardVerdictLike;
|
|
35
|
+
|
|
10
36
|
export interface InterceptorConfig {
|
|
11
37
|
enabled: boolean;
|
|
12
38
|
severityActions: Record<Severity, InterceptAction>;
|
|
13
39
|
failurePolicy: Record<Severity, FailureAction>;
|
|
40
|
+
actionGuard?: ActionGuardConfig;
|
|
14
41
|
logger?: { info: (msg: string) => void; warn: (msg: string) => void };
|
|
15
42
|
}
|
|
16
43
|
|
|
@@ -63,6 +90,12 @@ const DEFAULT_CONFIG: InterceptorConfig = {
|
|
|
63
90
|
high: 'deny',
|
|
64
91
|
critical: 'deny',
|
|
65
92
|
},
|
|
93
|
+
// Action Guard on by default: catastrophic ops are blocked out of the box;
|
|
94
|
+
// dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
|
|
95
|
+
actionGuard: {
|
|
96
|
+
enabled: true,
|
|
97
|
+
enforce: false,
|
|
98
|
+
},
|
|
66
99
|
};
|
|
67
100
|
|
|
68
101
|
export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
|
|
@@ -202,6 +235,32 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
|
|
|
202
235
|
].join('\n');
|
|
203
236
|
}
|
|
204
237
|
|
|
238
|
+
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
239
|
+
export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
|
|
240
|
+
if (!args) return '';
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
for (const [k, val] of Object.entries(args)) {
|
|
243
|
+
if (typeof val === 'string') parts.push(`${k}=${val.slice(0, 80)}`);
|
|
244
|
+
else if (typeof val === 'number' || typeof val === 'boolean') parts.push(`${k}=${val}`);
|
|
245
|
+
}
|
|
246
|
+
return parts.join(' ').slice(0, 160);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Operator-facing approval prompt for a gated action (not a memory write). */
|
|
250
|
+
export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLike): string {
|
|
251
|
+
return [
|
|
252
|
+
'🛡️ ShieldCortex — Action Intercepted',
|
|
253
|
+
'',
|
|
254
|
+
`Tool: ${toolName}`,
|
|
255
|
+
`Action: ${v.action}`,
|
|
256
|
+
`Risk: ${v.severity}`,
|
|
257
|
+
`Signals: ${v.signals.join(', ') || 'none'}`,
|
|
258
|
+
`Reason: ${v.reason}`,
|
|
259
|
+
'',
|
|
260
|
+
'[Approve] [Deny]',
|
|
261
|
+
].join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
205
264
|
// --- Audit Logging (local JSONL) ---
|
|
206
265
|
|
|
207
266
|
const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
|
|
@@ -297,6 +356,8 @@ type PipelineRunner = (content: string, title: string, source: { type: string; i
|
|
|
297
356
|
interface InterceptorOptions {
|
|
298
357
|
maxPromptsPerMinute?: number;
|
|
299
358
|
onAuditEntry?: (entry: InterceptAuditEntry) => void;
|
|
359
|
+
/** Tool Action Guard evaluator, injected from `shieldcortex/defence` at runtime. */
|
|
360
|
+
evaluateToolCall?: ToolGuardEvaluator;
|
|
300
361
|
}
|
|
301
362
|
|
|
302
363
|
export function createInterceptor(
|
|
@@ -311,14 +372,100 @@ export function createInterceptor(
|
|
|
311
372
|
const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
|
|
312
373
|
const log = config.logger ?? { info: console.log, warn: console.warn };
|
|
313
374
|
const onAuditEntry = options?.onAuditEntry;
|
|
375
|
+
const actionGuardCfg: ActionGuardConfig = config.actionGuard ?? { enabled: true, enforce: false };
|
|
376
|
+
const evaluateToolCall = options?.evaluateToolCall;
|
|
314
377
|
|
|
315
378
|
function emitAudit(entry: InterceptAuditEntry): void {
|
|
316
379
|
writeAuditEntry(entry);
|
|
317
380
|
onAuditEntry?.(entry);
|
|
318
381
|
}
|
|
319
382
|
|
|
383
|
+
function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
|
|
384
|
+
return {
|
|
385
|
+
type: 'intercept', tool: toolName,
|
|
386
|
+
severity: v.severity === 'catastrophic' ? 'critical' : 'high',
|
|
387
|
+
firewallResult: 'ACTION_GUARD', threats: v.signals,
|
|
388
|
+
anomalyScore: v.decision === 'block' ? 1 : 0.6,
|
|
389
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
390
|
+
preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
|
395
|
+
// This is what makes "Iron Dome protects what the agent DOES" true at runtime.
|
|
396
|
+
async function runActionGuard(context: ToolCallContext): Promise<void> {
|
|
397
|
+
if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function') return;
|
|
398
|
+
|
|
399
|
+
let v: ToolGuardVerdictLike;
|
|
400
|
+
try {
|
|
401
|
+
v = evaluateToolCall(context.toolName, context.arguments || {});
|
|
402
|
+
} catch (err) {
|
|
403
|
+
// A guard error must never break the agent — log and allow. (The memory
|
|
404
|
+
// pipeline is the hard-fail path; the action guard is best-effort.)
|
|
405
|
+
log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (v.decision === 'allow') return;
|
|
409
|
+
|
|
410
|
+
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
411
|
+
const base = guardAuditBase(context.toolName, v, preview);
|
|
412
|
+
const severity: Severity = v.severity === 'catastrophic' ? 'critical' : 'high';
|
|
413
|
+
|
|
414
|
+
// Catastrophic / exfil — hard block, always enforced when the guard is enabled.
|
|
415
|
+
if (v.decision === 'block') {
|
|
416
|
+
emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
|
|
417
|
+
throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// require_approval — warn-only by default (never nags), prompt when enforcing.
|
|
421
|
+
if (!actionGuardCfg.enforce) {
|
|
422
|
+
log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
|
|
423
|
+
emitAudit({ ...base, action: 'warn', outcome: 'warned' });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (typeof context.requireApproval !== 'function') {
|
|
428
|
+
const failAction = config.failurePolicy[severity];
|
|
429
|
+
emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
430
|
+
if (failAction === 'deny') {
|
|
431
|
+
throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!rateLimiter.shouldAllow()) {
|
|
437
|
+
emitAudit({ ...base, action: 'rate_limit', outcome: 'auto_denied' });
|
|
438
|
+
throw new Error('ShieldCortex: tool call auto-denied (approval rate limit exceeded)');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let approved: boolean;
|
|
442
|
+
try {
|
|
443
|
+
approved = await context.requireApproval(formatActionGuardPrompt(context.toolName, v));
|
|
444
|
+
} catch (err) {
|
|
445
|
+
const failAction = config.failurePolicy[severity];
|
|
446
|
+
log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
|
|
447
|
+
emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
448
|
+
if (failAction === 'deny') {
|
|
449
|
+
throw new Error('ShieldCortex: tool call blocked — approval error, failure policy: deny');
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (approved) {
|
|
455
|
+
emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
emitAudit({ ...base, action: 'require_approval', outcome: 'denied' });
|
|
459
|
+
throw new Error('ShieldCortex: tool call denied by user');
|
|
460
|
+
}
|
|
461
|
+
|
|
320
462
|
async function handleToolCall(context: ToolCallContext): Promise<void> {
|
|
321
|
-
|
|
463
|
+
// Non-memory tools go through the Action Guard (what the agent DOES); the
|
|
464
|
+
// memory-write tools continue through the content defence pipeline below.
|
|
465
|
+
if (!(WATCHED_TOOLS as readonly string[]).includes(context.toolName)) {
|
|
466
|
+
await runActionGuard(context);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
322
469
|
|
|
323
470
|
const { title, content } = extractContent(context.toolName, context.arguments);
|
|
324
471
|
const fullContent = [title, content].filter(Boolean).join(' ');
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.43.0",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/package.json
CHANGED