@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 +27 -0
- package/dist/index.js +34 -5
- package/dist/rules.d.ts +8 -2
- package/dist/rules.js +29 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
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
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
|
5
|
+
"version": "1.10.1",
|
|
6
6
|
"kind": [
|
|
7
7
|
"memory",
|
|
8
8
|
"context-engine"
|