@drakon-systems/shieldcortex-realtime 4.31.1 → 4.32.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 +113 -49
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +131 -48
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77,7 +77,11 @@ function collectRuntimeCandidates() {
|
|
|
77
77
|
}
|
|
78
78
|
return [...candidates];
|
|
79
79
|
}
|
|
80
|
+
// Test seam: lets the jest suite inject a spy runtime without touching disk.
|
|
81
|
+
let _runtimeOverride = null;
|
|
80
82
|
async function getRuntime() {
|
|
83
|
+
if (_runtimeOverride)
|
|
84
|
+
return _runtimeOverride;
|
|
81
85
|
if (!runtimePromise) {
|
|
82
86
|
runtimePromise = (async () => {
|
|
83
87
|
const tried = [];
|
|
@@ -100,6 +104,43 @@ async function getRuntime() {
|
|
|
100
104
|
}
|
|
101
105
|
return runtimePromise;
|
|
102
106
|
}
|
|
107
|
+
// ==================== SHARED IN-PROCESS DEFENCE MODULE ====================
|
|
108
|
+
// `shieldcortex/defence` is loaded ONCE and shared by both the
|
|
109
|
+
// `before_tool_call` interceptor (runDefencePipeline) and realtime scanning
|
|
110
|
+
// (scanToolResponse). The dynamic import uses a string-concatenated specifier
|
|
111
|
+
// so TypeScript does not resolve it at compile time — the module only exists at
|
|
112
|
+
// runtime when the package is installed, not during CI builds of the plugin.
|
|
113
|
+
// Returns null (cached) when the module is unavailable so callers can fall back
|
|
114
|
+
// to the mcporter shell-out gracefully.
|
|
115
|
+
let _defenceModPromise = null;
|
|
116
|
+
let _defenceModOverride; // undefined = not overridden
|
|
117
|
+
async function getDefenceModule() {
|
|
118
|
+
if (_defenceModOverride !== undefined)
|
|
119
|
+
return _defenceModOverride;
|
|
120
|
+
if (!_defenceModPromise) {
|
|
121
|
+
_defenceModPromise = (async () => {
|
|
122
|
+
try {
|
|
123
|
+
const defenceModPath = 'shieldcortex' + '/defence';
|
|
124
|
+
return (await import(/* webpackIgnore: true */ defenceModPath));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Older install / package not resolvable — caller falls back.
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
return _defenceModPromise;
|
|
133
|
+
}
|
|
134
|
+
// Test seams (jest only): inject a stub defence module / spy runtime, then reset.
|
|
135
|
+
export function __setDefenceModuleForTest(mod) {
|
|
136
|
+
_defenceModOverride = mod;
|
|
137
|
+
_defenceModPromise = null;
|
|
138
|
+
}
|
|
139
|
+
export function __setRuntimeForTest(runtime) {
|
|
140
|
+
_runtimeOverride = runtime;
|
|
141
|
+
if (runtime)
|
|
142
|
+
runtimePromise = null;
|
|
143
|
+
}
|
|
103
144
|
const PLUGIN_ID = "shieldcortex-realtime";
|
|
104
145
|
const PLUGIN_PACKAGE_NAME = "@drakon-systems/shieldcortex-realtime";
|
|
105
146
|
const PLUGIN_CONFIG_UI_HINTS = {
|
|
@@ -262,15 +303,10 @@ async function callCortex(tool, args = {}) {
|
|
|
262
303
|
return (await getRuntime()).callCortex(tool, args);
|
|
263
304
|
}
|
|
264
305
|
// ==================== REMOTE SCANNING ====================
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
mode: "advisory",
|
|
270
|
-
});
|
|
271
|
-
if (!response) {
|
|
272
|
-
return { clean: true, summary: "scan unavailable" };
|
|
273
|
-
}
|
|
306
|
+
// Build the `{ clean, summary }` contract from the parsed MCP text response.
|
|
307
|
+
// Kept identical to the historical regex parse so the fallback degrades to the
|
|
308
|
+
// exact behaviour callers depended on before in-process scanning landed.
|
|
309
|
+
function parseScanResponse(response) {
|
|
274
310
|
const cleanMatch = response.match(/\*\*Clean:\*\*\s*(Yes|No)/i);
|
|
275
311
|
const riskMatch = response.match(/\*\*Risk Level:\*\*\s*([A-Za-z]+)/i);
|
|
276
312
|
const detectionsMatch = response.match(/\*\*Detections:\*\*\s*(\d+)/i);
|
|
@@ -280,6 +316,34 @@ async function scanRealtimeContent(text) {
|
|
|
280
316
|
const summary = detections ? `${risk} (${detections} detections)` : risk;
|
|
281
317
|
return { clean, summary };
|
|
282
318
|
}
|
|
319
|
+
export async function scanRealtimeContent(text) {
|
|
320
|
+
// PRIMARY: scan in-process via the shared shieldcortex/defence module. The
|
|
321
|
+
// scan is pure (no DB handle required — scanToolResponse's audit write is
|
|
322
|
+
// guarded by isDatabaseInitialized()), so it is safe in the long-lived
|
|
323
|
+
// gateway and avoids booting a cold MCP server per message.
|
|
324
|
+
const defenceMod = await getDefenceModule();
|
|
325
|
+
if (defenceMod && typeof defenceMod.scanToolResponse === "function") {
|
|
326
|
+
const scan = defenceMod.scanToolResponse("openclaw-realtime", text, "advisory");
|
|
327
|
+
// Reproduce the historical summary contract exactly: risk level + detection
|
|
328
|
+
// count only when the injection scan flagged something.
|
|
329
|
+
const risk = scan.injection.clean ? "unknown" : scan.injection.riskLevel;
|
|
330
|
+
const summary = scan.injection.clean
|
|
331
|
+
? risk
|
|
332
|
+
: `${risk} (${scan.injection.detections.length} detections)`;
|
|
333
|
+
return { clean: scan.clean, summary };
|
|
334
|
+
}
|
|
335
|
+
// FALLBACK: in-process defence unavailable (older install, import failed) —
|
|
336
|
+
// degrade to the MCP shell-out so scanning still happens rather than breaking.
|
|
337
|
+
const response = await callCortex("scan_tool_response", {
|
|
338
|
+
toolName: "openclaw-realtime",
|
|
339
|
+
content: text,
|
|
340
|
+
mode: "advisory",
|
|
341
|
+
});
|
|
342
|
+
if (!response) {
|
|
343
|
+
return { clean: true, summary: "scan unavailable" };
|
|
344
|
+
}
|
|
345
|
+
return parseScanResponse(response);
|
|
346
|
+
}
|
|
283
347
|
// ==================== CONTENT PATTERNS ====================
|
|
284
348
|
const PATTERNS = {
|
|
285
349
|
architecture: [/\b(?:architecture|designed|structured)\b.*?(?:uses?|is|with)\b/i, /\b(?:decided?\s+to|going\s+with|chose)\b/i],
|
|
@@ -477,37 +541,41 @@ const SKIP_PATTERNS = [
|
|
|
477
541
|
function isInternalContent(text) {
|
|
478
542
|
return SKIP_PATTERNS.some(p => p.test(text.trim()));
|
|
479
543
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
544
|
+
// Awaitable scan body — extracted so the jest suite can verify behaviour
|
|
545
|
+
// deterministically. handleLlmInput wraps this fire-and-forget so the hook
|
|
546
|
+
// itself stays non-blocking.
|
|
547
|
+
export async function scanLlmInput(event, _ctx) {
|
|
548
|
+
try {
|
|
549
|
+
// Only scan user content, skip system/boot/heartbeat prompts
|
|
550
|
+
const userTexts = extractUserContent(event.historyMessages).slice(-5);
|
|
551
|
+
const texts = [event.prompt, ...userTexts].filter(t => t && !isInternalContent(t));
|
|
552
|
+
for (const text of texts) {
|
|
553
|
+
if (!text || text.length < 10)
|
|
554
|
+
continue;
|
|
555
|
+
const result = await scanRealtimeContent(text);
|
|
556
|
+
if (!result.clean) {
|
|
557
|
+
console.warn(`[shieldcortex] ⚠️ Threat in LLM input: ${result.summary}`);
|
|
558
|
+
const entry = {
|
|
559
|
+
type: "threat", hook: "llm_input", sessionId: event.sessionId,
|
|
560
|
+
model: event.model, reason: result.summary,
|
|
561
|
+
preview: text.slice(0, 100), ts: new Date().toISOString(),
|
|
562
|
+
};
|
|
563
|
+
auditLog(entry);
|
|
564
|
+
loadConfig()
|
|
565
|
+
// Pass the local entry as-is; cloudSync strips the input preview/content
|
|
566
|
+
// before transmit (metadata-only egress). No raw LLM input leaves here.
|
|
567
|
+
.then(cfg => cloudSync(entry, cfg))
|
|
568
|
+
.catch(() => { });
|
|
505
569
|
}
|
|
506
570
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function handleLlmInput(event, ctx) {
|
|
577
|
+
// Fire and forget
|
|
578
|
+
void scanLlmInput(event, ctx);
|
|
511
579
|
}
|
|
512
580
|
// Skip text blocks that are ShieldCortex/OpenClaw tool-result pass-throughs
|
|
513
581
|
function isToolResultContent(text) {
|
|
@@ -611,17 +679,13 @@ export default {
|
|
|
611
679
|
};
|
|
612
680
|
if (!interceptorConfig.enabled)
|
|
613
681
|
return null;
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
622
|
-
catch (importErr) {
|
|
623
|
-
// Stack overflow or missing module — interceptor can't load
|
|
624
|
-
api.logger?.warn?.(`[shieldcortex] Cannot load defence module: ${importErr instanceof Error ? importErr.message : importErr}`);
|
|
682
|
+
// Shared in-process defence module (same instance realtime scanning
|
|
683
|
+
// uses — see getDefenceModule). Loaded via a string-concatenated
|
|
684
|
+
// specifier so TypeScript doesn't resolve 'shieldcortex/defence' at
|
|
685
|
+
// compile time; it only exists at runtime once the package is installed.
|
|
686
|
+
const defenceMod = await getDefenceModule();
|
|
687
|
+
if (!defenceMod) {
|
|
688
|
+
api.logger?.warn?.('[shieldcortex] Cannot load defence module — interceptor disabled');
|
|
625
689
|
return null;
|
|
626
690
|
}
|
|
627
691
|
if (typeof defenceMod.runDefencePipeline !== 'function')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.32.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
|
@@ -28,6 +28,21 @@ type OpenClawRuntime = {
|
|
|
28
28
|
loadShieldConfig: () => Promise<any>;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// The subset of `shieldcortex/defence` the plugin uses in-process. Both the
|
|
32
|
+
// `before_tool_call` interceptor (runDefencePipeline) and realtime scanning
|
|
33
|
+
// (scanToolResponse) load from the SAME module via getDefenceModule().
|
|
34
|
+
type DefenceModule = {
|
|
35
|
+
runDefencePipeline?: (...args: any[]) => any;
|
|
36
|
+
scanToolResponse?: (
|
|
37
|
+
toolName: string,
|
|
38
|
+
content: string,
|
|
39
|
+
mode?: 'advisory' | 'enforce',
|
|
40
|
+
) => {
|
|
41
|
+
clean: boolean;
|
|
42
|
+
injection: { clean: boolean; riskLevel: string; detections: unknown[] };
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
let runtimePromise: Promise<OpenClawRuntime> | null = null;
|
|
32
47
|
|
|
33
48
|
function addRuntimeCandidate(candidates: Set<string>, packageRoot: string) {
|
|
@@ -95,7 +110,11 @@ function collectRuntimeCandidates(): string[] {
|
|
|
95
110
|
return [...candidates];
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
// Test seam: lets the jest suite inject a spy runtime without touching disk.
|
|
114
|
+
let _runtimeOverride: OpenClawRuntime | null = null;
|
|
115
|
+
|
|
98
116
|
async function getRuntime(): Promise<OpenClawRuntime> {
|
|
117
|
+
if (_runtimeOverride) return _runtimeOverride;
|
|
99
118
|
if (!runtimePromise) {
|
|
100
119
|
runtimePromise = (async () => {
|
|
101
120
|
const tried: string[] = [];
|
|
@@ -121,6 +140,43 @@ async function getRuntime(): Promise<OpenClawRuntime> {
|
|
|
121
140
|
return runtimePromise;
|
|
122
141
|
}
|
|
123
142
|
|
|
143
|
+
// ==================== SHARED IN-PROCESS DEFENCE MODULE ====================
|
|
144
|
+
// `shieldcortex/defence` is loaded ONCE and shared by both the
|
|
145
|
+
// `before_tool_call` interceptor (runDefencePipeline) and realtime scanning
|
|
146
|
+
// (scanToolResponse). The dynamic import uses a string-concatenated specifier
|
|
147
|
+
// so TypeScript does not resolve it at compile time — the module only exists at
|
|
148
|
+
// runtime when the package is installed, not during CI builds of the plugin.
|
|
149
|
+
// Returns null (cached) when the module is unavailable so callers can fall back
|
|
150
|
+
// to the mcporter shell-out gracefully.
|
|
151
|
+
let _defenceModPromise: Promise<DefenceModule | null> | null = null;
|
|
152
|
+
let _defenceModOverride: DefenceModule | null | undefined; // undefined = not overridden
|
|
153
|
+
|
|
154
|
+
async function getDefenceModule(): Promise<DefenceModule | null> {
|
|
155
|
+
if (_defenceModOverride !== undefined) return _defenceModOverride;
|
|
156
|
+
if (!_defenceModPromise) {
|
|
157
|
+
_defenceModPromise = (async () => {
|
|
158
|
+
try {
|
|
159
|
+
const defenceModPath = 'shieldcortex' + '/defence';
|
|
160
|
+
return (await import(/* webpackIgnore: true */ defenceModPath)) as DefenceModule;
|
|
161
|
+
} catch {
|
|
162
|
+
// Older install / package not resolvable — caller falls back.
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
}
|
|
167
|
+
return _defenceModPromise;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Test seams (jest only): inject a stub defence module / spy runtime, then reset.
|
|
171
|
+
export function __setDefenceModuleForTest(mod: DefenceModule | null | undefined): void {
|
|
172
|
+
_defenceModOverride = mod;
|
|
173
|
+
_defenceModPromise = null;
|
|
174
|
+
}
|
|
175
|
+
export function __setRuntimeForTest(runtime: OpenClawRuntime | null): void {
|
|
176
|
+
_runtimeOverride = runtime;
|
|
177
|
+
if (runtime) runtimePromise = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
124
180
|
type LlmInputEvent = {
|
|
125
181
|
runId: string; sessionId: string; provider: string; model: string;
|
|
126
182
|
systemPrompt?: string; prompt: string; historyMessages: unknown[]; imagesCount: number;
|
|
@@ -331,17 +387,10 @@ async function callCortex(tool: string, args: Record<string, string> = {}): Prom
|
|
|
331
387
|
|
|
332
388
|
// ==================== REMOTE SCANNING ====================
|
|
333
389
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
mode: "advisory",
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
if (!response) {
|
|
342
|
-
return { clean: true, summary: "scan unavailable" };
|
|
343
|
-
}
|
|
344
|
-
|
|
390
|
+
// Build the `{ clean, summary }` contract from the parsed MCP text response.
|
|
391
|
+
// Kept identical to the historical regex parse so the fallback degrades to the
|
|
392
|
+
// exact behaviour callers depended on before in-process scanning landed.
|
|
393
|
+
function parseScanResponse(response: string): { clean: boolean; summary: string } {
|
|
345
394
|
const cleanMatch = response.match(/\*\*Clean:\*\*\s*(Yes|No)/i);
|
|
346
395
|
const riskMatch = response.match(/\*\*Risk Level:\*\*\s*([A-Za-z]+)/i);
|
|
347
396
|
const detectionsMatch = response.match(/\*\*Detections:\*\*\s*(\d+)/i);
|
|
@@ -354,6 +403,38 @@ async function scanRealtimeContent(text: string): Promise<{ clean: boolean; summ
|
|
|
354
403
|
return { clean, summary };
|
|
355
404
|
}
|
|
356
405
|
|
|
406
|
+
export async function scanRealtimeContent(text: string): Promise<{ clean: boolean; summary: string }> {
|
|
407
|
+
// PRIMARY: scan in-process via the shared shieldcortex/defence module. The
|
|
408
|
+
// scan is pure (no DB handle required — scanToolResponse's audit write is
|
|
409
|
+
// guarded by isDatabaseInitialized()), so it is safe in the long-lived
|
|
410
|
+
// gateway and avoids booting a cold MCP server per message.
|
|
411
|
+
const defenceMod = await getDefenceModule();
|
|
412
|
+
if (defenceMod && typeof defenceMod.scanToolResponse === "function") {
|
|
413
|
+
const scan = defenceMod.scanToolResponse("openclaw-realtime", text, "advisory");
|
|
414
|
+
// Reproduce the historical summary contract exactly: risk level + detection
|
|
415
|
+
// count only when the injection scan flagged something.
|
|
416
|
+
const risk = scan.injection.clean ? "unknown" : scan.injection.riskLevel;
|
|
417
|
+
const summary = scan.injection.clean
|
|
418
|
+
? risk
|
|
419
|
+
: `${risk} (${scan.injection.detections.length} detections)`;
|
|
420
|
+
return { clean: scan.clean, summary };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// FALLBACK: in-process defence unavailable (older install, import failed) —
|
|
424
|
+
// degrade to the MCP shell-out so scanning still happens rather than breaking.
|
|
425
|
+
const response = await callCortex("scan_tool_response", {
|
|
426
|
+
toolName: "openclaw-realtime",
|
|
427
|
+
content: text,
|
|
428
|
+
mode: "advisory",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (!response) {
|
|
432
|
+
return { clean: true, summary: "scan unavailable" };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return parseScanResponse(response);
|
|
436
|
+
}
|
|
437
|
+
|
|
357
438
|
// ==================== CONTENT PATTERNS ====================
|
|
358
439
|
|
|
359
440
|
const PATTERNS: Record<string, RegExp[]> = {
|
|
@@ -579,35 +660,40 @@ function isInternalContent(text: string): boolean {
|
|
|
579
660
|
return SKIP_PATTERNS.some(p => p.test(text.trim()));
|
|
580
661
|
}
|
|
581
662
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
663
|
+
// Awaitable scan body — extracted so the jest suite can verify behaviour
|
|
664
|
+
// deterministically. handleLlmInput wraps this fire-and-forget so the hook
|
|
665
|
+
// itself stays non-blocking.
|
|
666
|
+
export async function scanLlmInput(event: LlmInputEvent, _ctx: AgentCtx): Promise<void> {
|
|
667
|
+
try {
|
|
668
|
+
// Only scan user content, skip system/boot/heartbeat prompts
|
|
669
|
+
const userTexts = extractUserContent(event.historyMessages).slice(-5);
|
|
670
|
+
const texts = [event.prompt, ...userTexts].filter(t => t && !isInternalContent(t));
|
|
671
|
+
for (const text of texts) {
|
|
672
|
+
if (!text || text.length < 10) continue;
|
|
673
|
+
const result = await scanRealtimeContent(text);
|
|
674
|
+
if (!result.clean) {
|
|
675
|
+
console.warn(`[shieldcortex] ⚠️ Threat in LLM input: ${result.summary}`);
|
|
676
|
+
const entry = {
|
|
677
|
+
type: "threat", hook: "llm_input", sessionId: event.sessionId,
|
|
678
|
+
model: event.model, reason: result.summary,
|
|
679
|
+
preview: text.slice(0, 100), ts: new Date().toISOString(),
|
|
680
|
+
};
|
|
681
|
+
auditLog(entry);
|
|
682
|
+
loadConfig()
|
|
683
|
+
// Pass the local entry as-is; cloudSync strips the input preview/content
|
|
684
|
+
// before transmit (metadata-only egress). No raw LLM input leaves here.
|
|
685
|
+
.then(cfg => cloudSync(entry, cfg))
|
|
686
|
+
.catch(() => {});
|
|
606
687
|
}
|
|
607
|
-
} catch (e) {
|
|
608
|
-
console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
|
|
609
688
|
}
|
|
610
|
-
}
|
|
689
|
+
} catch (e) {
|
|
690
|
+
console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function handleLlmInput(event: LlmInputEvent, ctx: AgentCtx): void {
|
|
695
|
+
// Fire and forget
|
|
696
|
+
void scanLlmInput(event, ctx);
|
|
611
697
|
}
|
|
612
698
|
|
|
613
699
|
// Skip text blocks that are ShieldCortex/OpenClaw tool-result pass-throughs
|
|
@@ -713,16 +799,13 @@ export default {
|
|
|
713
799
|
|
|
714
800
|
if (!interceptorConfig.enabled) return null;
|
|
715
801
|
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
} catch (importErr) {
|
|
724
|
-
// Stack overflow or missing module — interceptor can't load
|
|
725
|
-
(api.logger as any)?.warn?.(`[shieldcortex] Cannot load defence module: ${importErr instanceof Error ? importErr.message : importErr}`);
|
|
802
|
+
// Shared in-process defence module (same instance realtime scanning
|
|
803
|
+
// uses — see getDefenceModule). Loaded via a string-concatenated
|
|
804
|
+
// specifier so TypeScript doesn't resolve 'shieldcortex/defence' at
|
|
805
|
+
// compile time; it only exists at runtime once the package is installed.
|
|
806
|
+
const defenceMod = await getDefenceModule();
|
|
807
|
+
if (!defenceMod) {
|
|
808
|
+
(api.logger as any)?.warn?.('[shieldcortex] Cannot load defence module — interceptor disabled');
|
|
726
809
|
return null;
|
|
727
810
|
}
|
|
728
811
|
if (typeof defenceMod.runDefencePipeline !== 'function') return null;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.32.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