shieldcortex 2.10.10 → 2.11.1
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 +215 -327
- package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
- package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dist/setup/openclaw.d.ts +2 -1
- package/dist/setup/openclaw.d.ts.map +1 -1
- package/dist/setup/openclaw.js +105 -9
- package/dist/setup/openclaw.js.map +1 -1
- package/package.json +3 -2
- package/plugins/openclaw/README.md +69 -0
- package/plugins/openclaw/dist/index.js +228 -0
- package/plugins/openclaw/dist/openclaw.plugin.json +10 -0
- package/plugins/openclaw/index.ts +242 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{DxzDBnK5-_-dsNSUzsP-Q → YQLi2N9vG_BugYszi86eT}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{DxzDBnK5-_-dsNSUzsP-Q → YQLi2N9vG_BugYszi86eT}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{DxzDBnK5-_-dsNSUzsP-Q → YQLi2N9vG_BugYszi86eT}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.2.15+
|
|
3
|
+
*
|
|
4
|
+
* Hooks into llm_input/llm_output for real-time defence scanning
|
|
5
|
+
* and memory extraction. All operations are fire-and-forget.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
let _config = null;
|
|
12
|
+
async function loadConfig() {
|
|
13
|
+
if (_config)
|
|
14
|
+
return _config;
|
|
15
|
+
try {
|
|
16
|
+
_config = JSON.parse(await fs.readFile(path.join(homedir(), ".shieldcortex", "config.json"), "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
_config = {};
|
|
20
|
+
}
|
|
21
|
+
return _config;
|
|
22
|
+
}
|
|
23
|
+
// ==================== SERVER CMD ====================
|
|
24
|
+
let _serverCmd = null;
|
|
25
|
+
async function resolveServerCmd() {
|
|
26
|
+
if (_serverCmd)
|
|
27
|
+
return _serverCmd;
|
|
28
|
+
const config = await loadConfig();
|
|
29
|
+
if (config.binaryPath) {
|
|
30
|
+
try {
|
|
31
|
+
await fs.access(config.binaryPath);
|
|
32
|
+
return (_serverCmd = config.binaryPath);
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const { execFileSync } = await import("node:child_process");
|
|
38
|
+
const bin = execFileSync("which", ["shieldcortex"], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
39
|
+
if (bin)
|
|
40
|
+
return (_serverCmd = bin);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return (_serverCmd = "npx -y shieldcortex");
|
|
44
|
+
}
|
|
45
|
+
// ==================== MCP HELPER ====================
|
|
46
|
+
function callCortex(tool, args = {}) {
|
|
47
|
+
return resolveServerCmd().then(serverCmd => new Promise(resolve => {
|
|
48
|
+
const cmdArgs = ["mcporter", "call", "--stdio", serverCmd, tool];
|
|
49
|
+
for (const [k, v] of Object.entries(args))
|
|
50
|
+
cmdArgs.push(`${k}:${String(v).replace(/'/g, "''")}`);
|
|
51
|
+
execFile("npx", cmdArgs, { timeout: 15000, maxBuffer: 256 * 1024 }, (err, stdout) => {
|
|
52
|
+
resolve(err ? null : stdout?.trim() || null);
|
|
53
|
+
});
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
// ==================== DEFENCE PIPELINE ====================
|
|
57
|
+
let _pipeline = null;
|
|
58
|
+
async function getPipeline() {
|
|
59
|
+
if (_pipeline)
|
|
60
|
+
return _pipeline;
|
|
61
|
+
try {
|
|
62
|
+
const mod = await import("shieldcortex/defence");
|
|
63
|
+
_pipeline = mod.runDefencePipeline;
|
|
64
|
+
return _pipeline;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ==================== CONTENT PATTERNS ====================
|
|
71
|
+
const PATTERNS = {
|
|
72
|
+
architecture: [/\b(?:architecture|designed|structured)\b.*?(?:uses?|is|with)\b/i, /\b(?:decided?\s+to|going\s+with|chose)\b/i],
|
|
73
|
+
error: [/\b(?:fixed|resolved|solved)\s+(?:by|with|using)\b/i, /\b(?:solution|fix|root\s*cause)\s+(?:was|is)\b/i],
|
|
74
|
+
learning: [/\b(?:learned|discovered|turns?\s+out|figured\s+out|realized)\b/i],
|
|
75
|
+
preference: [/\b(?:always|never|prefer|should\s+always)\b/i],
|
|
76
|
+
note: [/\b(?:important|remember|key\s+point)\s*:/i],
|
|
77
|
+
};
|
|
78
|
+
function extractMemories(texts) {
|
|
79
|
+
const out = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const text of texts) {
|
|
82
|
+
if (text.length < 30)
|
|
83
|
+
continue;
|
|
84
|
+
for (const [cat, pats] of Object.entries(PATTERNS)) {
|
|
85
|
+
if (pats.some(p => p.test(text))) {
|
|
86
|
+
const title = text.slice(0, 80).replace(/["\n]/g, " ").trim();
|
|
87
|
+
if (!seen.has(title)) {
|
|
88
|
+
seen.add(title);
|
|
89
|
+
out.push({ title, content: text.slice(0, 500), category: cat });
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (out.length >= 3)
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (out.length >= 3)
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
// ==================== HELPERS ====================
|
|
102
|
+
function extractUserContent(msgs) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const msg of msgs) {
|
|
105
|
+
if (!msg || typeof msg !== "object")
|
|
106
|
+
continue;
|
|
107
|
+
const m = msg;
|
|
108
|
+
if (m.role !== "user")
|
|
109
|
+
continue;
|
|
110
|
+
if (typeof m.content === "string")
|
|
111
|
+
out.push(m.content);
|
|
112
|
+
else if (Array.isArray(m.content))
|
|
113
|
+
for (const b of m.content)
|
|
114
|
+
if (b?.type === "text")
|
|
115
|
+
out.push(b.text);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const AUDIT_DIR = path.join(homedir(), ".shieldcortex", "audit");
|
|
120
|
+
async function auditLog(entry) {
|
|
121
|
+
try {
|
|
122
|
+
await fs.mkdir(AUDIT_DIR, { recursive: true });
|
|
123
|
+
await fs.appendFile(path.join(AUDIT_DIR, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`), JSON.stringify(entry) + "\n");
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
}
|
|
127
|
+
async function cloudSync(threat) {
|
|
128
|
+
const cfg = await loadConfig();
|
|
129
|
+
if (!cfg.cloudApiKey)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
await fetch(`${cfg.cloudEndpoint || "https://api.shieldcortex.ai"}/v1/threats`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.cloudApiKey}` },
|
|
135
|
+
body: JSON.stringify(threat),
|
|
136
|
+
signal: AbortSignal.timeout(5000),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
}
|
|
141
|
+
// ==================== HOOK HANDLERS ====================
|
|
142
|
+
// Skip scanning internal OpenClaw content (boot checks, system prompts, heartbeats)
|
|
143
|
+
const SKIP_PATTERNS = [
|
|
144
|
+
/^You are running a boot check/i,
|
|
145
|
+
/^Read HEARTBEAT\.md/i,
|
|
146
|
+
/^System:/,
|
|
147
|
+
/^\[System Message\]/,
|
|
148
|
+
/^HEARTBEAT_OK$/,
|
|
149
|
+
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s/, // Timestamped system events
|
|
150
|
+
/^A subagent task/i,
|
|
151
|
+
/subagent.*completed/i,
|
|
152
|
+
];
|
|
153
|
+
function isInternalContent(text) {
|
|
154
|
+
return SKIP_PATTERNS.some(p => p.test(text.trim()));
|
|
155
|
+
}
|
|
156
|
+
function handleLlmInput(event, ctx) {
|
|
157
|
+
// Fire and forget
|
|
158
|
+
(async () => {
|
|
159
|
+
try {
|
|
160
|
+
const pipeline = await getPipeline();
|
|
161
|
+
if (!pipeline)
|
|
162
|
+
return;
|
|
163
|
+
// Only scan user content, skip system/boot/heartbeat prompts
|
|
164
|
+
const userTexts = extractUserContent(event.historyMessages).slice(-5);
|
|
165
|
+
const texts = [event.prompt, ...userTexts].filter(t => t && !isInternalContent(t));
|
|
166
|
+
for (const text of texts) {
|
|
167
|
+
if (!text || text.length < 10)
|
|
168
|
+
continue;
|
|
169
|
+
const result = pipeline(text, "llm_input", { type: "plugin", name: "openclaw-realtime", trust: "medium" });
|
|
170
|
+
if (result && !result.allowed) {
|
|
171
|
+
console.warn(`[shieldcortex] ⚠️ Threat in LLM input: ${result.reason}`);
|
|
172
|
+
const entry = {
|
|
173
|
+
type: "threat", hook: "llm_input", sessionId: event.sessionId,
|
|
174
|
+
model: event.model, reason: result.reason,
|
|
175
|
+
preview: text.slice(0, 100), ts: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
auditLog(entry);
|
|
178
|
+
cloudSync({ ...entry, content: text.slice(0, 200) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
function handleLlmOutput(event, ctx) {
|
|
188
|
+
// Fire and forget
|
|
189
|
+
(async () => {
|
|
190
|
+
try {
|
|
191
|
+
const texts = event.assistantTexts.filter(t => t && t.length >= 30);
|
|
192
|
+
if (!texts.length)
|
|
193
|
+
return;
|
|
194
|
+
const memories = extractMemories(texts);
|
|
195
|
+
if (!memories.length)
|
|
196
|
+
return;
|
|
197
|
+
let saved = 0;
|
|
198
|
+
for (const mem of memories) {
|
|
199
|
+
const r = await callCortex("remember", {
|
|
200
|
+
title: mem.title, content: mem.content, category: mem.category,
|
|
201
|
+
project: ctx.agentId || "openclaw", scope: "global",
|
|
202
|
+
importance: "normal", tags: "auto-extracted,realtime-plugin,llm-output",
|
|
203
|
+
});
|
|
204
|
+
if (r)
|
|
205
|
+
saved++;
|
|
206
|
+
}
|
|
207
|
+
if (saved) {
|
|
208
|
+
console.log(`[shieldcortex] Extracted ${saved} memor${saved === 1 ? "y" : "ies"} from LLM output`);
|
|
209
|
+
auditLog({ type: "memory", hook: "llm_output", sessionId: event.sessionId, count: saved, ts: new Date().toISOString() });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
console.error("[shieldcortex] llm_output error:", e instanceof Error ? e.message : String(e));
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
}
|
|
217
|
+
// ==================== PLUGIN EXPORT ====================
|
|
218
|
+
export default {
|
|
219
|
+
id: "shieldcortex-realtime",
|
|
220
|
+
name: "ShieldCortex Real-time Scanner",
|
|
221
|
+
description: "Real-time defence scanning on LLM inputs and memory extraction from outputs",
|
|
222
|
+
version: "2.11.0",
|
|
223
|
+
register(api) {
|
|
224
|
+
api.on("llm_input", handleLlmInput);
|
|
225
|
+
api.on("llm_output", handleLlmOutput);
|
|
226
|
+
api.logger.info("[shieldcortex] Real-time scanning plugin registered (llm_input + llm_output)");
|
|
227
|
+
},
|
|
228
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "shieldcortex-realtime",
|
|
3
|
+
"name": "ShieldCortex Real-time Scanner",
|
|
4
|
+
"description": "Real-time defence scanning on LLM input and memory extraction on LLM output.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.2.15+
|
|
3
|
+
*
|
|
4
|
+
* Hooks into llm_input/llm_output for real-time defence scanning
|
|
5
|
+
* and memory extraction. All operations are fire-and-forget.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
|
|
13
|
+
// ==================== TYPES (inline to avoid import issues) ====================
|
|
14
|
+
|
|
15
|
+
type LlmInputEvent = {
|
|
16
|
+
runId: string; sessionId: string; provider: string; model: string;
|
|
17
|
+
systemPrompt?: string; prompt: string; historyMessages: unknown[]; imagesCount: number;
|
|
18
|
+
};
|
|
19
|
+
type LlmOutputEvent = {
|
|
20
|
+
runId: string; sessionId: string; provider: string; model: string;
|
|
21
|
+
assistantTexts: string[]; lastAssistant?: unknown;
|
|
22
|
+
usage?: { input?: number; output?: number; total?: number };
|
|
23
|
+
};
|
|
24
|
+
type AgentCtx = {
|
|
25
|
+
agentId?: string; sessionKey?: string; sessionId?: string;
|
|
26
|
+
workspaceDir?: string; messageProvider?: string;
|
|
27
|
+
};
|
|
28
|
+
type PluginApi = {
|
|
29
|
+
id: string; name: string; logger: { info: (m: string) => void };
|
|
30
|
+
on: (hook: string, handler: (...args: any[]) => any) => void;
|
|
31
|
+
[k: string]: any;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ==================== CONFIG ====================
|
|
35
|
+
|
|
36
|
+
interface SCConfig { cloudApiKey?: string; cloudEndpoint?: string; binaryPath?: string }
|
|
37
|
+
let _config: SCConfig | null = null;
|
|
38
|
+
|
|
39
|
+
async function loadConfig(): Promise<SCConfig> {
|
|
40
|
+
if (_config) return _config;
|
|
41
|
+
try {
|
|
42
|
+
_config = JSON.parse(await fs.readFile(path.join(homedir(), ".shieldcortex", "config.json"), "utf-8"));
|
|
43
|
+
} catch { _config = {}; }
|
|
44
|
+
return _config!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ==================== SERVER CMD ====================
|
|
48
|
+
|
|
49
|
+
let _serverCmd: string | null = null;
|
|
50
|
+
async function resolveServerCmd(): Promise<string> {
|
|
51
|
+
if (_serverCmd) return _serverCmd;
|
|
52
|
+
const config = await loadConfig();
|
|
53
|
+
if (config.binaryPath) { try { await fs.access(config.binaryPath); return (_serverCmd = config.binaryPath); } catch {} }
|
|
54
|
+
try {
|
|
55
|
+
const { execFileSync } = await import("node:child_process");
|
|
56
|
+
const bin = execFileSync("which", ["shieldcortex"], { encoding: "utf-8", timeout: 3000 }).trim();
|
|
57
|
+
if (bin) return (_serverCmd = bin);
|
|
58
|
+
} catch {}
|
|
59
|
+
return (_serverCmd = "npx -y shieldcortex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ==================== MCP HELPER ====================
|
|
63
|
+
|
|
64
|
+
function callCortex(tool: string, args: Record<string, string> = {}): Promise<string | null> {
|
|
65
|
+
return resolveServerCmd().then(serverCmd => new Promise(resolve => {
|
|
66
|
+
const cmdArgs = ["mcporter", "call", "--stdio", serverCmd, tool];
|
|
67
|
+
for (const [k, v] of Object.entries(args)) cmdArgs.push(`${k}:${String(v).replace(/'/g, "''")}`);
|
|
68
|
+
execFile("npx", cmdArgs, { timeout: 15000, maxBuffer: 256 * 1024 }, (err, stdout) => {
|
|
69
|
+
resolve(err ? null : stdout?.trim() || null);
|
|
70
|
+
});
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ==================== DEFENCE PIPELINE ====================
|
|
75
|
+
|
|
76
|
+
let _pipeline: ((content: string, title: string, source: any, config?: any, project?: string) => any) | null = null;
|
|
77
|
+
|
|
78
|
+
async function getPipeline() {
|
|
79
|
+
if (_pipeline) return _pipeline;
|
|
80
|
+
try {
|
|
81
|
+
const mod = await import("shieldcortex/defence");
|
|
82
|
+
_pipeline = mod.runDefencePipeline;
|
|
83
|
+
return _pipeline;
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ==================== CONTENT PATTERNS ====================
|
|
88
|
+
|
|
89
|
+
const PATTERNS: Record<string, RegExp[]> = {
|
|
90
|
+
architecture: [/\b(?:architecture|designed|structured)\b.*?(?:uses?|is|with)\b/i, /\b(?:decided?\s+to|going\s+with|chose)\b/i],
|
|
91
|
+
error: [/\b(?:fixed|resolved|solved)\s+(?:by|with|using)\b/i, /\b(?:solution|fix|root\s*cause)\s+(?:was|is)\b/i],
|
|
92
|
+
learning: [/\b(?:learned|discovered|turns?\s+out|figured\s+out|realized)\b/i],
|
|
93
|
+
preference: [/\b(?:always|never|prefer|should\s+always)\b/i],
|
|
94
|
+
note: [/\b(?:important|remember|key\s+point)\s*:/i],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function extractMemories(texts: string[]): Array<{ title: string; content: string; category: string }> {
|
|
98
|
+
const out: Array<{ title: string; content: string; category: string }> = [];
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
for (const text of texts) {
|
|
101
|
+
if (text.length < 30) continue;
|
|
102
|
+
for (const [cat, pats] of Object.entries(PATTERNS)) {
|
|
103
|
+
if (pats.some(p => p.test(text))) {
|
|
104
|
+
const title = text.slice(0, 80).replace(/["\n]/g, " ").trim();
|
|
105
|
+
if (!seen.has(title)) { seen.add(title); out.push({ title, content: text.slice(0, 500), category: cat }); }
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (out.length >= 3) break;
|
|
109
|
+
}
|
|
110
|
+
if (out.length >= 3) break;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ==================== HELPERS ====================
|
|
116
|
+
|
|
117
|
+
function extractUserContent(msgs: unknown[]): string[] {
|
|
118
|
+
const out: string[] = [];
|
|
119
|
+
for (const msg of msgs) {
|
|
120
|
+
if (!msg || typeof msg !== "object") continue;
|
|
121
|
+
const m = msg as any;
|
|
122
|
+
if (m.role !== "user") continue;
|
|
123
|
+
if (typeof m.content === "string") out.push(m.content);
|
|
124
|
+
else if (Array.isArray(m.content)) for (const b of m.content) if (b?.type === "text") out.push(b.text);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const AUDIT_DIR = path.join(homedir(), ".shieldcortex", "audit");
|
|
130
|
+
|
|
131
|
+
async function auditLog(entry: Record<string, unknown>) {
|
|
132
|
+
try {
|
|
133
|
+
await fs.mkdir(AUDIT_DIR, { recursive: true });
|
|
134
|
+
await fs.appendFile(
|
|
135
|
+
path.join(AUDIT_DIR, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`),
|
|
136
|
+
JSON.stringify(entry) + "\n",
|
|
137
|
+
);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function cloudSync(threat: Record<string, unknown>) {
|
|
142
|
+
const cfg = await loadConfig();
|
|
143
|
+
if (!cfg.cloudApiKey) return;
|
|
144
|
+
try {
|
|
145
|
+
await fetch(`${cfg.cloudEndpoint || "https://api.shieldcortex.ai"}/v1/threats`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.cloudApiKey}` },
|
|
148
|
+
body: JSON.stringify(threat),
|
|
149
|
+
signal: AbortSignal.timeout(5000),
|
|
150
|
+
});
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ==================== HOOK HANDLERS ====================
|
|
155
|
+
|
|
156
|
+
// Skip scanning internal OpenClaw content (boot checks, system prompts, heartbeats)
|
|
157
|
+
const SKIP_PATTERNS = [
|
|
158
|
+
/^You are running a boot check/i,
|
|
159
|
+
/^Read HEARTBEAT\.md/i,
|
|
160
|
+
/^System:/,
|
|
161
|
+
/^\[System Message\]/,
|
|
162
|
+
/^HEARTBEAT_OK$/,
|
|
163
|
+
/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s/, // Timestamped system events
|
|
164
|
+
/^A subagent task/i,
|
|
165
|
+
/subagent.*completed/i,
|
|
166
|
+
];
|
|
167
|
+
function isInternalContent(text: string): boolean {
|
|
168
|
+
return SKIP_PATTERNS.some(p => p.test(text.trim()));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleLlmInput(event: LlmInputEvent, ctx: AgentCtx): void {
|
|
172
|
+
// Fire and forget
|
|
173
|
+
(async () => {
|
|
174
|
+
try {
|
|
175
|
+
const pipeline = await getPipeline();
|
|
176
|
+
if (!pipeline) return;
|
|
177
|
+
|
|
178
|
+
// Only scan user content, skip system/boot/heartbeat prompts
|
|
179
|
+
const userTexts = extractUserContent(event.historyMessages).slice(-5);
|
|
180
|
+
const texts = [event.prompt, ...userTexts].filter(t => t && !isInternalContent(t));
|
|
181
|
+
for (const text of texts) {
|
|
182
|
+
if (!text || text.length < 10) continue;
|
|
183
|
+
const result = pipeline(text, "llm_input", { type: "plugin", name: "openclaw-realtime", trust: "medium" });
|
|
184
|
+
if (result && !result.allowed) {
|
|
185
|
+
console.warn(`[shieldcortex] ⚠️ Threat in LLM input: ${result.reason}`);
|
|
186
|
+
const entry = {
|
|
187
|
+
type: "threat", hook: "llm_input", sessionId: event.sessionId,
|
|
188
|
+
model: event.model, reason: result.reason,
|
|
189
|
+
preview: text.slice(0, 100), ts: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
auditLog(entry);
|
|
192
|
+
cloudSync({ ...entry, content: text.slice(0, 200) });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
|
|
197
|
+
}
|
|
198
|
+
})();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function handleLlmOutput(event: LlmOutputEvent, ctx: AgentCtx): void {
|
|
202
|
+
// Fire and forget
|
|
203
|
+
(async () => {
|
|
204
|
+
try {
|
|
205
|
+
const texts = event.assistantTexts.filter(t => t && t.length >= 30);
|
|
206
|
+
if (!texts.length) return;
|
|
207
|
+
const memories = extractMemories(texts);
|
|
208
|
+
if (!memories.length) return;
|
|
209
|
+
|
|
210
|
+
let saved = 0;
|
|
211
|
+
for (const mem of memories) {
|
|
212
|
+
const r = await callCortex("remember", {
|
|
213
|
+
title: mem.title, content: mem.content, category: mem.category,
|
|
214
|
+
project: ctx.agentId || "openclaw", scope: "global",
|
|
215
|
+
importance: "normal", tags: "auto-extracted,realtime-plugin,llm-output",
|
|
216
|
+
});
|
|
217
|
+
if (r) saved++;
|
|
218
|
+
}
|
|
219
|
+
if (saved) {
|
|
220
|
+
console.log(`[shieldcortex] Extracted ${saved} memor${saved === 1 ? "y" : "ies"} from LLM output`);
|
|
221
|
+
auditLog({ type: "memory", hook: "llm_output", sessionId: event.sessionId, count: saved, ts: new Date().toISOString() });
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error("[shieldcortex] llm_output error:", e instanceof Error ? e.message : String(e));
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ==================== PLUGIN EXPORT ====================
|
|
230
|
+
|
|
231
|
+
export default {
|
|
232
|
+
id: "shieldcortex-realtime",
|
|
233
|
+
name: "ShieldCortex Real-time Scanner",
|
|
234
|
+
description: "Real-time defence scanning on LLM inputs and memory extraction from outputs",
|
|
235
|
+
version: "2.11.0",
|
|
236
|
+
|
|
237
|
+
register(api: PluginApi) {
|
|
238
|
+
api.on("llm_input", handleLlmInput);
|
|
239
|
+
api.on("llm_output", handleLlmOutput);
|
|
240
|
+
api.logger.info("[shieldcortex] Real-time scanning plugin registered (llm_input + llm_output)");
|
|
241
|
+
},
|
|
242
|
+
};
|
|
File without changes
|
|
File without changes
|