@drakon-systems/shieldcortex-realtime 3.4.4 → 4.8.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/README.md +1 -1
- package/index.ts +161 -43
- package/intercept-ingest.ts +28 -0
- package/interceptor.ts +461 -0
- package/openclaw.plugin.json +43 -2
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ openclaw plugins install @drakon-systems/shieldcortex-realtime
|
|
|
32
32
|
If you also want the companion session hook, install it from the main package:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
openclaw
|
|
35
|
+
openclaw skills install shieldcortex
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Restart OpenClaw after installing:
|
package/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.
|
|
2
|
+
* ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.3.22+
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and optional memory extraction.
|
|
4
|
+
* Uses explicit capability registration (registerHook + registerCommand)
|
|
5
|
+
* for llm_input/llm_output scanning and optional memory extraction.
|
|
6
|
+
* All scanning operations are fire-and-forget.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import fs from "node:fs/promises";
|
|
11
11
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -13,6 +13,10 @@ import path from "node:path";
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
|
|
16
|
+
import { createInterceptor, DEFAULT_CONFIG as DEFAULT_INTERCEPTOR_CONFIG } from './interceptor.js';
|
|
17
|
+
import type { InterceptorConfig, ToolCallContext } from './interceptor.js';
|
|
18
|
+
import { syncInterceptEvent } from './intercept-ingest.js';
|
|
19
|
+
|
|
16
20
|
// ==================== RESILIENT RUNTIME LOADER ====================
|
|
17
21
|
// Resolves runtime.mjs from multiple locations so the plugin works both
|
|
18
22
|
// inside the npm package tree AND when copied to ~/.openclaw/extensions/
|
|
@@ -48,49 +52,43 @@ function collectRuntimeCandidates(): string[] {
|
|
|
48
52
|
// 1. Relative path (works when running from within npm package tree)
|
|
49
53
|
candidates.add(new URL("../../hooks/openclaw/cortex-memory/runtime.mjs", import.meta.url).href);
|
|
50
54
|
|
|
51
|
-
// 2.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// 2. Config file override (reads path from ~/.shieldcortex/config.json instead of env var)
|
|
56
|
+
try {
|
|
57
|
+
const cfgPath = path.join(homedir(), ".shieldcortex", "config.json");
|
|
58
|
+
if (existsSync(cfgPath)) {
|
|
59
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
60
|
+
if (cfg.installRoot) addRuntimeCandidate(candidates, cfg.installRoot);
|
|
61
|
+
}
|
|
62
|
+
} catch { /* no config */ }
|
|
55
63
|
|
|
56
64
|
// 3. Walk up from current file location
|
|
57
65
|
addAncestorCandidates(candidates, path.dirname(fileURLToPath(import.meta.url)));
|
|
58
66
|
|
|
59
|
-
// 4.
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
} catch {}
|
|
67
|
-
|
|
68
|
-
// 5. npm global root
|
|
69
|
-
try {
|
|
70
|
-
const npmRoot = execFileSync("npm", ["root", "-g"], {
|
|
71
|
-
encoding: "utf-8",
|
|
72
|
-
timeout: 3000,
|
|
73
|
-
}).trim();
|
|
74
|
-
if (npmRoot) addRuntimeCandidate(candidates, path.join(npmRoot, "shieldcortex"));
|
|
75
|
-
} catch {}
|
|
76
|
-
|
|
77
|
-
// 6. npm prefix
|
|
78
|
-
try {
|
|
79
|
-
const prefix = execFileSync("npm", ["config", "get", "prefix"], {
|
|
80
|
-
encoding: "utf-8",
|
|
81
|
-
timeout: 3000,
|
|
82
|
-
}).trim();
|
|
83
|
-
if (prefix) addRuntimeCandidate(candidates, path.join(prefix, "lib", "node_modules", "shieldcortex"));
|
|
84
|
-
} catch {}
|
|
67
|
+
// 4. Resolve via common bin symlink paths (no child_process needed)
|
|
68
|
+
for (const binDir of ["/usr/local/bin", "/opt/homebrew/bin", path.join(homedir(), ".npm-global", "bin")]) {
|
|
69
|
+
const binPath = path.join(binDir, "shieldcortex");
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(binPath)) addAncestorCandidates(candidates, realpathSync(binPath));
|
|
72
|
+
} catch { /* broken symlink */ }
|
|
73
|
+
}
|
|
85
74
|
|
|
86
|
-
//
|
|
75
|
+
// 5. Common global install paths (covers npm root -g results without spawning npm)
|
|
87
76
|
for (const root of [
|
|
88
77
|
"/usr/lib/node_modules/shieldcortex",
|
|
89
78
|
"/usr/local/lib/node_modules/shieldcortex",
|
|
90
79
|
"/opt/homebrew/lib/node_modules/shieldcortex",
|
|
91
80
|
path.join(homedir(), ".npm-global", "lib", "node_modules", "shieldcortex"),
|
|
81
|
+
path.join(homedir(), ".nvm", "versions", "node"), // nvm users
|
|
92
82
|
]) {
|
|
93
|
-
|
|
83
|
+
if (root.includes(".nvm")) {
|
|
84
|
+
// For nvm, check the current symlink
|
|
85
|
+
try {
|
|
86
|
+
const currentNode = path.join(homedir(), ".nvm", "current", "lib", "node_modules", "shieldcortex");
|
|
87
|
+
addRuntimeCandidate(candidates, currentNode);
|
|
88
|
+
} catch { /* no nvm */ }
|
|
89
|
+
} else {
|
|
90
|
+
addRuntimeCandidate(candidates, root);
|
|
91
|
+
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
return [...candidates];
|
|
@@ -214,14 +212,16 @@ let _config: SCConfig | null = null;
|
|
|
214
212
|
let _configOverride: SCConfig | null = null;
|
|
215
213
|
let _version = "0.0.0";
|
|
216
214
|
try {
|
|
217
|
-
|
|
215
|
+
// Try package.json first, then openclaw.plugin.json (the manifest IS copied to extensions/)
|
|
216
|
+
for (const candidateUrl of [
|
|
218
217
|
new URL("./package.json", import.meta.url),
|
|
219
218
|
new URL("../../package.json", import.meta.url),
|
|
219
|
+
new URL("./openclaw.plugin.json", import.meta.url),
|
|
220
220
|
]) {
|
|
221
221
|
try {
|
|
222
|
-
const
|
|
223
|
-
if (typeof
|
|
224
|
-
_version =
|
|
222
|
+
const data = JSON.parse(readFileSync(candidateUrl, "utf-8"));
|
|
223
|
+
if (typeof data.version === "string" && data.version.trim()) {
|
|
224
|
+
_version = data.version;
|
|
225
225
|
break;
|
|
226
226
|
}
|
|
227
227
|
} catch {
|
|
@@ -230,6 +230,8 @@ try {
|
|
|
230
230
|
}
|
|
231
231
|
} catch { /* fallback */ }
|
|
232
232
|
|
|
233
|
+
let _registered = false;
|
|
234
|
+
|
|
233
235
|
function normaliseConfig(raw: unknown): SCConfig {
|
|
234
236
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
235
237
|
|
|
@@ -674,9 +676,125 @@ export default {
|
|
|
674
676
|
},
|
|
675
677
|
|
|
676
678
|
register(api: PluginApi) {
|
|
679
|
+
if (_registered) return;
|
|
680
|
+
_registered = true;
|
|
681
|
+
try {
|
|
677
682
|
applyPluginConfigOverride(api);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
683
|
+
|
|
684
|
+
// --- Interceptor (lazy init) ---
|
|
685
|
+
let interceptorReady: ReturnType<typeof createInterceptor> | null = null;
|
|
686
|
+
let interceptorInitAttempted = false;
|
|
687
|
+
|
|
688
|
+
async function initInterceptor(): Promise<ReturnType<typeof createInterceptor> | null> {
|
|
689
|
+
if (interceptorInitAttempted) return interceptorReady;
|
|
690
|
+
interceptorInitAttempted = true;
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const scConfig = await loadConfig();
|
|
694
|
+
const rawInterceptorConfig = (scConfig as any).interceptor;
|
|
695
|
+
const interceptorConfig: InterceptorConfig = {
|
|
696
|
+
...DEFAULT_INTERCEPTOR_CONFIG,
|
|
697
|
+
...(rawInterceptorConfig && typeof rawInterceptorConfig === 'object' ? {
|
|
698
|
+
enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
|
|
699
|
+
severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
|
|
700
|
+
failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
|
|
701
|
+
} : {}),
|
|
702
|
+
logger: { info: api.logger?.info ?? console.log, warn: (api.logger as any)?.warn ?? console.warn },
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
if (!interceptorConfig.enabled) return null;
|
|
706
|
+
|
|
707
|
+
// Dynamic import with string variable to prevent TypeScript from resolving
|
|
708
|
+
// at compile time — 'shieldcortex/defence' only exists at runtime when the
|
|
709
|
+
// package is installed globally, not during CI builds of the plugin itself.
|
|
710
|
+
let defenceMod: any;
|
|
711
|
+
try {
|
|
712
|
+
const defenceModPath = 'shieldcortex' + '/defence';
|
|
713
|
+
defenceMod = await import(/* webpackIgnore: true */ defenceModPath);
|
|
714
|
+
} catch (importErr) {
|
|
715
|
+
// Stack overflow or missing module — interceptor can't load
|
|
716
|
+
(api.logger as any)?.warn?.(`[shieldcortex] Cannot load defence module: ${importErr instanceof Error ? importErr.message : importErr}`);
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
if (typeof defenceMod.runDefencePipeline !== 'function') return null;
|
|
720
|
+
|
|
721
|
+
interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline as Parameters<typeof createInterceptor>[1], {
|
|
722
|
+
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
723
|
+
cloudApiKey: (scConfig as any).cloudApiKey ?? '',
|
|
724
|
+
cloudBaseUrl: (scConfig as any).cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
|
725
|
+
cloudEnabled: (scConfig as any).cloudEnabled ?? false,
|
|
726
|
+
}),
|
|
727
|
+
});
|
|
728
|
+
api.logger?.info?.('[shieldcortex] Interceptor active — watching: remember, mcp__memory__remember');
|
|
729
|
+
return interceptorReady;
|
|
730
|
+
} catch (err) {
|
|
731
|
+
(api.logger as any)?.warn?.(`[shieldcortex] Interceptor init failed: ${err instanceof Error ? err.message : err}`);
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Register before_tool_call with lazy-init wrapper
|
|
737
|
+
api.registerHook('before_tool_call', async (context: ToolCallContext) => {
|
|
738
|
+
const interceptor = await initInterceptor();
|
|
739
|
+
if (!interceptor) return;
|
|
740
|
+
try {
|
|
741
|
+
await interceptor.handleToolCall(context);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
// Intentional blocks from the interceptor (ShieldCortex: ...) should propagate
|
|
744
|
+
if (err instanceof Error && err.message.startsWith('ShieldCortex:')) throw err;
|
|
745
|
+
// Unexpected errors (DB crash, etc.) — log and allow the tool call through
|
|
746
|
+
(api.logger as any)?.warn?.(`[shieldcortex] Interceptor error (allowing tool call): ${err instanceof Error ? err.message : err}`);
|
|
747
|
+
}
|
|
748
|
+
}, {
|
|
749
|
+
name: 'shieldcortex-intercept-tool',
|
|
750
|
+
description: 'Active threat gating on tool calls',
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Try to register session_end for cache cleanup
|
|
754
|
+
try {
|
|
755
|
+
api.registerHook('session_end', () => { interceptorReady?.resetSession(); }, {
|
|
756
|
+
name: 'shieldcortex-session-cleanup',
|
|
757
|
+
description: 'Clear interceptor deny cache on session end',
|
|
758
|
+
});
|
|
759
|
+
} catch {
|
|
760
|
+
// session_end may not be a supported hook — TTL safety net handles this
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Explicit capability registration (replaces legacy api.on)
|
|
764
|
+
api.registerHook("llm_input", handleLlmInput, {
|
|
765
|
+
name: "shieldcortex-scan-input",
|
|
766
|
+
description: "Real-time threat scanning on LLM input",
|
|
767
|
+
});
|
|
768
|
+
api.registerHook("llm_output", handleLlmOutput, {
|
|
769
|
+
name: "shieldcortex-scan-output",
|
|
770
|
+
description: "Memory extraction from LLM output",
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Register a lightweight status command so the plugin is not hook-only
|
|
774
|
+
api.registerCommand({
|
|
775
|
+
name: "shieldcortex-status",
|
|
776
|
+
description: "Show ShieldCortex real-time scanner status",
|
|
777
|
+
async handler() {
|
|
778
|
+
const cfg = await loadConfig();
|
|
779
|
+
const autoMemory = isAutoMemoryEnabled(cfg) ? "on" : "off";
|
|
780
|
+
const dedupe = isAutoMemoryDedupeEnabled(cfg) ? "on" : "off";
|
|
781
|
+
const cloud = cfg.cloudApiKey ? "configured" : "not configured";
|
|
782
|
+
return {
|
|
783
|
+
text:
|
|
784
|
+
`ShieldCortex v${_version}\n` +
|
|
785
|
+
` Hooks: llm_input (scan), llm_output (memory)\n` +
|
|
786
|
+
` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
|
|
787
|
+
` Cloud sync: ${cloud}`,
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + before_tool_call + /shieldcortex-status)`);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
// Plugin must never block channel startup — warn and bail gracefully
|
|
795
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
796
|
+
console.warn(`[shieldcortex] WARNING: Plugin failed to initialize: ${msg}`);
|
|
797
|
+
console.warn('[shieldcortex] Real-time scanning is disabled. Channels will start normally.');
|
|
798
|
+
}
|
|
681
799
|
},
|
|
682
800
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// plugins/openclaw/intercept-ingest.ts
|
|
2
|
+
import type { InterceptAuditEntry } from './interceptor.js';
|
|
3
|
+
|
|
4
|
+
interface CloudConfig {
|
|
5
|
+
cloudApiKey: string;
|
|
6
|
+
cloudBaseUrl: string;
|
|
7
|
+
cloudEnabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function syncInterceptEvent(event: InterceptAuditEntry, config: CloudConfig): void {
|
|
11
|
+
if (!config.cloudEnabled || !config.cloudApiKey) return;
|
|
12
|
+
|
|
13
|
+
const url = `${config.cloudBaseUrl}/v1/audit/ingest`;
|
|
14
|
+
|
|
15
|
+
fetch(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
events: [{ ...event, source: 'openclaw-interceptor' }],
|
|
23
|
+
}),
|
|
24
|
+
signal: AbortSignal.timeout(5_000),
|
|
25
|
+
}).catch(() => {
|
|
26
|
+
// Fire-and-forget — never block on cloud sync failure
|
|
27
|
+
});
|
|
28
|
+
}
|
package/interceptor.ts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
export type Severity = 'low' | 'medium' | 'high' | 'critical';
|
|
7
|
+
export type InterceptAction = 'log' | 'warn' | 'require_approval';
|
|
8
|
+
export type FailureAction = 'allow' | 'deny';
|
|
9
|
+
|
|
10
|
+
export interface InterceptorConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
severityActions: Record<Severity, InterceptAction>;
|
|
13
|
+
failurePolicy: Record<Severity, FailureAction>;
|
|
14
|
+
logger?: { info: (msg: string) => void; warn: (msg: string) => void };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolCallContext {
|
|
18
|
+
toolName: string;
|
|
19
|
+
arguments: Record<string, unknown>;
|
|
20
|
+
requireApproval?: (message: string) => Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InterceptAuditEntry {
|
|
24
|
+
type: 'intercept';
|
|
25
|
+
tool: string;
|
|
26
|
+
severity: Severity;
|
|
27
|
+
firewallResult: string;
|
|
28
|
+
threats: string[];
|
|
29
|
+
anomalyScore: number;
|
|
30
|
+
action: InterceptAction | 'auto_deny' | 'rate_limit';
|
|
31
|
+
outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied';
|
|
32
|
+
preview: string;
|
|
33
|
+
ts: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const WATCHED_TOOLS = ['remember', 'mcp__memory__remember'] as const;
|
|
37
|
+
|
|
38
|
+
const CONTENT_FIELDS: Record<string, string[]> = {
|
|
39
|
+
remember: ['content', 'title'],
|
|
40
|
+
mcp__memory__remember: ['content', 'title'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CONFIG: InterceptorConfig = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
severityActions: {
|
|
46
|
+
low: 'log',
|
|
47
|
+
medium: 'warn',
|
|
48
|
+
high: 'require_approval',
|
|
49
|
+
critical: 'require_approval',
|
|
50
|
+
},
|
|
51
|
+
failurePolicy: {
|
|
52
|
+
low: 'allow',
|
|
53
|
+
medium: 'allow',
|
|
54
|
+
high: 'deny',
|
|
55
|
+
critical: 'deny',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
|
|
60
|
+
|
|
61
|
+
export function extractContent(toolName: string, args: Record<string, unknown>): { title: string; content: string } {
|
|
62
|
+
const fields = CONTENT_FIELDS[toolName];
|
|
63
|
+
if (!fields) return { title: '', content: '' };
|
|
64
|
+
const title = typeof args.title === 'string' ? args.title : '';
|
|
65
|
+
const content = typeof args.content === 'string' ? args.content : '';
|
|
66
|
+
return { title, content };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface FirewallResult {
|
|
70
|
+
result: 'ALLOW' | 'BLOCK' | 'QUARANTINE';
|
|
71
|
+
anomalyScore: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function mapSeverity(firewall: FirewallResult): Severity {
|
|
75
|
+
if (firewall.result === 'BLOCK') return 'critical';
|
|
76
|
+
if (firewall.result === 'QUARANTINE') return 'high';
|
|
77
|
+
if (firewall.result === 'ALLOW' && firewall.anomalyScore >= 0.3) return 'medium';
|
|
78
|
+
return 'low';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Deny Cache ---
|
|
82
|
+
|
|
83
|
+
// Exact replica of normalizeMemoryText() from index.ts (lines 426-434).
|
|
84
|
+
// Must produce identical output for SHA-256 hash consistency.
|
|
85
|
+
function normaliseContent(text: string): string {
|
|
86
|
+
return String(text || '')
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[`"'\\]/g, ' ')
|
|
89
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
90
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
91
|
+
.replace(/\s+/g, ' ')
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hashContent(text: string): string {
|
|
96
|
+
return createHash('sha256').update(normaliseContent(text)).digest('hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface DenyCacheEntry {
|
|
100
|
+
hash: string;
|
|
101
|
+
ts: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
|
105
|
+
|
|
106
|
+
export class DenyCache {
|
|
107
|
+
private cache = new Map<string, DenyCacheEntry[]>();
|
|
108
|
+
private maxPerTool: number;
|
|
109
|
+
private ttlMs: number;
|
|
110
|
+
|
|
111
|
+
constructor(maxPerTool = 200, ttlMs = TWO_HOURS_MS) {
|
|
112
|
+
this.maxPerTool = maxPerTool;
|
|
113
|
+
this.ttlMs = ttlMs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isDenied(tool: string, content: string): boolean {
|
|
117
|
+
const entries = this.cache.get(tool);
|
|
118
|
+
if (!entries) return false;
|
|
119
|
+
const hash = hashContent(content);
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
return entries.some(e => e.hash === hash && (now - e.ts) < this.ttlMs);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
addDenial(tool: string, content: string): void {
|
|
125
|
+
const hash = hashContent(content);
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (!this.cache.has(tool)) {
|
|
128
|
+
this.cache.set(tool, []);
|
|
129
|
+
}
|
|
130
|
+
const entries = this.cache.get(tool)!;
|
|
131
|
+
const live = entries.filter(e => (now - e.ts) < this.ttlMs);
|
|
132
|
+
if (live.some(e => e.hash === hash)) return;
|
|
133
|
+
live.push({ hash, ts: now });
|
|
134
|
+
while (live.length > this.maxPerTool) {
|
|
135
|
+
live.shift();
|
|
136
|
+
}
|
|
137
|
+
this.cache.set(tool, live);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
reset(): void {
|
|
141
|
+
this.cache.clear();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Rate Limiter ---
|
|
146
|
+
|
|
147
|
+
export class RateLimiter {
|
|
148
|
+
private timestamps: number[] = [];
|
|
149
|
+
private maxPerWindow: number;
|
|
150
|
+
private windowMs: number;
|
|
151
|
+
|
|
152
|
+
constructor(maxPerWindow = 5, windowMs = 60_000) {
|
|
153
|
+
this.maxPerWindow = maxPerWindow;
|
|
154
|
+
this.windowMs = windowMs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
shouldAllow(): boolean {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
this.timestamps = this.timestamps.filter(t => now - t < this.windowMs);
|
|
160
|
+
if (this.timestamps.length >= this.maxPerWindow) return false;
|
|
161
|
+
this.timestamps.push(now);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Approval Prompt ---
|
|
167
|
+
|
|
168
|
+
interface ApprovalPromptInput {
|
|
169
|
+
tool: string;
|
|
170
|
+
severity: Severity;
|
|
171
|
+
firewallResult: string;
|
|
172
|
+
threats: string[];
|
|
173
|
+
content: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function formatApprovalPrompt(input: ApprovalPromptInput): string {
|
|
177
|
+
const preview = input.content.length > 200
|
|
178
|
+
? input.content.slice(0, 200) + '...'
|
|
179
|
+
: input.content;
|
|
180
|
+
const threatList = input.threats.length > 0
|
|
181
|
+
? input.threats.join(', ')
|
|
182
|
+
: 'none identified';
|
|
183
|
+
|
|
184
|
+
return [
|
|
185
|
+
'🛡️ ShieldCortex — Tool Call Intercepted',
|
|
186
|
+
'',
|
|
187
|
+
`Tool: ${input.tool}`,
|
|
188
|
+
`Risk: ${input.severity} (${input.firewallResult})`,
|
|
189
|
+
`Threats: ${threatList}`,
|
|
190
|
+
`Content: "${preview}"`,
|
|
191
|
+
'',
|
|
192
|
+
'[Approve] [Deny]',
|
|
193
|
+
].join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Audit Logging (local JSONL) ---
|
|
197
|
+
|
|
198
|
+
const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
|
|
199
|
+
|
|
200
|
+
function writeAuditEntry(entry: InterceptAuditEntry): void {
|
|
201
|
+
try {
|
|
202
|
+
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
203
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
204
|
+
const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
|
|
205
|
+
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
206
|
+
} catch {
|
|
207
|
+
// Best-effort — never block on audit failure
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- X-Ray Inline Guard ---
|
|
212
|
+
// Lightweight inline version of xrayMemoryContent for the plugin build boundary.
|
|
213
|
+
// Detects AI directive injection patterns in memory content.
|
|
214
|
+
|
|
215
|
+
const XRAY_AI_DIRECTIVE_PATTERNS: RegExp[] = [
|
|
216
|
+
/ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|context)/i,
|
|
217
|
+
/disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|rules?)/i,
|
|
218
|
+
/override\s+(previous|prior|all)\s+(instructions?|rules?|constraints?)/i,
|
|
219
|
+
/you\s+are\s+now\s+(?:in\s+)?(?:developer|god|admin|root|unrestricted)\s+mode/i,
|
|
220
|
+
/enter\s+(?:developer|god|admin|DAN|jailbreak)\s+mode/i,
|
|
221
|
+
/(?:system|hidden|secret)\s*(?:prompt|instruction|directive)\s*:/i,
|
|
222
|
+
/\[SYSTEM\]\s*:/i,
|
|
223
|
+
/\[INST\]/i,
|
|
224
|
+
/<\|(?:system|user|assistant|im_start|im_end)\|>/i,
|
|
225
|
+
/(?:decode|execute|follow)\s+(?:the\s+)?hidden\s+(?:instructions?|payload|message)/i,
|
|
226
|
+
/(?:hidden|embedded|encoded)\s+(?:instructions?|directive|command)\s+(?:in|within|inside)/i,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const XRAY_FILENAME_PATTERNS: RegExp[] = [
|
|
230
|
+
/ignore_previous/i, /decode_hidden/i, /execute_instructions/i,
|
|
231
|
+
/override_previous/i, /developer_mode/i, /system_prompt/i,
|
|
232
|
+
/jailbreak/i, /\[SYSTEM\]/i, /\[INST\]/i,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
interface XRayGuardResult {
|
|
236
|
+
allowed: boolean;
|
|
237
|
+
findings: Array<{ category: string; title: string; severity: string }>;
|
|
238
|
+
riskLevel: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function xrayMemoryGuard(content: string, title?: string): XRayGuardResult {
|
|
242
|
+
const findings: Array<{ category: string; title: string; severity: string }> = [];
|
|
243
|
+
const text = content.length > 50000 ? content.slice(0, 50000) : content;
|
|
244
|
+
|
|
245
|
+
for (const pattern of XRAY_AI_DIRECTIVE_PATTERNS) {
|
|
246
|
+
if (pattern.test(text)) {
|
|
247
|
+
findings.push({ category: 'ai-directive', title: 'AI directive injection detected', severity: 'critical' });
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (title) {
|
|
253
|
+
for (const pattern of XRAY_FILENAME_PATTERNS) {
|
|
254
|
+
if (pattern.test(title)) {
|
|
255
|
+
findings.push({ category: 'ai-directive', title: 'AI directive in title', severity: 'critical' });
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Score: 100 - 60 per critical finding (single critical = blocked)
|
|
262
|
+
const score = Math.max(0, 100 - findings.length * 60);
|
|
263
|
+
const riskLevel = score >= 80 ? 'SAFE' : score >= 60 ? 'LOW' : score >= 40 ? 'MEDIUM' : score >= 20 ? 'HIGH' : 'CRITICAL';
|
|
264
|
+
return { allowed: score >= 60, findings, riskLevel };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Interceptor Factory ---
|
|
268
|
+
|
|
269
|
+
type PipelineRunner = (content: string, title: string, source: { type: string; identifier: string }) => {
|
|
270
|
+
allowed: boolean;
|
|
271
|
+
firewall: {
|
|
272
|
+
result: 'ALLOW' | 'BLOCK' | 'QUARANTINE';
|
|
273
|
+
reason: string;
|
|
274
|
+
threatIndicators: string[];
|
|
275
|
+
anomalyScore: number;
|
|
276
|
+
blockedPatterns: string[];
|
|
277
|
+
};
|
|
278
|
+
auditId: number;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
interface InterceptorOptions {
|
|
282
|
+
maxPromptsPerMinute?: number;
|
|
283
|
+
onAuditEntry?: (entry: InterceptAuditEntry) => void;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function createInterceptor(
|
|
287
|
+
config: InterceptorConfig,
|
|
288
|
+
pipeline: PipelineRunner,
|
|
289
|
+
options?: InterceptorOptions,
|
|
290
|
+
): {
|
|
291
|
+
handleToolCall: (context: ToolCallContext) => Promise<void>;
|
|
292
|
+
resetSession: () => void;
|
|
293
|
+
} {
|
|
294
|
+
const denyCache = new DenyCache();
|
|
295
|
+
const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
|
|
296
|
+
const log = config.logger ?? { info: console.log, warn: console.warn };
|
|
297
|
+
const onAuditEntry = options?.onAuditEntry;
|
|
298
|
+
|
|
299
|
+
function emitAudit(entry: InterceptAuditEntry): void {
|
|
300
|
+
writeAuditEntry(entry);
|
|
301
|
+
onAuditEntry?.(entry);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function handleToolCall(context: ToolCallContext): Promise<void> {
|
|
305
|
+
if (!(WATCHED_TOOLS as readonly string[]).includes(context.toolName)) return;
|
|
306
|
+
|
|
307
|
+
const { title, content } = extractContent(context.toolName, context.arguments);
|
|
308
|
+
const fullContent = [title, content].filter(Boolean).join(' ');
|
|
309
|
+
if (!fullContent.trim()) return;
|
|
310
|
+
|
|
311
|
+
// X-Ray content scan — fast, synchronous, no I/O
|
|
312
|
+
const xrayResult = xrayMemoryGuard(content, title || undefined);
|
|
313
|
+
if (!xrayResult.allowed) {
|
|
314
|
+
const xrayEntry: InterceptAuditEntry = {
|
|
315
|
+
type: 'intercept', tool: context.toolName, severity: 'critical',
|
|
316
|
+
firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
|
|
317
|
+
anomalyScore: 1, action: 'auto_deny', outcome: 'auto_denied',
|
|
318
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
319
|
+
};
|
|
320
|
+
emitAudit(xrayEntry);
|
|
321
|
+
throw new Error(`ShieldCortex: tool call blocked by X-Ray memory guard (risk: ${xrayResult.riskLevel}, findings: ${xrayResult.findings.length})`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let severity: Severity;
|
|
325
|
+
let firewallResult: string;
|
|
326
|
+
let threats: string[];
|
|
327
|
+
let anomalyScore: number;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
|
|
331
|
+
severity = mapSeverity(result.firewall);
|
|
332
|
+
firewallResult = result.firewall.result;
|
|
333
|
+
threats = result.firewall.threatIndicators;
|
|
334
|
+
anomalyScore = result.firewall.anomalyScore;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
|
|
337
|
+
const failAction = config.failurePolicy.high;
|
|
338
|
+
const entry: InterceptAuditEntry = {
|
|
339
|
+
type: 'intercept', tool: context.toolName, severity: 'high',
|
|
340
|
+
firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
|
|
341
|
+
action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
342
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
343
|
+
};
|
|
344
|
+
emitAudit(entry);
|
|
345
|
+
if (failAction === 'deny') {
|
|
346
|
+
throw new Error('ShieldCortex: tool call blocked — pipeline error, failure policy: deny');
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (denyCache.isDenied(context.toolName, fullContent)) {
|
|
352
|
+
const entry: InterceptAuditEntry = {
|
|
353
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
354
|
+
threats, anomalyScore, action: 'auto_deny', outcome: 'auto_denied',
|
|
355
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
emitAudit(entry);
|
|
358
|
+
throw new Error('ShieldCortex: tool call auto-denied (previously denied content)');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const action = config.severityActions[severity];
|
|
362
|
+
|
|
363
|
+
if (action === 'log') {
|
|
364
|
+
const entry: InterceptAuditEntry = {
|
|
365
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
366
|
+
threats, anomalyScore, action: 'log', outcome: 'logged',
|
|
367
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
368
|
+
};
|
|
369
|
+
emitAudit(entry);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (action === 'warn') {
|
|
374
|
+
log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
|
|
375
|
+
const entry: InterceptAuditEntry = {
|
|
376
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
377
|
+
threats, anomalyScore, action: 'warn', outcome: 'warned',
|
|
378
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
379
|
+
};
|
|
380
|
+
emitAudit(entry);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// action === 'require_approval'
|
|
385
|
+
if (typeof context.requireApproval !== 'function') {
|
|
386
|
+
// requireApproval unavailable (pre-v2026.3.28) — apply failurePolicy, not blanket allow
|
|
387
|
+
const failAction = config.failurePolicy[severity];
|
|
388
|
+
log.warn(`[shieldcortex] ⚠️ requireApproval not available for ${severity} risk in ${context.toolName} — failure policy: ${failAction}`);
|
|
389
|
+
const entry: InterceptAuditEntry = {
|
|
390
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
391
|
+
threats, anomalyScore, action: 'require_approval',
|
|
392
|
+
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
393
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
394
|
+
};
|
|
395
|
+
emitAudit(entry);
|
|
396
|
+
if (failAction === 'deny') {
|
|
397
|
+
throw new Error(`ShieldCortex: tool call blocked — requireApproval unavailable, failure policy: deny`);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!rateLimiter.shouldAllow()) {
|
|
403
|
+
log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
|
|
404
|
+
const entry: InterceptAuditEntry = {
|
|
405
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
406
|
+
threats, anomalyScore, action: 'rate_limit', outcome: 'auto_denied',
|
|
407
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
408
|
+
};
|
|
409
|
+
emitAudit(entry);
|
|
410
|
+
denyCache.addDenial(context.toolName, fullContent);
|
|
411
|
+
throw new Error('ShieldCortex: tool call auto-denied (rate limit exceeded)');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const message = formatApprovalPrompt({ tool: context.toolName, severity, firewallResult, threats, content: fullContent });
|
|
415
|
+
|
|
416
|
+
let approved: boolean;
|
|
417
|
+
try {
|
|
418
|
+
approved = await context.requireApproval(message);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const failAction = config.failurePolicy[severity];
|
|
421
|
+
log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
|
|
422
|
+
const entry: InterceptAuditEntry = {
|
|
423
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
424
|
+
threats, anomalyScore, action: 'require_approval',
|
|
425
|
+
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
426
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
427
|
+
};
|
|
428
|
+
emitAudit(entry);
|
|
429
|
+
if (failAction === 'deny') {
|
|
430
|
+
throw new Error(`ShieldCortex: tool call blocked — requireApproval error, failure policy: deny`);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (approved) {
|
|
436
|
+
const entry: InterceptAuditEntry = {
|
|
437
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
438
|
+
threats, anomalyScore, action: 'require_approval', outcome: 'approved',
|
|
439
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
440
|
+
};
|
|
441
|
+
emitAudit(entry);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Denied
|
|
446
|
+
denyCache.addDenial(context.toolName, fullContent);
|
|
447
|
+
const entry: InterceptAuditEntry = {
|
|
448
|
+
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
449
|
+
threats, anomalyScore, action: 'require_approval', outcome: 'denied',
|
|
450
|
+
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
451
|
+
};
|
|
452
|
+
emitAudit(entry);
|
|
453
|
+
throw new Error('ShieldCortex: tool call denied by user');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resetSession(): void {
|
|
457
|
+
denyCache.reset();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { handleToolCall, resetSession };
|
|
461
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
|
-
"description": "Real-time defence scanning on LLM input
|
|
5
|
+
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"uiHints": {
|
|
7
7
|
"binaryPath": {
|
|
8
8
|
"label": "ShieldCortex Binary Path",
|
|
@@ -40,6 +40,21 @@
|
|
|
40
40
|
"label": "Recent Memory Cache Size",
|
|
41
41
|
"help": "How many recent extracted memories to keep in the dedupe cache.",
|
|
42
42
|
"advanced": true
|
|
43
|
+
},
|
|
44
|
+
"interceptor.enabled": {
|
|
45
|
+
"label": "Enable Tool Call Interceptor",
|
|
46
|
+
"description": "Scan memory-write tool calls and gate suspicious content behind user approval",
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
49
|
+
"interceptor.severityActions.high": {
|
|
50
|
+
"label": "High Severity Action",
|
|
51
|
+
"description": "Action for high-severity threats (log, warn, require_approval)",
|
|
52
|
+
"type": "string"
|
|
53
|
+
},
|
|
54
|
+
"interceptor.severityActions.critical": {
|
|
55
|
+
"label": "Critical Severity Action",
|
|
56
|
+
"description": "Action for critical-severity threats (log, warn, require_approval)",
|
|
57
|
+
"type": "string"
|
|
43
58
|
}
|
|
44
59
|
},
|
|
45
60
|
"configSchema": {
|
|
@@ -73,6 +88,32 @@
|
|
|
73
88
|
"type": "integer",
|
|
74
89
|
"minimum": 50,
|
|
75
90
|
"maximum": 1000
|
|
91
|
+
},
|
|
92
|
+
"interceptor": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"enabled": { "type": "boolean", "default": true },
|
|
96
|
+
"severityActions": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"properties": {
|
|
100
|
+
"low": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "log" },
|
|
101
|
+
"medium": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "warn" },
|
|
102
|
+
"high": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" },
|
|
103
|
+
"critical": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"failurePolicy": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"low": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
|
|
111
|
+
"medium": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
|
|
112
|
+
"high": { "type": "string", "enum": ["allow", "deny"], "default": "deny" },
|
|
113
|
+
"critical": { "type": "string", "enum": ["allow", "deny"], "default": "deny" }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
76
117
|
}
|
|
77
118
|
}
|
|
78
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakon-systems/shieldcortex-realtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
],
|
|
16
16
|
"files": [
|
|
17
17
|
"index.ts",
|
|
18
|
+
"interceptor.ts",
|
|
19
|
+
"intercept-ingest.ts",
|
|
18
20
|
"openclaw.plugin.json",
|
|
19
21
|
"README.md"
|
|
20
22
|
],
|
|
@@ -22,7 +24,10 @@
|
|
|
22
24
|
"pack:verify": "npm pack --dry-run"
|
|
23
25
|
},
|
|
24
26
|
"peerDependencies": {
|
|
25
|
-
"shieldcortex": "
|
|
27
|
+
"shieldcortex": "^4.6.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
26
31
|
},
|
|
27
32
|
"publishConfig": {
|
|
28
33
|
"access": "public"
|