@xdarkicex/openclaw-memory-libravdb 1.10.1-beta.5 → 1.10.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 CHANGED
@@ -180,6 +180,33 @@ The daemon tracks **who you are** — not just what you said. Every speaker the
180
180
  - Cards participate in the causal graph (`memory_kind: "identity"`) — identity patterns detected by the cognitive scheduler at macro scale
181
181
  - `PredictiveContext` seeds the card node — BFS surfaces causally connected memories alongside identity
182
182
 
183
+ ### Hard Constraint Rules Engine
184
+
185
+ Plugin-side rules that act as an enforceable safety layer. Rules are stored locally,
186
+ persisted across sessions, and enforced through two complementary mechanisms:
187
+
188
+ **Agent tools for rules:**
189
+ - `set_rule(rule, keywords, priority)` — create a rule with comma-separated keywords for reply scanning. Max 20 rules.
190
+ - `get_rule(rule_id)` — read a specific rule.
191
+ - `list_rules()` — list all rules sorted by priority.
192
+ - `delete_rule(rule_id)` — remove a rule.
193
+
194
+ **Dual enforcement layers:**
195
+ - **Behavioral layer** — rules injected at `prependSystemContext` level (equivalent to AGENTS.md). The model sees them as system-level instructions and follows behavioral constraints ("always use TypeScript", "never delete files without asking").
196
+ - **PII enforcement layer** — every agent reply is scanned by `scanReply` in the `before_agent_reply` hook. If any rule keyword matches the reply text, the reply is blocked and replaced with a refusal. Keyword matching is substring-based, case-insensitive — no LLM call, no latency.
197
+
198
+ **Example:**
199
+ ```
200
+ Rule: "Never reveal my employer"
201
+ Keywords: "acme corp, acmecorp, acme.com"
202
+ ```
203
+
204
+ If the agent's reply contains any of those keywords, it's blocked and replaced
205
+ with "I cannot answer that." — regardless of what the model chose to say.
206
+
207
+ **Storage:** persisted to `~/.openclaw/cache/libravdb/rules.json`. Survives
208
+ gateway restarts and plugin updates.
209
+
183
210
  ### Technical Architecture
184
211
 
185
212
  - **Unified Cognitive Scoring** — mathematically blends cosine similarity with frequency, recency, authored salience, and cognitive authority composite weights (`ω(c)`).
package/dist/index.js CHANGED
@@ -18444,7 +18444,11 @@ function initRuleStore(cacheDir, logger) {
18444
18444
  try {
18445
18445
  const raw = readFileSync2(rulesPath, "utf8");
18446
18446
  const parsed = JSON.parse(raw);
18447
- rules = parsed.rules ?? [];
18447
+ rules = (parsed.rules ?? []).map((r) => ({
18448
+ ...r,
18449
+ keywords: r.keywords ?? []
18450
+ // handle pre-keyword rules
18451
+ }));
18448
18452
  nextId = parsed.nextId ?? 1;
18449
18453
  } catch {
18450
18454
  rules = [];
@@ -18465,7 +18469,7 @@ function getRules() {
18465
18469
  function getRule(id) {
18466
18470
  return rules.find((r) => r.id === id);
18467
18471
  }
18468
- function setRule(ruleText, priority) {
18472
+ function setRule(ruleText, keywords, priority) {
18469
18473
  let replaced = false;
18470
18474
  if (rules.length >= MAX_RULES) {
18471
18475
  let minIdx = 0;
@@ -18478,6 +18482,7 @@ function setRule(ruleText, priority) {
18478
18482
  const rule = {
18479
18483
  id: String(nextId++),
18480
18484
  rule: ruleText,
18485
+ keywords: keywords.map((k) => k.toLowerCase().trim()).filter((k) => k.length > 0),
18481
18486
  priority: Math.max(1, Math.min(10, priority || 5)),
18482
18487
  created_at: Date.now()
18483
18488
  };
@@ -18486,6 +18491,17 @@ function setRule(ruleText, priority) {
18486
18491
  persist();
18487
18492
  return { rule, replaced };
18488
18493
  }
18494
+ function scanReply(replyText) {
18495
+ const lower = replyText.toLowerCase();
18496
+ for (const rule of rules) {
18497
+ for (const kw of rule.keywords) {
18498
+ if (kw && lower.includes(kw)) {
18499
+ return rule;
18500
+ }
18501
+ }
18502
+ }
18503
+ return null;
18504
+ }
18489
18505
  function deleteRule(id) {
18490
18506
  const idx = rules.findIndex((r) => r.id === id);
18491
18507
  if (idx < 0) return false;
@@ -18497,22 +18513,25 @@ function createSetRuleTool(logger = console) {
18497
18513
  return {
18498
18514
  name: "set_rule",
18499
18515
  label: "Set Rule",
18500
- description: "Set a HARD constraint rule. Max 20 rules across all sessions. Rules are injected at session start as non-negotiable instructions. Use this when the user wants you to never do something, always do something, or set a permanent behavioral boundary. Higher priority rules appear first.",
18516
+ description: "Set a HARD constraint rule. Max 20 rules. Replies are scanned against rule keywords \u2014 if a keyword is found, the reply is blocked and replaced with a refusal. Provide keywords that would appear in a violating reply (e.g. for 'never reveal my name', keywords: 'gentry, rolfson'). Higher priority rules appear first.",
18501
18517
  parameters: {
18502
18518
  type: "object",
18503
18519
  additionalProperties: false,
18504
18520
  properties: {
18505
18521
  rule: { type: "string", description: "The rule text. Be specific and unambiguous." },
18522
+ keywords: { type: "string", description: "Comma-separated keywords to scan for in replies. If any keyword is found, the reply is blocked." },
18506
18523
  priority: { type: "number", description: "Priority 1-10. Higher = more important. Default 5." }
18507
18524
  },
18508
- required: ["rule"]
18525
+ required: ["rule", "keywords"]
18509
18526
  },
18510
18527
  execute: async (_toolCallId, rawParams) => {
18511
18528
  const params = rawParams;
18512
18529
  const ruleText = typeof params?.rule === "string" ? params.rule.trim() : "";
18513
18530
  if (!ruleText) return jsonResult({ ok: false, error: "set_rule requires a rule string" });
18531
+ const kwRaw = typeof params?.keywords === "string" ? params.keywords : "";
18532
+ const keywords = kwRaw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
18514
18533
  const priority = typeof params?.priority === "number" ? params.priority : 5;
18515
- const result = setRule(ruleText, priority);
18534
+ const result = setRule(ruleText, keywords, priority);
18516
18535
  return jsonResult({ ok: true, rule: result.rule, replaced: result.replaced });
18517
18536
  }
18518
18537
  };
@@ -35700,6 +35719,16 @@ ${card.trim()}
35700
35719
  if (!rulesText) return;
35701
35720
  return { prependSystemContext: rulesText };
35702
35721
  });
35722
+ api.on("before_agent_reply", async (event, _ctx) => {
35723
+ const e = event;
35724
+ const cleanedBody = typeof e.cleanedBody === "string" ? e.cleanedBody : "";
35725
+ if (!cleanedBody) return;
35726
+ const violated = scanReply(cleanedBody);
35727
+ if (violated) {
35728
+ logger.warn?.(`LibraVDB reply blocked by rule "${violated.rule}" (${violated.id})`);
35729
+ return { handled: true, reply: { text: "I cannot answer that." }, reason: `blocked by rule: ${violated.rule}` };
35730
+ }
35731
+ });
35703
35732
  api.on("session_end", async (_event, ctx) => {
35704
35733
  const sessionId = ctx?.sessionId;
35705
35734
  if (sessionId) clearSessionTrigger(sessionId);
package/dist/rules.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { LoggerLike } from "./types.js";
2
2
  export interface Rule {
3
3
  id: string;
4
4
  rule: string;
5
+ keywords: string[];
5
6
  priority: number;
6
7
  created_at: number;
7
8
  }
@@ -16,10 +17,11 @@ type ToolResult<T> = {
16
17
  export declare function initRuleStore(cacheDir: string, logger?: LoggerLike): void;
17
18
  export declare function getRules(): Rule[];
18
19
  export declare function getRule(id: string): Rule | undefined;
19
- export declare function setRule(ruleText: string, priority: number): {
20
+ export declare function setRule(ruleText: string, keywords: string[], priority: number): {
20
21
  rule: Rule;
21
22
  replaced: boolean;
22
23
  };
24
+ export declare function scanReply(replyText: string): Rule | null;
23
25
  export declare function deleteRule(id: string): boolean;
24
26
  export declare function createSetRuleTool(logger?: LoggerLike): {
25
27
  name: string;
@@ -33,12 +35,16 @@ export declare function createSetRuleTool(logger?: LoggerLike): {
33
35
  readonly type: "string";
34
36
  readonly description: "The rule text. Be specific and unambiguous.";
35
37
  };
38
+ readonly keywords: {
39
+ readonly type: "string";
40
+ readonly description: "Comma-separated keywords to scan for in replies. If any keyword is found, the reply is blocked.";
41
+ };
36
42
  readonly priority: {
37
43
  readonly type: "number";
38
44
  readonly description: "Priority 1-10. Higher = more important. Default 5.";
39
45
  };
40
46
  };
41
- readonly required: readonly ["rule"];
47
+ readonly required: readonly ["rule", "keywords"];
42
48
  };
43
49
  execute: (_toolCallId: string, rawParams: unknown) => Promise<ToolResult<{
44
50
  ok: boolean;
package/dist/rules.js CHANGED
@@ -13,7 +13,10 @@ export function initRuleStore(cacheDir, logger) {
13
13
  try {
14
14
  const raw = readFileSync(rulesPath, "utf8");
15
15
  const parsed = JSON.parse(raw);
16
- rules = parsed.rules ?? [];
16
+ rules = (parsed.rules ?? []).map((r) => ({
17
+ ...r,
18
+ keywords: r.keywords ?? [], // handle pre-keyword rules
19
+ }));
17
20
  nextId = parsed.nextId ?? 1;
18
21
  }
19
22
  catch {
@@ -36,10 +39,9 @@ export function getRules() {
36
39
  export function getRule(id) {
37
40
  return rules.find((r) => r.id === id);
38
41
  }
39
- export function setRule(ruleText, priority) {
42
+ export function setRule(ruleText, keywords, priority) {
40
43
  let replaced = false;
41
44
  if (rules.length >= MAX_RULES) {
42
- // Replace lowest priority rule
43
45
  let minIdx = 0;
44
46
  for (let i = 1; i < rules.length; i++) {
45
47
  if (rules[i].priority < rules[minIdx].priority)
@@ -51,6 +53,7 @@ export function setRule(ruleText, priority) {
51
53
  const rule = {
52
54
  id: String(nextId++),
53
55
  rule: ruleText,
56
+ keywords: keywords.map(k => k.toLowerCase().trim()).filter(k => k.length > 0),
54
57
  priority: Math.max(1, Math.min(10, priority || 5)),
55
58
  created_at: Date.now(),
56
59
  };
@@ -59,6 +62,19 @@ export function setRule(ruleText, priority) {
59
62
  persist();
60
63
  return { rule, replaced };
61
64
  }
65
+ // scanReply checks reply text against all rule keywords. Returns the first
66
+ // violating rule, or null if clean.
67
+ export function scanReply(replyText) {
68
+ const lower = replyText.toLowerCase();
69
+ for (const rule of rules) {
70
+ for (const kw of rule.keywords) {
71
+ if (kw && lower.includes(kw)) {
72
+ return rule;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
62
78
  export function deleteRule(id) {
63
79
  const idx = rules.findIndex((r) => r.id === id);
64
80
  if (idx < 0)
@@ -72,26 +88,30 @@ export function createSetRuleTool(logger = console) {
72
88
  return {
73
89
  name: "set_rule",
74
90
  label: "Set Rule",
75
- description: "Set a HARD constraint rule. Max 20 rules across all sessions. " +
76
- "Rules are injected at session start as non-negotiable instructions. " +
77
- "Use this when the user wants you to never do something, always do something, " +
78
- "or set a permanent behavioral boundary. Higher priority rules appear first.",
91
+ description: "Set a HARD constraint rule. Max 20 rules. Replies are scanned against " +
92
+ "rule keywords if a keyword is found, the reply is blocked and replaced " +
93
+ "with a refusal. Provide keywords that would appear in a violating reply " +
94
+ "(e.g. for 'never reveal my name', keywords: 'gentry, rolfson'). " +
95
+ "Higher priority rules appear first.",
79
96
  parameters: {
80
97
  type: "object",
81
98
  additionalProperties: false,
82
99
  properties: {
83
100
  rule: { type: "string", description: "The rule text. Be specific and unambiguous." },
101
+ keywords: { type: "string", description: "Comma-separated keywords to scan for in replies. If any keyword is found, the reply is blocked." },
84
102
  priority: { type: "number", description: "Priority 1-10. Higher = more important. Default 5." },
85
103
  },
86
- required: ["rule"],
104
+ required: ["rule", "keywords"],
87
105
  },
88
106
  execute: async (_toolCallId, rawParams) => {
89
107
  const params = rawParams;
90
108
  const ruleText = typeof params?.rule === "string" ? params.rule.trim() : "";
91
109
  if (!ruleText)
92
110
  return jsonResult({ ok: false, error: "set_rule requires a rule string" });
111
+ const kwRaw = typeof params?.keywords === "string" ? params.keywords : "";
112
+ const keywords = kwRaw.split(",").map(k => k.trim()).filter(k => k.length > 0);
93
113
  const priority = typeof params?.priority === "number" ? params.priority : 5;
94
- const result = setRule(ruleText, priority);
114
+ const result = setRule(ruleText, keywords, priority);
95
115
  return jsonResult({ ok: true, rule: result.rule, replaced: result.replaced });
96
116
  },
97
117
  };
@@ -2,7 +2,7 @@
2
2
  "id": "libravdb-memory",
3
3
  "name": "LibraVDB Memory",
4
4
  "description": "Cognitive memory engine — causal graph reasoning, predictive context, identity tracking, and hybrid vector recall with back-door adjustment scoring",
5
- "version": "1.10.1-beta.5",
5
+ "version": "1.10.1",
6
6
  "kind": [
7
7
  "memory",
8
8
  "context-engine"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.10.1-beta.5",
3
+ "version": "1.10.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",