shieldcortex 2.16.2 → 2.17.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 +322 -465
- 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/page_client-reference-manifest.js +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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- 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 +2 -2
- 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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_25b1b286._.js +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/dashboard/.next/standalone/dashboard/.next/static/chunks/{1bf33aa1c01418e1.js → 61a3c89b08347bc2.js} +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/c252c4de65df6d09.css +3 -0
- package/dist/api/visualization-server.d.ts.map +1 -1
- package/dist/api/visualization-server.js +30 -2
- package/dist/api/visualization-server.js.map +1 -1
- package/dist/cloud/cli.d.ts.map +1 -1
- package/dist/cloud/cli.js +21 -1
- package/dist/cloud/cli.js.map +1 -1
- package/dist/cloud/config.d.ts +23 -0
- package/dist/cloud/config.d.ts.map +1 -1
- package/dist/cloud/config.js +57 -0
- package/dist/cloud/config.js.map +1 -1
- package/dist/defence/__tests__/pipeline.test.js +45 -1
- package/dist/defence/__tests__/pipeline.test.js.map +1 -1
- package/dist/defence/index.d.ts +2 -2
- package/dist/defence/index.d.ts.map +1 -1
- package/dist/defence/index.js +1 -1
- package/dist/defence/index.js.map +1 -1
- package/dist/defence/pipeline.d.ts.map +1 -1
- package/dist/defence/pipeline.js +4 -0
- package/dist/defence/pipeline.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/__tests__/openclaw.test.d.ts +2 -0
- package/dist/integrations/__tests__/openclaw.test.d.ts.map +1 -0
- package/dist/integrations/__tests__/openclaw.test.js +72 -0
- package/dist/integrations/__tests__/openclaw.test.js.map +1 -0
- package/dist/integrations/__tests__/universal.test.d.ts +2 -0
- package/dist/integrations/__tests__/universal.test.d.ts.map +1 -0
- package/dist/integrations/__tests__/universal.test.js +144 -0
- package/dist/integrations/__tests__/universal.test.js.map +1 -0
- package/dist/integrations/index.d.ts +3 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/openclaw.d.ts +38 -0
- package/dist/integrations/openclaw.d.ts.map +1 -0
- package/dist/integrations/openclaw.js +169 -0
- package/dist/integrations/openclaw.js.map +1 -0
- package/dist/integrations/universal.d.ts +62 -0
- package/dist/integrations/universal.d.ts.map +1 -0
- package/dist/integrations/universal.js +100 -0
- package/dist/integrations/universal.js.map +1 -0
- package/dist/lib.d.ts +3 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +3 -1
- package/dist/lib.js.map +1 -1
- package/dist/setup/openclaw.d.ts.map +1 -1
- package/dist/setup/openclaw.js +3 -2
- package/dist/setup/openclaw.js.map +1 -1
- package/dist/setup/status.d.ts.map +1 -1
- package/dist/setup/status.js +34 -0
- package/dist/setup/status.js.map +1 -1
- package/dist/setup/uninstall.d.ts.map +1 -1
- package/dist/setup/uninstall.js +6 -1
- package/dist/setup/uninstall.js.map +1 -1
- package/hooks/openclaw/cortex-memory/HOOK.md +32 -2
- package/hooks/openclaw/cortex-memory/handler.ts +213 -10
- package/package.json +9 -1
- package/plugins/openclaw/README.md +38 -2
- package/plugins/openclaw/dist/index.js +145 -5
- package/plugins/openclaw/index.ts +183 -6
- package/scripts/postinstall.mjs +34 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/bccda52164e63171.css +0 -3
- /package/dashboard/.next/standalone/dashboard/.next/static/{z7V0ywXg56o1kWteW7hN0 → H-BGC5Yp6YmPEZGryV6bd}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{z7V0ywXg56o1kWteW7hN0 → H-BGC5Yp6YmPEZGryV6bd}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{z7V0ywXg56o1kWteW7hN0 → H-BGC5Yp6YmPEZGryV6bd}/_ssgManifest.js +0 -0
|
@@ -7,11 +7,34 @@
|
|
|
7
7
|
* - Keyword-triggered memory saves
|
|
8
8
|
*/
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import fs from "node:fs/promises";
|
|
12
|
+
import { homedir } from "node:os";
|
|
11
13
|
import path from "node:path";
|
|
12
14
|
|
|
13
15
|
// ==================== SERVER COMMAND RESOLUTION ====================
|
|
14
16
|
|
|
17
|
+
let _shieldConfig = null;
|
|
18
|
+
let _autoMemoryNoticeShown = false;
|
|
19
|
+
|
|
20
|
+
async function loadShieldConfig() {
|
|
21
|
+
if (_shieldConfig) return _shieldConfig;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const configPath = path.join(homedir(), ".shieldcortex", "config.json");
|
|
25
|
+
_shieldConfig = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
26
|
+
} catch {
|
|
27
|
+
_shieldConfig = {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return _shieldConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function isOpenClawAutoMemoryEnabled() {
|
|
34
|
+
const config = await loadShieldConfig();
|
|
35
|
+
return config?.openclawAutoMemory === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
/**
|
|
16
39
|
* Resolve the fastest way to invoke shieldcortex:
|
|
17
40
|
* 1. ~/.shieldcortex/config.json "binaryPath" override
|
|
@@ -23,14 +46,10 @@ let _resolvedServerCmd = null;
|
|
|
23
46
|
async function resolveServerCmd() {
|
|
24
47
|
if (_resolvedServerCmd) return _resolvedServerCmd;
|
|
25
48
|
|
|
26
|
-
const path = await import("node:path");
|
|
27
|
-
const { homedir } = await import("node:os");
|
|
28
|
-
|
|
29
49
|
// 1. Check config for explicit binaryPath
|
|
30
50
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (config.binaryPath) {
|
|
51
|
+
const config = await loadShieldConfig();
|
|
52
|
+
if (config?.binaryPath) {
|
|
34
53
|
await fs.access(config.binaryPath);
|
|
35
54
|
_resolvedServerCmd = config.binaryPath;
|
|
36
55
|
console.log(`[cortex-memory] Using configured binary: ${config.binaryPath}`);
|
|
@@ -115,6 +134,150 @@ async function callCortex(tool, args = {}, options = { retries: 1, timeout: 1500
|
|
|
115
134
|
});
|
|
116
135
|
}
|
|
117
136
|
|
|
137
|
+
// ==================== NOVELTY / DEDUPE GATE ====================
|
|
138
|
+
|
|
139
|
+
const NOVELTY_CACHE_FILE = path.join(homedir(), ".shieldcortex", "openclaw-memory-cache.json");
|
|
140
|
+
const DEFAULT_NOVELTY_THRESHOLD = 0.88;
|
|
141
|
+
const DEFAULT_MAX_RECENT = 300;
|
|
142
|
+
const MIN_NOVELTY_CHARS = 40;
|
|
143
|
+
|
|
144
|
+
function normalizeMemoryText(text) {
|
|
145
|
+
return String(text || "")
|
|
146
|
+
.toLowerCase()
|
|
147
|
+
.replace(/[`"'\\]/g, " ")
|
|
148
|
+
.replace(/https?:\/\/\S+/g, " ")
|
|
149
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
150
|
+
.replace(/\s+/g, " ")
|
|
151
|
+
.trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hashToken(token) {
|
|
155
|
+
return createHash("sha1").update(token).digest("hex").slice(0, 12);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildTokenHashes(normalized) {
|
|
159
|
+
const words = normalized.split(" ").filter((w) => w.length >= 3);
|
|
160
|
+
const set = new Set();
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < words.length; i++) {
|
|
163
|
+
set.add(hashToken(words[i]));
|
|
164
|
+
if (i < words.length - 1) {
|
|
165
|
+
set.add(hashToken(`${words[i]}_${words[i + 1]}`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Array.from(set).slice(0, 200);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function jaccardSimilarity(a, b) {
|
|
173
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
174
|
+
let intersection = 0;
|
|
175
|
+
for (const item of a) {
|
|
176
|
+
if (b.has(item)) intersection++;
|
|
177
|
+
}
|
|
178
|
+
const union = a.size + b.size - intersection;
|
|
179
|
+
return union === 0 ? 0 : intersection / union;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function clamp(value, min, max) {
|
|
183
|
+
return Math.max(min, Math.min(max, value));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function getNoveltyConfig() {
|
|
187
|
+
const config = await loadShieldConfig();
|
|
188
|
+
const rawThreshold = Number(config?.openclawAutoMemoryNoveltyThreshold);
|
|
189
|
+
const rawMaxRecent = Number(config?.openclawAutoMemoryMaxRecent);
|
|
190
|
+
return {
|
|
191
|
+
enabled: config?.openclawAutoMemoryDedupe !== false,
|
|
192
|
+
threshold: Number.isFinite(rawThreshold)
|
|
193
|
+
? clamp(rawThreshold, 0.6, 0.99)
|
|
194
|
+
: DEFAULT_NOVELTY_THRESHOLD,
|
|
195
|
+
maxRecent: Number.isFinite(rawMaxRecent)
|
|
196
|
+
? Math.floor(clamp(rawMaxRecent, 50, 1000))
|
|
197
|
+
: DEFAULT_MAX_RECENT,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function loadNoveltyCache(maxRecent) {
|
|
202
|
+
try {
|
|
203
|
+
const raw = JSON.parse(await fs.readFile(NOVELTY_CACHE_FILE, "utf-8"));
|
|
204
|
+
if (!Array.isArray(raw)) return [];
|
|
205
|
+
return raw
|
|
206
|
+
.filter((entry) => entry && typeof entry.hash === "string" && Array.isArray(entry.tokenHashes))
|
|
207
|
+
.slice(0, maxRecent);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function saveNoveltyCache(entries) {
|
|
214
|
+
await fs.mkdir(path.dirname(NOVELTY_CACHE_FILE), { recursive: true });
|
|
215
|
+
await fs.writeFile(NOVELTY_CACHE_FILE, JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function inspectNovelty(content, entries, threshold) {
|
|
219
|
+
const normalized = normalizeMemoryText(content);
|
|
220
|
+
if (normalized.length < MIN_NOVELTY_CHARS) {
|
|
221
|
+
return { allow: true, normalized, contentHash: null, tokenHashes: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const contentHash = createHash("sha256").update(normalized).digest("hex").slice(0, 24);
|
|
225
|
+
if (entries.some((entry) => entry.hash === contentHash)) {
|
|
226
|
+
return { allow: false, normalized, contentHash, tokenHashes: [], reason: "exact duplicate" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tokenHashes = buildTokenHashes(normalized);
|
|
230
|
+
const currentSet = new Set(tokenHashes);
|
|
231
|
+
let bestSimilarity = 0;
|
|
232
|
+
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
const score = jaccardSimilarity(currentSet, new Set(entry.tokenHashes || []));
|
|
235
|
+
if (score > bestSimilarity) bestSimilarity = score;
|
|
236
|
+
if (score >= threshold) {
|
|
237
|
+
return {
|
|
238
|
+
allow: false,
|
|
239
|
+
normalized,
|
|
240
|
+
contentHash,
|
|
241
|
+
tokenHashes,
|
|
242
|
+
reason: `near duplicate (similarity ${score.toFixed(2)})`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { allow: true, normalized, contentHash, tokenHashes, bestSimilarity };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function createNoveltyGate() {
|
|
251
|
+
const cfg = await getNoveltyConfig();
|
|
252
|
+
const entries = cfg.enabled ? await loadNoveltyCache(cfg.maxRecent) : [];
|
|
253
|
+
let dirty = false;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
enabled: cfg.enabled,
|
|
257
|
+
inspect(content) {
|
|
258
|
+
if (!cfg.enabled) return { allow: true, reason: null };
|
|
259
|
+
return inspectNovelty(content, entries, cfg.threshold);
|
|
260
|
+
},
|
|
261
|
+
remember(memory, novelty) {
|
|
262
|
+
if (!cfg.enabled) return;
|
|
263
|
+
if (!novelty?.contentHash || !Array.isArray(novelty?.tokenHashes)) return;
|
|
264
|
+
entries.unshift({
|
|
265
|
+
hash: novelty.contentHash,
|
|
266
|
+
tokenHashes: novelty.tokenHashes,
|
|
267
|
+
title: String(memory?.title || "").slice(0, 120),
|
|
268
|
+
category: String(memory?.category || "note"),
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
if (entries.length > cfg.maxRecent) entries.length = cfg.maxRecent;
|
|
272
|
+
dirty = true;
|
|
273
|
+
},
|
|
274
|
+
async flush() {
|
|
275
|
+
if (!cfg.enabled || !dirty) return;
|
|
276
|
+
await saveNoveltyCache(entries);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
118
281
|
// ==================== HOOK SCANNER ====================
|
|
119
282
|
|
|
120
283
|
/**
|
|
@@ -282,6 +445,14 @@ async function getRecentMessages(sessionFilePath) {
|
|
|
282
445
|
* Handle command:new — extract memories from ending session
|
|
283
446
|
*/
|
|
284
447
|
async function onSessionEnd(event) {
|
|
448
|
+
if (!(await isOpenClawAutoMemoryEnabled())) {
|
|
449
|
+
if (!_autoMemoryNoticeShown) {
|
|
450
|
+
console.log("[cortex-memory] Auto memory extraction disabled (set openclawAutoMemory=true to enable)");
|
|
451
|
+
_autoMemoryNoticeShown = true;
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
285
456
|
const context = event.context || {};
|
|
286
457
|
const sessionEntry = context.previousSessionEntry || context.sessionEntry || {};
|
|
287
458
|
const sessionFile = sessionEntry.sessionFile;
|
|
@@ -303,8 +474,16 @@ async function onSessionEnd(event) {
|
|
|
303
474
|
return;
|
|
304
475
|
}
|
|
305
476
|
|
|
477
|
+
const noveltyGate = await createNoveltyGate();
|
|
306
478
|
let saved = 0;
|
|
479
|
+
let skipped = 0;
|
|
307
480
|
for (const mem of memories) {
|
|
481
|
+
const novelty = noveltyGate.inspect(mem.content);
|
|
482
|
+
if (!novelty.allow) {
|
|
483
|
+
skipped++;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
308
487
|
const result = await callCortex("remember", {
|
|
309
488
|
title: mem.title,
|
|
310
489
|
content: mem.content,
|
|
@@ -314,10 +493,14 @@ async function onSessionEnd(event) {
|
|
|
314
493
|
importance: "high",
|
|
315
494
|
tags: "auto-extracted,openclaw-hook",
|
|
316
495
|
});
|
|
317
|
-
if (result)
|
|
496
|
+
if (result) {
|
|
497
|
+
saved++;
|
|
498
|
+
noveltyGate.remember(mem, novelty);
|
|
499
|
+
}
|
|
318
500
|
}
|
|
501
|
+
await noveltyGate.flush();
|
|
319
502
|
|
|
320
|
-
console.log(`[cortex-memory] Saved ${saved}/${memories.length} memories from session`);
|
|
503
|
+
console.log(`[cortex-memory] Saved ${saved}/${memories.length} memories from session (${skipped} skipped as duplicates)`);
|
|
321
504
|
|
|
322
505
|
// Provide visible feedback to user
|
|
323
506
|
if (saved > 0 && event.messages) {
|
|
@@ -330,6 +513,14 @@ async function onSessionEnd(event) {
|
|
|
330
513
|
* This fires when user explicitly calls /stop
|
|
331
514
|
*/
|
|
332
515
|
async function onSessionStop(event) {
|
|
516
|
+
if (!(await isOpenClawAutoMemoryEnabled())) {
|
|
517
|
+
if (!_autoMemoryNoticeShown) {
|
|
518
|
+
console.log("[cortex-memory] Auto memory extraction disabled (set openclawAutoMemory=true to enable)");
|
|
519
|
+
_autoMemoryNoticeShown = true;
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
333
524
|
const context = event.context || {};
|
|
334
525
|
const sessionEntry = context.sessionEntry || {};
|
|
335
526
|
const sessionFile = sessionEntry.sessionFile;
|
|
@@ -351,8 +542,16 @@ async function onSessionStop(event) {
|
|
|
351
542
|
return;
|
|
352
543
|
}
|
|
353
544
|
|
|
545
|
+
const noveltyGate = await createNoveltyGate();
|
|
354
546
|
let saved = 0;
|
|
547
|
+
let skipped = 0;
|
|
355
548
|
for (const mem of memories) {
|
|
549
|
+
const novelty = noveltyGate.inspect(mem.content);
|
|
550
|
+
if (!novelty.allow) {
|
|
551
|
+
skipped++;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
356
555
|
const result = await callCortex("remember", {
|
|
357
556
|
title: mem.title,
|
|
358
557
|
content: mem.content,
|
|
@@ -362,10 +561,14 @@ async function onSessionStop(event) {
|
|
|
362
561
|
importance: "high",
|
|
363
562
|
tags: "auto-extracted,openclaw-hook,session-stop",
|
|
364
563
|
});
|
|
365
|
-
if (result)
|
|
564
|
+
if (result) {
|
|
565
|
+
saved++;
|
|
566
|
+
noveltyGate.remember(mem, novelty);
|
|
567
|
+
}
|
|
366
568
|
}
|
|
569
|
+
await noveltyGate.flush();
|
|
367
570
|
|
|
368
|
-
console.log(`[cortex-memory] Saved ${saved}/${memories.length} memories on session stop`);
|
|
571
|
+
console.log(`[cortex-memory] Saved ${saved}/${memories.length} memories on session stop (${skipped} skipped as duplicates)`);
|
|
369
572
|
|
|
370
573
|
// Provide visible feedback to user
|
|
371
574
|
if (saved > 0 && event.messages) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shieldcortex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.1",
|
|
4
4
|
"description": "Persistent brain for AI agents. Knowledge graphs, memory decay, contradiction detection, Iron Dome behaviour protection — plus the only defence pipeline that stops memory poisoning. Works with Claude Code, OpenClaw, LangChain, and any MCP agent.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
"import": "./dist/integrations/langchain.js",
|
|
15
15
|
"types": "./dist/integrations/langchain.d.ts"
|
|
16
16
|
},
|
|
17
|
+
"./integrations/universal": {
|
|
18
|
+
"import": "./dist/integrations/universal.js",
|
|
19
|
+
"types": "./dist/integrations/universal.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./integrations/openclaw": {
|
|
22
|
+
"import": "./dist/integrations/openclaw.js",
|
|
23
|
+
"types": "./dist/integrations/openclaw.d.ts"
|
|
24
|
+
},
|
|
17
25
|
"./integrations": {
|
|
18
26
|
"import": "./dist/integrations/index.js",
|
|
19
27
|
"types": "./dist/integrations/index.d.ts"
|
|
@@ -7,7 +7,7 @@ Real-time defence scanning and memory extraction for OpenClaw v2026.2.15+.
|
|
|
7
7
|
| Hook | Action |
|
|
8
8
|
|------|--------|
|
|
9
9
|
| `llm_input` | Scans prompts + history through ShieldCortex defence pipeline. Logs threats, writes audit log, optionally syncs to cloud. **Fire-and-forget.** |
|
|
10
|
-
| `llm_output` |
|
|
10
|
+
| `llm_output` | Optional memory extraction from assistant responses (disabled by default). Saves to ShieldCortex memory via mcporter when enabled, with novelty/dedupe filtering to reduce noise. **Fire-and-forget.** |
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -39,6 +39,42 @@ Find the package root with `npm root -g` (global) or `npm root` (local).
|
|
|
39
39
|
- ShieldCortex installed globally (`npm i -g shieldcortex`) or at `~/ShieldCortex/`
|
|
40
40
|
- `mcporter` available via npx (for memory saves)
|
|
41
41
|
|
|
42
|
+
## Optional Auto-Memory
|
|
43
|
+
|
|
44
|
+
Auto-memory extraction is off by default to avoid duplicate/noisy memory when OpenClaw already has native memory.
|
|
45
|
+
|
|
46
|
+
Enable it:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx shieldcortex config --openclaw-auto-memory true
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Disable it:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx shieldcortex config --openclaw-auto-memory false
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Enable it in `~/.shieldcortex/config.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"openclawAutoMemory": true
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Novelty filtering is enabled by default when auto-memory is on. Optional tuning keys:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"openclawAutoMemoryDedupe": true,
|
|
71
|
+
"openclawAutoMemoryNoveltyThreshold": 0.88,
|
|
72
|
+
"openclawAutoMemoryMaxRecent": 300
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can also manage these settings from the local dashboard in `Shield Overview -> OpenClaw Memory`.
|
|
77
|
+
|
|
42
78
|
## Cloud Sync (optional)
|
|
43
79
|
|
|
44
80
|
Add your API key to `~/.shieldcortex/config.json`:
|
|
@@ -46,7 +82,7 @@ Add your API key to `~/.shieldcortex/config.json`:
|
|
|
46
82
|
```json
|
|
47
83
|
{
|
|
48
84
|
"cloudApiKey": "sc_...",
|
|
49
|
-
"
|
|
85
|
+
"cloudBaseUrl": "https://api.shieldcortex.ai"
|
|
50
86
|
}
|
|
51
87
|
```
|
|
52
88
|
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.2.15+
|
|
3
3
|
*
|
|
4
4
|
* Hooks into llm_input/llm_output for real-time defence scanning
|
|
5
|
-
* and memory extraction. All operations are fire-and-forget.
|
|
5
|
+
* and optional memory extraction. All operations are fire-and-forget.
|
|
6
6
|
*/
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
import fs from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { homedir } from "node:os";
|
|
@@ -20,6 +21,12 @@ async function loadConfig() {
|
|
|
20
21
|
}
|
|
21
22
|
return _config;
|
|
22
23
|
}
|
|
24
|
+
function isAutoMemoryEnabled(config) {
|
|
25
|
+
return config.openclawAutoMemory === true;
|
|
26
|
+
}
|
|
27
|
+
function isAutoMemoryDedupeEnabled(config) {
|
|
28
|
+
return config.openclawAutoMemoryDedupe !== false;
|
|
29
|
+
}
|
|
23
30
|
// ==================== SERVER CMD ====================
|
|
24
31
|
let _serverCmd = null;
|
|
25
32
|
async function resolveServerCmd() {
|
|
@@ -154,6 +161,10 @@ function extractUserContent(msgs) {
|
|
|
154
161
|
return out;
|
|
155
162
|
}
|
|
156
163
|
const AUDIT_DIR = path.join(homedir(), ".shieldcortex", "audit");
|
|
164
|
+
const NOVELTY_CACHE_FILE = path.join(homedir(), ".shieldcortex", "openclaw-memory-cache.json");
|
|
165
|
+
const DEFAULT_NOVELTY_THRESHOLD = 0.88;
|
|
166
|
+
const DEFAULT_MAX_RECENT = 300;
|
|
167
|
+
const MIN_NOVELTY_CHARS = 40;
|
|
157
168
|
async function auditLog(entry) {
|
|
158
169
|
try {
|
|
159
170
|
await fs.mkdir(AUDIT_DIR, { recursive: true });
|
|
@@ -175,6 +186,122 @@ async function cloudSync(threat) {
|
|
|
175
186
|
}
|
|
176
187
|
catch { }
|
|
177
188
|
}
|
|
189
|
+
function normalizeMemoryText(text) {
|
|
190
|
+
return String(text || "")
|
|
191
|
+
.toLowerCase()
|
|
192
|
+
.replace(/[`"'\\]/g, " ")
|
|
193
|
+
.replace(/https?:\/\/\S+/g, " ")
|
|
194
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
195
|
+
.replace(/\s+/g, " ")
|
|
196
|
+
.trim();
|
|
197
|
+
}
|
|
198
|
+
function hashToken(token) {
|
|
199
|
+
return createHash("sha1").update(token).digest("hex").slice(0, 12);
|
|
200
|
+
}
|
|
201
|
+
function buildTokenHashes(normalized) {
|
|
202
|
+
const words = normalized.split(" ").filter((w) => w.length >= 3);
|
|
203
|
+
const set = new Set();
|
|
204
|
+
for (let i = 0; i < words.length; i++) {
|
|
205
|
+
set.add(hashToken(words[i]));
|
|
206
|
+
if (i < words.length - 1)
|
|
207
|
+
set.add(hashToken(`${words[i]}_${words[i + 1]}`));
|
|
208
|
+
}
|
|
209
|
+
return Array.from(set).slice(0, 200);
|
|
210
|
+
}
|
|
211
|
+
function jaccardSimilarity(a, b) {
|
|
212
|
+
if (a.size === 0 || b.size === 0)
|
|
213
|
+
return 0;
|
|
214
|
+
let intersection = 0;
|
|
215
|
+
for (const item of a) {
|
|
216
|
+
if (b.has(item))
|
|
217
|
+
intersection++;
|
|
218
|
+
}
|
|
219
|
+
const union = a.size + b.size - intersection;
|
|
220
|
+
return union === 0 ? 0 : intersection / union;
|
|
221
|
+
}
|
|
222
|
+
function clamp(value, min, max) {
|
|
223
|
+
return Math.max(min, Math.min(max, value));
|
|
224
|
+
}
|
|
225
|
+
async function loadNoveltyCache(maxRecent) {
|
|
226
|
+
try {
|
|
227
|
+
const raw = JSON.parse(await fs.readFile(NOVELTY_CACHE_FILE, "utf-8"));
|
|
228
|
+
if (!Array.isArray(raw))
|
|
229
|
+
return [];
|
|
230
|
+
return raw
|
|
231
|
+
.filter((entry) => entry && typeof entry.hash === "string" && Array.isArray(entry.tokenHashes))
|
|
232
|
+
.slice(0, maxRecent);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function saveNoveltyCache(entries) {
|
|
239
|
+
await fs.mkdir(path.dirname(NOVELTY_CACHE_FILE), { recursive: true });
|
|
240
|
+
await fs.writeFile(NOVELTY_CACHE_FILE, JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
|
241
|
+
}
|
|
242
|
+
function inspectNovelty(content, entries, threshold) {
|
|
243
|
+
const normalized = normalizeMemoryText(content);
|
|
244
|
+
if (normalized.length < MIN_NOVELTY_CHARS) {
|
|
245
|
+
return { allow: true, contentHash: null, tokenHashes: [] };
|
|
246
|
+
}
|
|
247
|
+
const contentHash = createHash("sha256").update(normalized).digest("hex").slice(0, 24);
|
|
248
|
+
if (entries.some((entry) => entry.hash === contentHash)) {
|
|
249
|
+
return { allow: false, contentHash, tokenHashes: [], reason: "exact duplicate" };
|
|
250
|
+
}
|
|
251
|
+
const tokenHashes = buildTokenHashes(normalized);
|
|
252
|
+
const currentSet = new Set(tokenHashes);
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const score = jaccardSimilarity(currentSet, new Set(entry.tokenHashes || []));
|
|
255
|
+
if (score >= threshold) {
|
|
256
|
+
return {
|
|
257
|
+
allow: false,
|
|
258
|
+
contentHash,
|
|
259
|
+
tokenHashes,
|
|
260
|
+
reason: `near duplicate (similarity ${score.toFixed(2)})`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { allow: true, contentHash, tokenHashes };
|
|
265
|
+
}
|
|
266
|
+
async function createNoveltyGate(config) {
|
|
267
|
+
const thresholdRaw = Number(config.openclawAutoMemoryNoveltyThreshold);
|
|
268
|
+
const maxRecentRaw = Number(config.openclawAutoMemoryMaxRecent);
|
|
269
|
+
const threshold = Number.isFinite(thresholdRaw)
|
|
270
|
+
? clamp(thresholdRaw, 0.6, 0.99)
|
|
271
|
+
: DEFAULT_NOVELTY_THRESHOLD;
|
|
272
|
+
const maxRecent = Number.isFinite(maxRecentRaw)
|
|
273
|
+
? Math.floor(clamp(maxRecentRaw, 50, 1000))
|
|
274
|
+
: DEFAULT_MAX_RECENT;
|
|
275
|
+
const enabled = isAutoMemoryDedupeEnabled(config);
|
|
276
|
+
const entries = enabled ? await loadNoveltyCache(maxRecent) : [];
|
|
277
|
+
let dirty = false;
|
|
278
|
+
return {
|
|
279
|
+
inspect(content) {
|
|
280
|
+
if (!enabled)
|
|
281
|
+
return { allow: true, contentHash: null, tokenHashes: [] };
|
|
282
|
+
return inspectNovelty(content, entries, threshold);
|
|
283
|
+
},
|
|
284
|
+
remember(memory, novelty) {
|
|
285
|
+
if (!enabled || !novelty.contentHash || novelty.tokenHashes.length === 0)
|
|
286
|
+
return;
|
|
287
|
+
entries.unshift({
|
|
288
|
+
hash: novelty.contentHash,
|
|
289
|
+
tokenHashes: novelty.tokenHashes,
|
|
290
|
+
title: String(memory.title || "").slice(0, 120),
|
|
291
|
+
category: String(memory.category || "note"),
|
|
292
|
+
createdAt: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
if (entries.length > maxRecent)
|
|
295
|
+
entries.length = maxRecent;
|
|
296
|
+
dirty = true;
|
|
297
|
+
},
|
|
298
|
+
async flush() {
|
|
299
|
+
if (!enabled || !dirty)
|
|
300
|
+
return;
|
|
301
|
+
await saveNoveltyCache(entries);
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
178
305
|
// ==================== HOOK HANDLERS ====================
|
|
179
306
|
// Skip scanning internal OpenClaw content (boot checks, system prompts, heartbeats)
|
|
180
307
|
const SKIP_PATTERNS = [
|
|
@@ -225,25 +352,38 @@ function handleLlmOutput(event, ctx) {
|
|
|
225
352
|
// Fire and forget
|
|
226
353
|
(async () => {
|
|
227
354
|
try {
|
|
355
|
+
const config = await loadConfig();
|
|
356
|
+
if (!isAutoMemoryEnabled(config))
|
|
357
|
+
return;
|
|
228
358
|
const texts = event.assistantTexts.filter(t => t && t.length >= 30);
|
|
229
359
|
if (!texts.length)
|
|
230
360
|
return;
|
|
231
361
|
const memories = extractMemories(texts);
|
|
232
362
|
if (!memories.length)
|
|
233
363
|
return;
|
|
364
|
+
const noveltyGate = await createNoveltyGate(config);
|
|
234
365
|
let saved = 0;
|
|
366
|
+
let skipped = 0;
|
|
235
367
|
for (const mem of memories) {
|
|
368
|
+
const novelty = noveltyGate.inspect(mem.content);
|
|
369
|
+
if (!novelty.allow) {
|
|
370
|
+
skipped++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
236
373
|
const r = await callCortex("remember", {
|
|
237
374
|
title: mem.title, content: mem.content, category: mem.category,
|
|
238
375
|
project: ctx.agentId || "openclaw", scope: "global",
|
|
239
376
|
importance: "normal",
|
|
240
377
|
});
|
|
241
|
-
if (r)
|
|
378
|
+
if (r) {
|
|
242
379
|
saved++;
|
|
380
|
+
noveltyGate.remember(mem, novelty);
|
|
381
|
+
}
|
|
243
382
|
}
|
|
383
|
+
await noveltyGate.flush();
|
|
244
384
|
if (saved) {
|
|
245
|
-
console.log(`[shieldcortex] Extracted ${saved} memor${saved === 1 ? "y" : "ies"} from LLM output`);
|
|
246
|
-
auditLog({ type: "memory", hook: "llm_output", sessionId: event.sessionId, count: saved, ts: new Date().toISOString() });
|
|
385
|
+
console.log(`[shieldcortex] Extracted ${saved} memor${saved === 1 ? "y" : "ies"} from LLM output (${skipped} duplicates skipped)`);
|
|
386
|
+
auditLog({ type: "memory", hook: "llm_output", sessionId: event.sessionId, count: saved, skipped, ts: new Date().toISOString() });
|
|
247
387
|
}
|
|
248
388
|
}
|
|
249
389
|
catch (e) {
|
|
@@ -255,7 +395,7 @@ function handleLlmOutput(event, ctx) {
|
|
|
255
395
|
export default {
|
|
256
396
|
id: "shieldcortex-realtime",
|
|
257
397
|
name: "ShieldCortex Real-time Scanner",
|
|
258
|
-
description: "Real-time defence scanning on LLM inputs
|
|
398
|
+
description: "Real-time defence scanning on LLM inputs with optional memory extraction from outputs",
|
|
259
399
|
version: "__SHIELDCORTEX_VERSION__",
|
|
260
400
|
register(api) {
|
|
261
401
|
api.on("llm_input", handleLlmInput);
|