shieldcortex 3.4.37 → 4.0.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 +42 -10
- package/dist/api/routes/memories.js +18 -3
- package/dist/cortex/cli.js +44 -1
- package/dist/cortex/store.d.ts +23 -0
- package/dist/cortex/store.js +43 -0
- package/dist/database/init.js +11 -1
- package/dist/index.js +15 -1
- package/dist/lib.d.ts +11 -0
- package/dist/lib.js +7 -0
- package/dist/memory/consolidate.d.ts +14 -0
- package/dist/memory/consolidate.js +73 -0
- package/dist/memory/rerank.d.ts +31 -0
- package/dist/memory/rerank.js +88 -0
- package/dist/memory/save-filter.d.ts +19 -0
- package/dist/memory/save-filter.js +60 -0
- package/dist/memory/staleness.d.ts +27 -0
- package/dist/memory/staleness.js +68 -0
- package/dist/memory/store.js +13 -3
- package/dist/memory/types.d.ts +12 -0
- package/dist/memory/types.js +9 -0
- package/dist/tools/recall.js +9 -0
- package/dist/tools/remember.d.ts +6 -0
- package/dist/tools/remember.js +15 -0
- package/package.json +1 -1
- package/plugins/openclaw/dist/index.js +72 -7
- package/plugins/openclaw/dist/intercept-ingest.js +18 -0
- package/plugins/openclaw/dist/interceptor.js +280 -0
- package/plugins/openclaw/dist/openclaw.plugin.json +42 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Staleness Scoring — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Provides staleness awareness to memories based on age.
|
|
5
|
+
* Used by search and recall to surface freshness warnings.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Days since memory was created.
|
|
9
|
+
*/
|
|
10
|
+
export declare function memoryAgeDays(createdAt: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Human-readable age string.
|
|
13
|
+
*/
|
|
14
|
+
export declare function memoryAge(createdAt: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Freshness score: 1.0 for today, exponentially decaying.
|
|
17
|
+
* Half-life of ~7 days: score ≈ 0.5 after a week.
|
|
18
|
+
*/
|
|
19
|
+
export declare function memoryFreshnessScore(createdAt: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Warning text for stale memories (>2 days old). Returns null for fresh memories.
|
|
22
|
+
*/
|
|
23
|
+
export declare function memoryFreshnessWarning(createdAt: number): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Append staleness warning to a memory's content for display.
|
|
26
|
+
*/
|
|
27
|
+
export declare function appendStalenessWarning(content: string, createdAt: Date): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Staleness Scoring — v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Provides staleness awareness to memories based on age.
|
|
5
|
+
* Used by search and recall to surface freshness warnings.
|
|
6
|
+
*/
|
|
7
|
+
const MS_PER_DAY = 86_400_000;
|
|
8
|
+
/**
|
|
9
|
+
* Days since memory was created.
|
|
10
|
+
*/
|
|
11
|
+
export function memoryAgeDays(createdAt) {
|
|
12
|
+
return Math.max(0, Math.floor((Date.now() - createdAt) / MS_PER_DAY));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable age string.
|
|
16
|
+
*/
|
|
17
|
+
export function memoryAge(createdAt) {
|
|
18
|
+
const days = memoryAgeDays(createdAt);
|
|
19
|
+
if (days === 0)
|
|
20
|
+
return 'today';
|
|
21
|
+
if (days === 1)
|
|
22
|
+
return 'yesterday';
|
|
23
|
+
if (days < 7)
|
|
24
|
+
return `${days} days ago`;
|
|
25
|
+
if (days < 14)
|
|
26
|
+
return '1 week ago';
|
|
27
|
+
if (days < 30)
|
|
28
|
+
return `${Math.floor(days / 7)} weeks ago`;
|
|
29
|
+
if (days < 60)
|
|
30
|
+
return '1 month ago';
|
|
31
|
+
if (days < 365)
|
|
32
|
+
return `${Math.floor(days / 30)} months ago`;
|
|
33
|
+
return `${Math.floor(days / 365)} year${Math.floor(days / 365) > 1 ? 's' : ''} ago`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Freshness score: 1.0 for today, exponentially decaying.
|
|
37
|
+
* Half-life of ~7 days: score ≈ 0.5 after a week.
|
|
38
|
+
*/
|
|
39
|
+
export function memoryFreshnessScore(createdAt) {
|
|
40
|
+
const days = memoryAgeDays(createdAt);
|
|
41
|
+
if (days === 0)
|
|
42
|
+
return 1.0;
|
|
43
|
+
// Exponential decay with half-life of 7 days
|
|
44
|
+
return Math.max(0.01, Math.exp(-0.099 * days));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Warning text for stale memories (>2 days old). Returns null for fresh memories.
|
|
48
|
+
*/
|
|
49
|
+
export function memoryFreshnessWarning(createdAt) {
|
|
50
|
+
const days = memoryAgeDays(createdAt);
|
|
51
|
+
if (days <= 2)
|
|
52
|
+
return null;
|
|
53
|
+
const age = memoryAge(createdAt);
|
|
54
|
+
const score = memoryFreshnessScore(createdAt);
|
|
55
|
+
if (score < 0.1) {
|
|
56
|
+
return `⚠️ Very stale memory (${age}, freshness ${(score * 100).toFixed(0)}%) — verify before relying on this`;
|
|
57
|
+
}
|
|
58
|
+
return `⚠️ Aging memory (${age}, freshness ${(score * 100).toFixed(0)}%) — may need verification`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Append staleness warning to a memory's content for display.
|
|
62
|
+
*/
|
|
63
|
+
export function appendStalenessWarning(content, createdAt) {
|
|
64
|
+
const warning = memoryFreshnessWarning(createdAt.getTime());
|
|
65
|
+
if (!warning)
|
|
66
|
+
return content;
|
|
67
|
+
return `${content}\n\n${warning}`;
|
|
68
|
+
}
|
package/dist/memory/store.js
CHANGED
|
@@ -198,6 +198,8 @@ export function rowToMemory(row) {
|
|
|
198
198
|
sensitivityLevel: row.sensitivity_level ?? 'INTERNAL',
|
|
199
199
|
source: row.source ?? null,
|
|
200
200
|
cloudExcluded: Boolean(row.cloud_excluded),
|
|
201
|
+
memoryPurpose: (row.memory_purpose ?? 'project'),
|
|
202
|
+
memoryScope: (row.memory_scope ?? 'private'),
|
|
201
203
|
};
|
|
202
204
|
}
|
|
203
205
|
/**
|
|
@@ -417,9 +419,9 @@ export function addMemory(input, config = DEFAULT_CONFIG, source) {
|
|
|
417
419
|
const stmt = db.prepare(`
|
|
418
420
|
INSERT INTO memories (
|
|
419
421
|
uuid, type, category, title, content, project, tags, salience, metadata, scope, transferable,
|
|
420
|
-
status, pinned, reviewed_at, reviewed_by, source_kind, capture_method, cloud_excluded, updated_at
|
|
422
|
+
status, pinned, reviewed_at, reviewed_by, source_kind, capture_method, cloud_excluded, memory_purpose, memory_scope, updated_at
|
|
421
423
|
)
|
|
422
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
424
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
423
425
|
`);
|
|
424
426
|
// Anti-bloat: Truncate content if too large
|
|
425
427
|
const truncationResult = truncateContent(input.content);
|
|
@@ -432,7 +434,7 @@ export function addMemory(input, config = DEFAULT_CONFIG, source) {
|
|
|
432
434
|
// Transaction: INSERT + defence UPDATE must be atomic to prevent wrong trust scores
|
|
433
435
|
const insertedId = db.transaction(() => {
|
|
434
436
|
const memoryUuid = randomUUID();
|
|
435
|
-
const result = stmt.run(memoryUuid, type, category, input.title, truncationResult.content, input.project || null, JSON.stringify(tags), salience, JSON.stringify(input.metadata || {}), scope, transferable, status, pinned, input.reviewedBy ? new Date().toISOString() : null, input.reviewedBy ?? null, sourceDetails.sourceKind, sourceDetails.captureMethod, cloudExcluded);
|
|
437
|
+
const result = stmt.run(memoryUuid, type, category, input.title, truncationResult.content, input.project || null, JSON.stringify(tags), salience, JSON.stringify(input.metadata || {}), scope, transferable, status, pinned, input.reviewedBy ? new Date().toISOString() : null, input.reviewedBy ?? null, sourceDetails.sourceKind, sourceDetails.captureMethod, cloudExcluded, input.memoryPurpose || 'project', input.memoryScope || 'private');
|
|
436
438
|
if (defenceResult) {
|
|
437
439
|
db.prepare(`UPDATE memories SET trust_score = ?, sensitivity_level = ?, source = ? WHERE id = ?`)
|
|
438
440
|
.run(defenceResult.trust.score, defenceResult.sensitivity.level, sourceDetails.sourceValue, result.lastInsertRowid);
|
|
@@ -653,6 +655,14 @@ export function updateMemory(id, updates) {
|
|
|
653
655
|
fields.push('cloud_excluded = ?');
|
|
654
656
|
values.push(updates.cloudExcluded ? 1 : 0);
|
|
655
657
|
}
|
|
658
|
+
if (updates.memoryPurpose !== undefined) {
|
|
659
|
+
fields.push('memory_purpose = ?');
|
|
660
|
+
values.push(updates.memoryPurpose);
|
|
661
|
+
}
|
|
662
|
+
if (updates.memoryScope !== undefined) {
|
|
663
|
+
fields.push('memory_scope = ?');
|
|
664
|
+
values.push(updates.memoryScope);
|
|
665
|
+
}
|
|
656
666
|
if (fields.length === 0)
|
|
657
667
|
return existing;
|
|
658
668
|
values.push(id);
|
package/dist/memory/types.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type MemoryType = 'short_term' | 'long_term' | 'episodic';
|
|
|
5
5
|
export type MemoryStatus = 'active' | 'archived' | 'suppressed' | 'canonical';
|
|
6
6
|
export type MemorySourceKind = 'user' | 'cli' | 'hook' | 'plugin' | 'agent' | 'import' | 'cloud' | 'api' | 'system';
|
|
7
7
|
export type MemoryCaptureMethod = 'manual' | 'hook' | 'plugin' | 'import' | 'cloud' | 'api' | 'auto' | 'review';
|
|
8
|
+
export type MemoryPurpose = 'user' | 'feedback' | 'project' | 'reference';
|
|
9
|
+
export type MemoryScope = 'private' | 'team';
|
|
8
10
|
export type MemoryCategory = 'architecture' | 'pattern' | 'preference' | 'error' | 'context' | 'learning' | 'todo' | 'note' | 'relationship' | 'custom';
|
|
9
11
|
export interface Memory {
|
|
10
12
|
id: number;
|
|
@@ -35,6 +37,8 @@ export interface Memory {
|
|
|
35
37
|
sensitivityLevel: string;
|
|
36
38
|
source: string | null;
|
|
37
39
|
cloudExcluded: boolean;
|
|
40
|
+
memoryPurpose: MemoryPurpose;
|
|
41
|
+
memoryScope: MemoryScope;
|
|
38
42
|
}
|
|
39
43
|
export interface MemoryInput {
|
|
40
44
|
type?: MemoryType;
|
|
@@ -56,6 +60,8 @@ export interface MemoryInput {
|
|
|
56
60
|
sensitivityLevel?: string;
|
|
57
61
|
source?: string | null;
|
|
58
62
|
cloudExcluded?: boolean;
|
|
63
|
+
memoryPurpose?: MemoryPurpose;
|
|
64
|
+
memoryScope?: MemoryScope;
|
|
59
65
|
}
|
|
60
66
|
export interface SearchOptions {
|
|
61
67
|
query: string;
|
|
@@ -69,6 +75,8 @@ export interface SearchOptions {
|
|
|
69
75
|
includeGlobal?: boolean;
|
|
70
76
|
includeArchived?: boolean;
|
|
71
77
|
includeSuppressed?: boolean;
|
|
78
|
+
memoryPurpose?: MemoryPurpose;
|
|
79
|
+
memoryScope?: MemoryScope;
|
|
72
80
|
}
|
|
73
81
|
export interface SearchResult {
|
|
74
82
|
memory: Memory;
|
|
@@ -149,3 +157,7 @@ export declare const DEFAULT_CONFIG: MemoryConfig;
|
|
|
149
157
|
* Lower threshold = harder to delete (more valuable)
|
|
150
158
|
*/
|
|
151
159
|
export declare const DELETION_THRESHOLDS: Record<MemoryCategory, number>;
|
|
160
|
+
export declare const VALID_MEMORY_PURPOSES: MemoryPurpose[];
|
|
161
|
+
export declare const VALID_MEMORY_SCOPES: MemoryScope[];
|
|
162
|
+
export declare function isValidMemoryPurpose(value: unknown): value is MemoryPurpose;
|
|
163
|
+
export declare function isValidMemoryScope(value: unknown): value is MemoryScope;
|
package/dist/memory/types.js
CHANGED
|
@@ -27,3 +27,12 @@ export const DELETION_THRESHOLDS = {
|
|
|
27
27
|
todo: 0.25, // Todos - easier to delete
|
|
28
28
|
custom: 0.22, // Custom memories
|
|
29
29
|
};
|
|
30
|
+
// ── Memory Purpose Validation ────────────────────────────────────
|
|
31
|
+
export const VALID_MEMORY_PURPOSES = ['user', 'feedback', 'project', 'reference'];
|
|
32
|
+
export const VALID_MEMORY_SCOPES = ['private', 'team'];
|
|
33
|
+
export function isValidMemoryPurpose(value) {
|
|
34
|
+
return typeof value === 'string' && VALID_MEMORY_PURPOSES.includes(value);
|
|
35
|
+
}
|
|
36
|
+
export function isValidMemoryScope(value) {
|
|
37
|
+
return typeof value === 'string' && VALID_MEMORY_SCOPES.includes(value);
|
|
38
|
+
}
|
package/dist/tools/recall.js
CHANGED
|
@@ -8,6 +8,7 @@ import { searchMemories, recallWithEmbeddings, accessMemory, getRecentMemories,
|
|
|
8
8
|
import { formatTimeSinceAccess } from '../memory/decay.js';
|
|
9
9
|
import { MemoryNotFoundError, formatErrorForMcp } from '../errors.js';
|
|
10
10
|
import { resolveProject } from '../context/project-context.js';
|
|
11
|
+
import { memoryFreshnessWarning } from '../memory/staleness.js';
|
|
11
12
|
const sourceSchema = z.object({
|
|
12
13
|
type: z.enum(['user', 'cli', 'hook', 'email', 'web', 'agent', 'file', 'api', 'tool_response']),
|
|
13
14
|
identifier: z.string(),
|
|
@@ -100,6 +101,14 @@ export async function executeRecall(input) {
|
|
|
100
101
|
}
|
|
101
102
|
// Access each memory to reinforce it
|
|
102
103
|
memories = memories.map(m => accessMemory(m.id, undefined, source) || m);
|
|
104
|
+
// v4.0.0: Append staleness warnings to old memories
|
|
105
|
+
memories = memories.map(m => {
|
|
106
|
+
const warning = memoryFreshnessWarning(m.createdAt.getTime());
|
|
107
|
+
if (warning) {
|
|
108
|
+
return { ...m, content: m.content + '\n\n' + warning };
|
|
109
|
+
}
|
|
110
|
+
return m;
|
|
111
|
+
});
|
|
103
112
|
return {
|
|
104
113
|
success: true,
|
|
105
114
|
memories,
|
package/dist/tools/remember.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare const rememberSchema: z.ZodObject<{
|
|
|
15
15
|
importance: z.ZodOptional<z.ZodEnum<["low", "normal", "high", "critical"]>>;
|
|
16
16
|
scope: z.ZodOptional<z.ZodEnum<["project", "global"]>>;
|
|
17
17
|
transferable: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
memoryPurpose: z.ZodOptional<z.ZodEnum<["user", "feedback", "project", "reference"]>>;
|
|
19
|
+
memoryScope: z.ZodOptional<z.ZodEnum<["private", "team"]>>;
|
|
18
20
|
source: z.ZodOptional<z.ZodObject<{
|
|
19
21
|
type: z.ZodEnum<["user", "cli", "hook", "email", "web", "agent", "file", "api", "tool_response"]>;
|
|
20
22
|
identifier: z.ZodString;
|
|
@@ -46,6 +48,8 @@ export declare const rememberSchema: z.ZodObject<{
|
|
|
46
48
|
sourceIdentifier?: string | undefined;
|
|
47
49
|
sessionId?: string | undefined;
|
|
48
50
|
sourceType?: "user" | "cli" | "hook" | "agent" | "api" | "email" | "web" | "file" | "tool_response" | undefined;
|
|
51
|
+
memoryPurpose?: "project" | "user" | "feedback" | "reference" | undefined;
|
|
52
|
+
memoryScope?: "private" | "team" | undefined;
|
|
49
53
|
importance?: "critical" | "low" | "high" | "normal" | undefined;
|
|
50
54
|
agentId?: string | undefined;
|
|
51
55
|
workspaceDir?: string | undefined;
|
|
@@ -65,6 +69,8 @@ export declare const rememberSchema: z.ZodObject<{
|
|
|
65
69
|
sourceIdentifier?: string | undefined;
|
|
66
70
|
sessionId?: string | undefined;
|
|
67
71
|
sourceType?: "user" | "cli" | "hook" | "agent" | "api" | "email" | "web" | "file" | "tool_response" | undefined;
|
|
72
|
+
memoryPurpose?: "project" | "user" | "feedback" | "reference" | undefined;
|
|
73
|
+
memoryScope?: "private" | "team" | undefined;
|
|
68
74
|
importance?: "critical" | "low" | "high" | "normal" | undefined;
|
|
69
75
|
agentId?: string | undefined;
|
|
70
76
|
workspaceDir?: string | undefined;
|
package/dist/tools/remember.js
CHANGED
|
@@ -8,6 +8,7 @@ import { addMemory, searchMemories, detectRelationships, createMemoryLink, getLa
|
|
|
8
8
|
import { analyzeSalienceFactors, explainSalience } from '../memory/salience.js';
|
|
9
9
|
import { formatErrorForMcp } from '../errors.js';
|
|
10
10
|
import { resolveProject } from '../context/project-context.js';
|
|
11
|
+
import { shouldFilterMemory } from '../memory/save-filter.js';
|
|
11
12
|
// Input schema for the remember tool
|
|
12
13
|
export const rememberSchema = z.object({
|
|
13
14
|
title: z.string().describe('Short title for the memory (what to remember)'),
|
|
@@ -26,6 +27,10 @@ export const rememberSchema = z.object({
|
|
|
26
27
|
.describe('Memory scope: project (default) or global (cross-project)'),
|
|
27
28
|
transferable: z.boolean().optional()
|
|
28
29
|
.describe('Whether this memory can be transferred to other projects'),
|
|
30
|
+
memoryPurpose: z.enum(['user', 'feedback', 'project', 'reference']).optional()
|
|
31
|
+
.describe('Purpose of memory: user (preferences), feedback (corrections/confirmations), project (work context), reference (docs/specs)'),
|
|
32
|
+
memoryScope: z.enum(['private', 'team']).optional()
|
|
33
|
+
.describe('Scope: private (agent-specific) or team (shared across agents)'),
|
|
29
34
|
source: z.object({
|
|
30
35
|
type: z.enum(['user', 'cli', 'hook', 'email', 'web', 'agent', 'file', 'api', 'tool_response']),
|
|
31
36
|
identifier: z.string(),
|
|
@@ -103,6 +108,14 @@ export async function executeRemember(input) {
|
|
|
103
108
|
};
|
|
104
109
|
}
|
|
105
110
|
// Create the memory (use trimmed title and content)
|
|
111
|
+
// v4.0.0: Save filter — prevent storing derivable info
|
|
112
|
+
const filterResult = shouldFilterMemory(title, content);
|
|
113
|
+
if (!filterResult.allowed) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: `Memory filtered: ${filterResult.reason}${filterResult.warning ? ' — ' + filterResult.warning : ''}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
106
119
|
const memory = addMemory({
|
|
107
120
|
title,
|
|
108
121
|
content,
|
|
@@ -114,6 +127,8 @@ export async function executeRemember(input) {
|
|
|
114
127
|
scope: input.scope,
|
|
115
128
|
transferable: input.transferable,
|
|
116
129
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
130
|
+
memoryPurpose: input.memoryPurpose,
|
|
131
|
+
memoryScope: input.memoryScope,
|
|
117
132
|
}, undefined, derivedSource ?? { type: 'cli', identifier: 'mcp' });
|
|
118
133
|
// Auto-detect and create relationships with existing memories
|
|
119
134
|
let linksCreated = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shieldcortex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,6 +12,8 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { createInterceptor, DEFAULT_CONFIG as DEFAULT_INTERCEPTOR_CONFIG } from './interceptor.js';
|
|
16
|
+
import { syncInterceptEvent } from './intercept-ingest.js';
|
|
15
17
|
let runtimePromise = null;
|
|
16
18
|
function addRuntimeCandidate(candidates, packageRoot) {
|
|
17
19
|
const runtimePath = path.join(packageRoot, "hooks", "openclaw", "cortex-memory", "runtime.mjs");
|
|
@@ -583,6 +585,68 @@ export default {
|
|
|
583
585
|
},
|
|
584
586
|
register(api) {
|
|
585
587
|
applyPluginConfigOverride(api);
|
|
588
|
+
// --- Interceptor (lazy init) ---
|
|
589
|
+
let interceptorReady = null;
|
|
590
|
+
let interceptorInitAttempted = false;
|
|
591
|
+
async function initInterceptor() {
|
|
592
|
+
if (interceptorInitAttempted)
|
|
593
|
+
return interceptorReady;
|
|
594
|
+
interceptorInitAttempted = true;
|
|
595
|
+
try {
|
|
596
|
+
const scConfig = await loadConfig();
|
|
597
|
+
const rawInterceptorConfig = scConfig.interceptor;
|
|
598
|
+
const interceptorConfig = {
|
|
599
|
+
...DEFAULT_INTERCEPTOR_CONFIG,
|
|
600
|
+
...(rawInterceptorConfig && typeof rawInterceptorConfig === 'object' ? {
|
|
601
|
+
enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
|
|
602
|
+
severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
|
|
603
|
+
failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
|
|
604
|
+
} : {}),
|
|
605
|
+
logger: { info: api.logger?.info ?? console.log, warn: api.logger?.warn ?? console.warn },
|
|
606
|
+
};
|
|
607
|
+
if (!interceptorConfig.enabled)
|
|
608
|
+
return null;
|
|
609
|
+
// Dynamic import with string variable to prevent TypeScript from resolving
|
|
610
|
+
// at compile time — 'shieldcortex/defence' only exists at runtime when the
|
|
611
|
+
// package is installed globally, not during CI builds of the plugin itself.
|
|
612
|
+
const defenceModPath = 'shieldcortex' + '/defence';
|
|
613
|
+
const defenceMod = await import(/* webpackIgnore: true */ defenceModPath);
|
|
614
|
+
if (typeof defenceMod.runDefencePipeline !== 'function')
|
|
615
|
+
return null;
|
|
616
|
+
interceptorReady = createInterceptor(interceptorConfig, defenceMod.runDefencePipeline, {
|
|
617
|
+
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
618
|
+
cloudApiKey: scConfig.cloudApiKey ?? '',
|
|
619
|
+
cloudBaseUrl: scConfig.cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
|
620
|
+
cloudEnabled: scConfig.cloudEnabled ?? false,
|
|
621
|
+
}),
|
|
622
|
+
});
|
|
623
|
+
api.logger?.info?.('[shieldcortex] Interceptor active — watching: remember, mcp__memory__remember');
|
|
624
|
+
return interceptorReady;
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
api.logger?.warn?.(`[shieldcortex] Interceptor init failed: ${err instanceof Error ? err.message : err}`);
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Register before_tool_call with lazy-init wrapper
|
|
632
|
+
api.registerHook('before_tool_call', async (context) => {
|
|
633
|
+
const interceptor = await initInterceptor();
|
|
634
|
+
if (interceptor)
|
|
635
|
+
await interceptor.handleToolCall(context);
|
|
636
|
+
}, {
|
|
637
|
+
name: 'shieldcortex-intercept-tool',
|
|
638
|
+
description: 'Active threat gating on tool calls',
|
|
639
|
+
});
|
|
640
|
+
// Try to register session_end for cache cleanup
|
|
641
|
+
try {
|
|
642
|
+
api.registerHook('session_end', () => { interceptorReady?.resetSession(); }, {
|
|
643
|
+
name: 'shieldcortex-session-cleanup',
|
|
644
|
+
description: 'Clear interceptor deny cache on session end',
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// session_end may not be a supported hook — TTL safety net handles this
|
|
649
|
+
}
|
|
586
650
|
// Explicit capability registration (replaces legacy api.on)
|
|
587
651
|
api.registerHook("llm_input", handleLlmInput, {
|
|
588
652
|
name: "shieldcortex-scan-input",
|
|
@@ -595,19 +659,20 @@ export default {
|
|
|
595
659
|
// Register a lightweight status command so the plugin is not hook-only
|
|
596
660
|
api.registerCommand({
|
|
597
661
|
name: "shieldcortex-status",
|
|
598
|
-
aliases: ["sc-status"],
|
|
599
662
|
description: "Show ShieldCortex real-time scanner status",
|
|
600
|
-
async
|
|
663
|
+
async handler() {
|
|
601
664
|
const cfg = await loadConfig();
|
|
602
665
|
const autoMemory = isAutoMemoryEnabled(cfg) ? "on" : "off";
|
|
603
666
|
const dedupe = isAutoMemoryDedupeEnabled(cfg) ? "on" : "off";
|
|
604
667
|
const cloud = cfg.cloudApiKey ? "configured" : "not configured";
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
668
|
+
return {
|
|
669
|
+
text: `ShieldCortex v${_version}\n` +
|
|
670
|
+
` Hooks: llm_input (scan), llm_output (memory)\n` +
|
|
671
|
+
` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
|
|
672
|
+
` Cloud sync: ${cloud}`,
|
|
673
|
+
};
|
|
609
674
|
},
|
|
610
675
|
});
|
|
611
|
-
api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + /shieldcortex-status)`);
|
|
676
|
+
api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + before_tool_call + /shieldcortex-status)`);
|
|
612
677
|
},
|
|
613
678
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function syncInterceptEvent(event, config) {
|
|
2
|
+
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
3
|
+
return;
|
|
4
|
+
const url = `${config.cloudBaseUrl}/v1/audit/ingest`;
|
|
5
|
+
fetch(url, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
events: [{ ...event, source: 'openclaw-interceptor' }],
|
|
13
|
+
}),
|
|
14
|
+
signal: AbortSignal.timeout(5_000),
|
|
15
|
+
}).catch(() => {
|
|
16
|
+
// Fire-and-forget — never block on cloud sync failure
|
|
17
|
+
});
|
|
18
|
+
}
|