@xdarkicex/openclaw-memory-libravdb 1.10.1 → 1.10.4

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
@@ -160,6 +160,7 @@ If your service runs elsewhere, set `sidecarPath`:
160
160
  - **Memory-mapped embedding cache.** Frequently embedded text is cached in a file-backed mmap region that survives daemon restarts. Cold starts are faster, repeat queries are instant.
161
161
  - **Pluggable summarization backend.** The memory kernel's extractive summarization can replace LLM-based compaction — zero tokens burned on summarization.
162
162
  - **Local-first inference.** GGUF, ONNX, or remote embedding backends. Hardware-native acceleration on Apple Silicon and NVIDIA. No cloud required.
163
+ - **PII scrubbing & hard constraint rules.** Set rules with keywords — the rules engine scans every reply before dispatch. If a rule keyword is found, the reply is blocked and replaced. No LLM overhead, no latency, can't be bypassed. Names, employers, locations, API keys — anything you don't want leaking gets enforced at the reply layer, not the prompt layer.
163
164
  - **Operational CLI.** `libravdbd status`, `health`, `search`, `tenant evict`, `migrate` — live observability and management without interrupting active sessions.
164
165
 
165
166
  ### Identity Tracking & User Cards
@@ -180,10 +181,10 @@ The daemon tracks **who you are** — not just what you said. Every speaker the
180
181
  - Cards participate in the causal graph (`memory_kind: "identity"`) — identity patterns detected by the cognitive scheduler at macro scale
181
182
  - `PredictiveContext` seeds the card node — BFS surfaces causally connected memories alongside identity
182
183
 
183
- ### Hard Constraint Rules Engine
184
+ ### PII Scrubbing & Hard Constraint Rules
184
185
 
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:
186
+ Agent replies are scanned for forbidden keywords before dispatch. Rules are stored
187
+ locally and enforced through two layers:
187
188
 
188
189
  **Agent tools for rules:**
189
190
  - `set_rule(rule, keywords, priority)` — create a rule with comma-separated keywords for reply scanning. Max 20 rules.
@@ -192,8 +193,8 @@ persisted across sessions, and enforced through two complementary mechanisms:
192
193
  - `delete_rule(rule_id)` — remove a rule.
193
194
 
194
195
  **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.
196
+ - **Reply scan (hard enforcement)** — the `before_agent_reply` hook runs `scanReply` on every agent response. If any rule keyword is found anywhere in the reply text, the response is blocked and replaced with "I cannot answer that." This happens at the dispatch layer — the model can't override it, hallucinate around it, or leak PII. Substring match, case-insensitive, zero LLM overhead.
197
+ - **System prompt (behavioral guidance)** — rules are injected at `prependSystemContext` level (AGENTS.md equivalent) so the model follows behavioral constraints ("always use TypeScript", "never delete files without asking").
197
198
 
198
199
  **Example:**
199
200
  ```
@@ -204,6 +205,8 @@ Keywords: "acme corp, acmecorp, acme.com"
204
205
  If the agent's reply contains any of those keywords, it's blocked and replaced
205
206
  with "I cannot answer that." — regardless of what the model chose to say.
206
207
 
208
+ **Config:** `maxRules` sets the cap (default 20, set to 0 to disable rules entirely).
209
+
207
210
  **Storage:** persisted to `~/.openclaw/cache/libravdb/rules.json`. Survives
208
211
  gateway restarts and plugin updates.
209
212
 
@@ -306,6 +309,8 @@ All keys are optional. For the full reference, see [Configuration](./docs/config
306
309
  | `onnxDevice` | string | `cpu` | ONNX execution provider; `cpu` is the default; `auto` lets libravdbd auto-detect |
307
310
  | `userId` | string | auto-derived | Stable identity for cross-session durable memory |
308
311
  | `tenantId` | string | auto-derived | Multi-tenant identifier. Resolved as `cfg.tenantId` > `LIBRAVDB_AGENT_ID` env > `userId`. Isolates the agent to a dedicated `.libravdb` file. |
312
+ | `tenantIdByAgent` | object | — | Per-agent tenant map. `{"agent1": "t1"}`. Unlisted agents fall through to `tenantId` → `userId`. |
313
+ | `maxRules` | number | 20 | Max hard constraint rules. Set to 0 to disable rules entirely. |
309
314
  | `crossSessionRecall` | boolean | `true` | When `false`, only session-scoped memories are retrieved |
310
315
  | `compactSessionTokenBudget` | number | `2000` | Auto-compaction token threshold; `0` disables |
311
316
 
@@ -330,6 +335,30 @@ If you want to run multiple distinct agents (e.g., a "research-agent" and a "cod
330
335
  }
331
336
  ```
332
337
 
338
+ **Per-agent routing on a single host:** If you run multiple agents through one
339
+ OpenClaw instance, use `tenantIdByAgent` to map each agent to its own tenant:
340
+
341
+ ```json
342
+ {
343
+ "plugins": {
344
+ "entries": {
345
+ "libravdb-memory": {
346
+ "enabled": true,
347
+ "config": {
348
+ "tenantIdByAgent": {
349
+ "jarvis": "jarvis",
350
+ "gideon": "gideon",
351
+ "steel": "steel"
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ Unlisted agents fall through to `tenantId` → `userId`. Zero daemon changes required.
361
+
333
362
  The memory kernel will seamlessly route the agent's requests to a dedicated, isolated vector database file. It manages all tenant instances efficiently within a single process and automatically shares a centralized, memory-mapped embedding cache to keep hardware usage incredibly low.
334
363
 
335
364
  ### Directory Structure
@@ -21,4 +21,4 @@ export declare function resolveIdentity(params: {
21
21
  * 2. LIBRAVDB_AGENT_ID env var (container/CI override)
22
22
  * 3. Fall back to resolved userId (existing identity system)
23
23
  */
24
- export declare function resolveTenantKey(cfg: PluginConfig): string;
24
+ export declare function resolveTenantKey(cfg: PluginConfig, agentId?: string): string;
package/dist/identity.js CHANGED
@@ -126,7 +126,13 @@ export function resolveIdentity(params) {
126
126
  * 2. LIBRAVDB_AGENT_ID env var (container/CI override)
127
127
  * 3. Fall back to resolved userId (existing identity system)
128
128
  */
129
- export function resolveTenantKey(cfg) {
129
+ export function resolveTenantKey(cfg, agentId) {
130
+ // Per-agent override (highest priority).
131
+ if (agentId && cfg.tenantIdByAgent) {
132
+ const byAgent = cfg.tenantIdByAgent[agentId]?.trim();
133
+ if (byAgent)
134
+ return byAgent;
135
+ }
130
136
  const explicit = cfg.tenantId?.trim();
131
137
  if (explicit)
132
138
  return explicit;
package/dist/index.js CHANGED
@@ -16900,7 +16900,11 @@ function resolveIdentity(params) {
16900
16900
  }
16901
16901
  return { userId: autoId, source: "auto" };
16902
16902
  }
16903
- function resolveTenantKey(cfg) {
16903
+ function resolveTenantKey(cfg, agentId) {
16904
+ if (agentId && cfg.tenantIdByAgent) {
16905
+ const byAgent = cfg.tenantIdByAgent[agentId]?.trim();
16906
+ if (byAgent) return byAgent;
16907
+ }
16904
16908
  const explicit = cfg.tenantId?.trim();
16905
16909
  if (explicit) return explicit;
16906
16910
  const envId = process.env.LIBRAVDB_AGENT_ID?.trim();
@@ -18432,7 +18436,10 @@ import { join as join3 } from "node:path";
18432
18436
  // src/rules.ts
18433
18437
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
18434
18438
  import { dirname as dirname2 } from "node:path";
18435
- var MAX_RULES = 20;
18439
+ var maxRules = 20;
18440
+ function setMaxRules(n) {
18441
+ maxRules = Math.max(0, n);
18442
+ }
18436
18443
  function jsonResult(details) {
18437
18444
  return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], details };
18438
18445
  }
@@ -18471,7 +18478,7 @@ function getRule(id) {
18471
18478
  }
18472
18479
  function setRule(ruleText, keywords, priority) {
18473
18480
  let replaced = false;
18474
- if (rules.length >= MAX_RULES) {
18481
+ if (maxRules > 0 && rules.length >= maxRules) {
18475
18482
  let minIdx = 0;
18476
18483
  for (let i = 1; i < rules.length; i++) {
18477
18484
  if (rules[i].priority < rules[minIdx].priority) minIdx = i;
@@ -35005,6 +35012,7 @@ var LibravDBClient = class {
35005
35012
  legacyProbeTimeoutMs;
35006
35013
  nonceHex;
35007
35014
  closed = false;
35015
+ tenantKey;
35008
35016
  constructor(options = {}) {
35009
35017
  this.secret = options.secret ?? loadSecretFromEnv();
35010
35018
  const rawEndpoint = resolveClientEndpoint(options.endpoint);
@@ -35038,14 +35046,14 @@ var LibravDBClient = class {
35038
35046
  bootstrap: () => self.bootstrapHandshake(),
35039
35047
  rpcMutex
35040
35048
  });
35049
+ this.tenantKey = options.tenantKey;
35041
35050
  const interceptors = [];
35042
- if (options.tenantKey) {
35043
- const tenantKey = options.tenantKey;
35044
- interceptors.push((next) => async (req) => {
35045
- req.header.set("libravdb-tenant-key", tenantKey);
35046
- return next(req);
35047
- });
35048
- }
35051
+ interceptors.push((next) => async (req) => {
35052
+ if (self.tenantKey) {
35053
+ req.header.set("libravdb-tenant-key", self.tenantKey);
35054
+ }
35055
+ return next(req);
35056
+ });
35049
35057
  interceptors.push(authInterceptor);
35050
35058
  const transport = createGrpcTransport({
35051
35059
  baseUrl: targetUrl,
@@ -35090,6 +35098,11 @@ var LibravDBClient = class {
35090
35098
  throw new Error("LibravDB client is closed");
35091
35099
  }
35092
35100
  }
35101
+ /** Update the tenant key for subsequent gRPC calls. Thread-safe —
35102
+ * the interceptor reads this.tenantKey on every request. */
35103
+ setTenantKey(key) {
35104
+ this.tenantKey = key;
35105
+ }
35093
35106
  // ── Session lifecycle ────────────────────────────────────────────
35094
35107
  async health(req = {}) {
35095
35108
  this.guardOpen();
@@ -35506,7 +35519,10 @@ function register(api) {
35506
35519
  const runtimeOrNull = isLightweight ? null : createPluginRuntime(cfg, logger);
35507
35520
  if (runtimeOrNull && !isLightweight) {
35508
35521
  const cacheDir = api.cacheDir ?? process.env.OPENCLAW_CACHE_DIR ?? (process.env.HOME || process.env.USERPROFILE || "") + "/.openclaw/cache";
35509
- if (cacheDir) initRuleStore(cacheDir + "/libravdb", logger);
35522
+ if (cacheDir) {
35523
+ initRuleStore(cacheDir + "/libravdb", logger);
35524
+ if (cfg.maxRules !== void 0) setMaxRules(cfg.maxRules);
35525
+ }
35510
35526
  }
35511
35527
  registerMemoryCli(api, runtimeOrNull, cfg, logger);
35512
35528
  const ownsMemorySlot = memSlot === MEMORY_ID;
@@ -35650,9 +35666,19 @@ function register(api) {
35650
35666
  }
35651
35667
  });
35652
35668
  api.on("before_prompt_build", async (_event, ctx) => {
35653
- const sessionId = ctx?.sessionId;
35654
- const trigger = ctx?.trigger;
35669
+ const c = ctx;
35670
+ const sessionId = c?.sessionId;
35671
+ const trigger = c?.trigger;
35655
35672
  if (sessionId) setSessionTrigger(sessionId, trigger);
35673
+ if (runtimeOrNull) {
35674
+ const agentId = c?.agentId;
35675
+ const tenantKey = resolveTenantKey(cfg, agentId);
35676
+ try {
35677
+ const client = await runtimeOrNull.getClient();
35678
+ client.setTenantKey(tenantKey);
35679
+ } catch {
35680
+ }
35681
+ }
35656
35682
  });
35657
35683
  const MULTI_SPEAKER_PROVIDERS = /* @__PURE__ */ new Set([
35658
35684
  "discord",
@@ -35,9 +35,13 @@ export declare class LibravDBClient {
35
35
  private readonly legacyProbeTimeoutMs;
36
36
  private nonceHex;
37
37
  private closed;
38
+ private tenantKey;
38
39
  constructor(options?: LibravDBClientOptions);
39
40
  bootstrapHandshake(): Promise<void>;
40
41
  private guardOpen;
42
+ /** Update the tenant key for subsequent gRPC calls. Thread-safe —
43
+ * the interceptor reads this.tenantKey on every request. */
44
+ setTenantKey(key: string): void;
41
45
  health(req?: PartialMessage<HealthRequest>): Promise<HealthResponse>;
42
46
  status(req?: PartialMessage<MemoryStatusRequest>): Promise<MemoryStatusResponse>;
43
47
  flush(req?: PartialMessage<FlushRequest>): Promise<FlushResponse>;
@@ -149,6 +149,7 @@ export class LibravDBClient {
149
149
  legacyProbeTimeoutMs;
150
150
  nonceHex;
151
151
  closed = false;
152
+ tenantKey;
152
153
  constructor(options = {}) {
153
154
  this.secret = options.secret ?? loadSecretFromEnv();
154
155
  const rawEndpoint = resolveClientEndpoint(options.endpoint);
@@ -180,14 +181,14 @@ export class LibravDBClient {
180
181
  bootstrap: () => self.bootstrapHandshake(),
181
182
  rpcMutex,
182
183
  });
184
+ this.tenantKey = options.tenantKey;
183
185
  const interceptors = [];
184
- if (options.tenantKey) {
185
- const tenantKey = options.tenantKey;
186
- interceptors.push((next) => async (req) => {
187
- req.header.set("libravdb-tenant-key", tenantKey);
188
- return next(req);
189
- });
190
- }
186
+ interceptors.push((next) => async (req) => {
187
+ if (self.tenantKey) {
188
+ req.header.set("libravdb-tenant-key", self.tenantKey);
189
+ }
190
+ return next(req);
191
+ });
191
192
  interceptors.push(authInterceptor);
192
193
  const transport = createGrpcTransport({
193
194
  baseUrl: targetUrl,
@@ -231,6 +232,11 @@ export class LibravDBClient {
231
232
  throw new Error("LibravDB client is closed");
232
233
  }
233
234
  }
235
+ /** Update the tenant key for subsequent gRPC calls. Thread-safe —
236
+ * the interceptor reads this.tenantKey on every request. */
237
+ setTenantKey(key) {
238
+ this.tenantKey = key;
239
+ }
234
240
  // ── Session lifecycle ────────────────────────────────────────────
235
241
  async health(req = {}) {
236
242
  this.guardOpen();
package/dist/rules.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface Rule {
6
6
  priority: number;
7
7
  created_at: number;
8
8
  }
9
+ export declare function setMaxRules(n: number): void;
9
10
  type ToolContent = {
10
11
  type: "text";
11
12
  text: string;
package/dist/rules.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- const MAX_RULES = 20;
3
+ let maxRules = 20;
4
+ export function setMaxRules(n) {
5
+ maxRules = Math.max(0, n);
6
+ }
4
7
  function jsonResult(details) {
5
8
  return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], details };
6
9
  }
@@ -41,7 +44,7 @@ export function getRule(id) {
41
44
  }
42
45
  export function setRule(ruleText, keywords, priority) {
43
46
  let replaced = false;
44
- if (rules.length >= MAX_RULES) {
47
+ if (maxRules > 0 && rules.length >= maxRules) {
45
48
  let minIdx = 0;
46
49
  for (let i = 1; i < rules.length; i++) {
47
50
  if (rules[i].priority < rules[minIdx].priority)
package/dist/types.d.ts CHANGED
@@ -7,6 +7,11 @@ export interface PluginConfig {
7
7
  * the plugin falls back to the auto-derived userId. Set different values per
8
8
  * agent to isolate memory storage. */
9
9
  tenantId?: string;
10
+ /** Per-agent tenant overrides. Maps agentId → tenantId. Agents not in this
11
+ * map fall through to tenantId → userId. Opt-in. */
12
+ tenantIdByAgent?: Record<string, string>;
13
+ /** Maximum number of hard constraint rules. Default 20. */
14
+ maxRules?: number;
10
15
  /** Stable identity for cross-session durable memory. When set, all sessions
11
16
  * share memories under user:{userId}. When unset, the plugin auto-derives
12
17
  * identity from the OS and persists it to the identity file. */
@@ -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.4",
6
6
  "kind": [
7
7
  "memory",
8
8
  "context-engine"
@@ -81,6 +81,17 @@
81
81
  "type": "string",
82
82
  "description": "Stable tenant identifier for multi-agent deployments. When set, the daemon routes this plugin instance to an isolated vector database. When unset, the plugin falls back to the auto-derived userId. Set different values per agent to isolate memory storage."
83
83
  },
84
+ "tenantIdByAgent": {
85
+ "type": "object",
86
+ "additionalProperties": { "type": "string" },
87
+ "description": "Per-agent tenant overrides. Maps agentId to tenantId. Agents not listed fall through to tenantId -> userId. Example: {\"jarvis\": \"jarvis\", \"gideon\": \"gideon\"}."
88
+ },
89
+ "maxRules": {
90
+ "type": "number",
91
+ "default": 20,
92
+ "minimum": 0,
93
+ "description": "Maximum hard constraint rules. 0 disables rules. Default 20."
94
+ },
84
95
  "userId": {
85
96
  "type": "string",
86
97
  "description": "Stable identity for cross-session durable memory. When set, sessions share memories under user:{userId}."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdarkicex/openclaw-memory-libravdb",
3
- "version": "1.10.1",
3
+ "version": "1.10.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",