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.
@@ -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
+ }
@@ -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);
@@ -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;
@@ -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
+ }
@@ -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,
@@ -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;
@@ -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.4.37",
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 execute({ reply }) {
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
- reply(`ShieldCortex v${_version}\n` +
606
- ` Hooks: llm_input (scan), llm_output (memory)\n` +
607
- ` Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
608
- ` Cloud sync: ${cloud}`);
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
+ }