clawvault 3.2.1 → 3.4.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.
Files changed (161) hide show
  1. package/README.md +56 -16
  2. package/bin/clawvault.js +0 -2
  3. package/bin/command-registration.test.js +15 -2
  4. package/bin/help-contract.test.js +16 -0
  5. package/bin/register-core-commands.js +88 -0
  6. package/bin/register-core-commands.test.js +80 -0
  7. package/bin/register-maintenance-commands.js +84 -7
  8. package/bin/register-query-commands.js +45 -28
  9. package/bin/register-query-commands.test.js +15 -0
  10. package/bin/test-helpers/cli-command-fixtures.js +1 -0
  11. package/dist/chunk-2PKBIKDH.js +130 -0
  12. package/dist/{chunk-U67V476Y.js → chunk-2ZDO52B4.js} +18 -1
  13. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  14. package/dist/chunk-35JCYSRR.js +158 -0
  15. package/dist/{chunk-AZYOKJYC.js → chunk-4PY655YM.js} +13 -1
  16. package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
  17. package/dist/{chunk-Y3TIJEBP.js → chunk-7SWP5FKU.js} +34 -613
  18. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  19. package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
  20. package/dist/{chunk-4ITRXIVT.js → chunk-BLQXXX7Q.js} +6 -6
  21. package/dist/chunk-CSHO3PJB.js +684 -0
  22. package/dist/chunk-D5U3Q4N5.js +872 -0
  23. package/dist/chunk-DCF4KMFD.js +158 -0
  24. package/dist/{chunk-S5OJEGFG.js → chunk-DOIUYIXV.js} +2 -2
  25. package/dist/{chunk-YXQCA6B7.js → chunk-DVOUSOR3.js} +112 -7
  26. package/dist/{chunk-YDWHS4LJ.js → chunk-ECGJYWNA.js} +205 -33
  27. package/dist/{chunk-QMHPQYUV.js → chunk-EL6UBSX5.js} +7 -6
  28. package/dist/chunk-FZ5I2NF7.js +352 -0
  29. package/dist/{chunk-WJVWINEM.js → chunk-GFCHWMGD.js} +55 -6
  30. package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
  31. package/dist/chunk-H3JZIB5O.js +322 -0
  32. package/dist/chunk-HEHO7SMV.js +51 -0
  33. package/dist/{chunk-UCQAOZHW.js → chunk-HGDDW24U.js} +3 -3
  34. package/dist/chunk-J3YUXVID.js +907 -0
  35. package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
  36. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  37. package/dist/chunk-NSXYM6EZ.js +255 -0
  38. package/dist/{chunk-YNIPYN4F.js → chunk-OFOCU2V4.js} +6 -5
  39. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  40. package/dist/chunk-PTWPPVC7.js +972 -0
  41. package/dist/{chunk-FAKNOB7Y.js → chunk-QFWERBDP.js} +2 -2
  42. package/dist/chunk-QYQAGBTM.js +2097 -0
  43. package/dist/chunk-RL2L6I6K.js +223 -0
  44. package/dist/{chunk-IIOU45CK.js → chunk-S7N7HI5E.js} +2 -2
  45. package/dist/{chunk-ECRZL5XR.js → chunk-T7E764W3.js} +23 -7
  46. package/dist/{chunk-MNPUYCHQ.js → chunk-TWMI3SNN.js} +6 -5
  47. package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
  48. package/dist/{chunk-PI4WMLMG.js → chunk-VXAGOLDP.js} +1 -1
  49. package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
  50. package/dist/chunk-YTRZNA64.js +37 -0
  51. package/dist/chunk-ZKWPCBYT.js +600 -0
  52. package/dist/cli/index.js +28 -21
  53. package/dist/commands/archive.js +3 -3
  54. package/dist/commands/backlog.js +1 -1
  55. package/dist/commands/benchmark.d.ts +12 -0
  56. package/dist/commands/benchmark.js +12 -0
  57. package/dist/commands/blocked.js +1 -1
  58. package/dist/commands/canvas.js +2 -2
  59. package/dist/commands/checkpoint.js +1 -1
  60. package/dist/commands/compat.js +1 -1
  61. package/dist/commands/context.js +8 -7
  62. package/dist/commands/doctor.d.ts +8 -3
  63. package/dist/commands/doctor.js +8 -22
  64. package/dist/commands/embed.js +6 -5
  65. package/dist/commands/entities.d.ts +8 -1
  66. package/dist/commands/entities.js +46 -3
  67. package/dist/commands/graph.js +4 -4
  68. package/dist/commands/inbox.d.ts +23 -0
  69. package/dist/commands/inbox.js +11 -0
  70. package/dist/commands/inject.d.ts +1 -1
  71. package/dist/commands/inject.js +5 -5
  72. package/dist/commands/kanban.js +1 -1
  73. package/dist/commands/link.js +5 -5
  74. package/dist/commands/maintain.d.ts +32 -0
  75. package/dist/commands/maintain.js +13 -0
  76. package/dist/commands/migrate-observations.js +3 -3
  77. package/dist/commands/observe.js +11 -10
  78. package/dist/commands/project.js +2 -2
  79. package/dist/commands/rebuild-embeddings.js +48 -17
  80. package/dist/commands/rebuild.js +9 -8
  81. package/dist/commands/recall.d.ts +14 -0
  82. package/dist/commands/recall.js +15 -0
  83. package/dist/commands/recover.js +1 -1
  84. package/dist/commands/reflect.js +6 -6
  85. package/dist/commands/repair-session.js +1 -1
  86. package/dist/commands/replay.js +10 -9
  87. package/dist/commands/session-recap.js +1 -1
  88. package/dist/commands/setup.js +4 -3
  89. package/dist/commands/shell-init.js +1 -1
  90. package/dist/commands/sleep.d.ts +1 -1
  91. package/dist/commands/sleep.js +20 -18
  92. package/dist/commands/status.js +40 -26
  93. package/dist/commands/sync-bd.js +3 -3
  94. package/dist/commands/tailscale.js +3 -3
  95. package/dist/commands/task.js +1 -1
  96. package/dist/commands/template.js +1 -1
  97. package/dist/commands/wake.d.ts +1 -1
  98. package/dist/commands/wake.js +10 -9
  99. package/dist/index.d.ts +233 -16
  100. package/dist/index.js +325 -111
  101. package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
  102. package/dist/lib/auto-linker.js +2 -2
  103. package/dist/lib/canvas-layout.js +1 -1
  104. package/dist/lib/config.js +2 -2
  105. package/dist/lib/entity-index.js +1 -1
  106. package/dist/lib/project-utils.js +2 -2
  107. package/dist/lib/session-repair.js +1 -1
  108. package/dist/lib/session-utils.js +1 -1
  109. package/dist/lib/tailscale.js +1 -1
  110. package/dist/lib/task-utils.js +1 -1
  111. package/dist/lib/template-engine.js +1 -1
  112. package/dist/lib/webdav.js +1 -1
  113. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  114. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  115. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  116. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  117. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  118. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  119. package/dist/openclaw-plugin--gqA2BZw.d.ts +267 -0
  120. package/dist/openclaw-plugin.d.ts +4 -0
  121. package/dist/openclaw-plugin.js +20 -0
  122. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  123. package/dist/types-CbL-wIKi.d.ts +36 -0
  124. package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
  125. package/hooks/clawvault/HOOK.md +25 -8
  126. package/hooks/clawvault/handler.js +215 -78
  127. package/hooks/clawvault/handler.test.js +109 -43
  128. package/hooks/clawvault/integrity.js +112 -0
  129. package/hooks/clawvault/integrity.test.js +32 -0
  130. package/hooks/clawvault/openclaw.plugin.json +133 -15
  131. package/openclaw.plugin.json +161 -194
  132. package/package.json +8 -5
  133. package/bin/register-workgraph-commands.js +0 -451
  134. package/dist/chunk-5PJ4STIC.js +0 -465
  135. package/dist/chunk-ERNE2FZ5.js +0 -189
  136. package/dist/chunk-HR4KN6S2.js +0 -152
  137. package/dist/chunk-IJBFGPCS.js +0 -33
  138. package/dist/chunk-K7PNYS45.js +0 -93
  139. package/dist/chunk-NTOPJI7W.js +0 -207
  140. package/dist/chunk-PG56HX5T.js +0 -154
  141. package/dist/chunk-QPDDIHXE.js +0 -501
  142. package/dist/chunk-WIOLLGAD.js +0 -190
  143. package/dist/chunk-WMGIIABP.js +0 -15
  144. package/dist/ledger-B7g7jhqG.d.ts +0 -44
  145. package/dist/plugin/index.d.ts +0 -352
  146. package/dist/plugin/index.js +0 -4264
  147. package/dist/registry-BR4326o0.d.ts +0 -30
  148. package/dist/store-CA-6sKCJ.d.ts +0 -34
  149. package/dist/thread-B9LhXNU0.d.ts +0 -41
  150. package/dist/workgraph/index.d.ts +0 -5
  151. package/dist/workgraph/index.js +0 -23
  152. package/dist/workgraph/ledger.d.ts +0 -2
  153. package/dist/workgraph/ledger.js +0 -25
  154. package/dist/workgraph/registry.d.ts +0 -2
  155. package/dist/workgraph/registry.js +0 -19
  156. package/dist/workgraph/store.d.ts +0 -2
  157. package/dist/workgraph/store.js +0 -25
  158. package/dist/workgraph/thread.d.ts +0 -2
  159. package/dist/workgraph/thread.js +0 -25
  160. package/dist/workgraph/types.d.ts +0 -54
  161. package/dist/workgraph/types.js +0 -7
@@ -0,0 +1,36 @@
1
+ import { a as SearchOptions, b as SearchResult } from './types-DslKvCaj.js';
2
+ import { PluginConfig } from './lib/config.js';
3
+
4
+ type RecallStrategy = 'quick' | 'entity' | 'temporal' | 'verification' | 'relationship';
5
+ interface RecallQueryClassification {
6
+ strategy: RecallStrategy;
7
+ entityName?: string;
8
+ temporalDays?: number;
9
+ }
10
+ interface RecallOptions {
11
+ limit?: number;
12
+ strategy?: RecallStrategy;
13
+ includeSources?: boolean;
14
+ vaultPath?: string;
15
+ agentId?: string;
16
+ pluginConfig?: PluginConfig;
17
+ searchOptions?: SearchOptions;
18
+ }
19
+ interface RecallSource {
20
+ title: string;
21
+ path: string;
22
+ category: string;
23
+ score: number;
24
+ snippet: string;
25
+ modified: string;
26
+ }
27
+ interface RecallResult {
28
+ query: string;
29
+ strategy: RecallStrategy;
30
+ entityName?: string;
31
+ context: string;
32
+ sources: RecallSource[];
33
+ rawResults: SearchResult[];
34
+ }
35
+
36
+ export type { RecallStrategy as R, RecallQueryClassification as a, RecallOptions as b, RecallResult as c, RecallSource as d };
@@ -14,6 +14,8 @@ interface VaultConfig {
14
14
  qmdRoot?: string;
15
15
  /** Custom templates path (optional) */
16
16
  templatesPath?: string;
17
+ /** Search configuration */
18
+ search?: VaultSearchConfig;
17
19
  }
18
20
  interface VaultMeta {
19
21
  name: string;
@@ -26,6 +28,37 @@ interface VaultMeta {
26
28
  qmdCollection?: string;
27
29
  /** Root path for qmd collection (defaults to vault path) */
28
30
  qmdRoot?: string;
31
+ /** Search configuration */
32
+ search?: VaultSearchConfig;
33
+ }
34
+ type SearchBackend = 'in-process' | 'qmd';
35
+ type EmbeddingProvider = 'none' | 'openai' | 'gemini' | 'ollama';
36
+ type RerankProvider = 'none' | 'jina' | 'voyage' | 'siliconflow' | 'pinecone';
37
+ interface VaultSearchConfig {
38
+ /**
39
+ * Default backend used for search commands.
40
+ * in-process is default and recommended for constrained devices.
41
+ */
42
+ backend?: SearchBackend;
43
+ /** Allow qmd fallback when installed and in-process search fails */
44
+ qmdFallback?: boolean;
45
+ /** Chunk size in characters for in-process BM25 indexing */
46
+ chunkSize?: number;
47
+ /** Chunk overlap in characters for in-process BM25 indexing */
48
+ chunkOverlap?: number;
49
+ embeddings?: {
50
+ provider?: EmbeddingProvider;
51
+ model?: string;
52
+ baseUrl?: string;
53
+ apiKey?: string;
54
+ };
55
+ rerank?: {
56
+ provider?: RerankProvider;
57
+ model?: string;
58
+ endpoint?: string;
59
+ apiKey?: string;
60
+ weight?: number;
61
+ };
29
62
  }
30
63
  interface Document {
31
64
  /** Unique ID (relative path without extension) */
@@ -89,6 +122,23 @@ interface StoreOptions {
89
122
  /** Optional qmd index name override (defaults to configured/global index) */
90
123
  qmdIndexName?: string;
91
124
  }
125
+ type PatchMode = 'append' | 'replace' | 'content';
126
+ interface PatchOptions {
127
+ /** Document id/path to patch (e.g. decisions/my-note or decisions/my-note.md) */
128
+ idOrPath: string;
129
+ /** Patch mode */
130
+ mode: PatchMode;
131
+ /** Text to append when mode=append */
132
+ append?: string;
133
+ /** Search text when mode=replace */
134
+ replace?: string;
135
+ /** Replacement text when mode=replace */
136
+ with?: string;
137
+ /** Replacement body when mode=content */
138
+ content?: string;
139
+ /** Optional markdown section heading (without #) to scope the patch */
140
+ section?: string;
141
+ }
92
142
  interface SyncOptions {
93
143
  /** Target directory to sync to */
94
144
  target: string;
@@ -159,4 +209,4 @@ interface SessionRecap {
159
209
  }
160
210
  declare const DEFAULT_CONFIG: Partial<VaultConfig>;
161
211
 
162
- export { type Category as C, type Document as D, type HandoffDocument as H, type MemoryType as M, type SessionRecap as S, TYPE_TO_CATEGORY as T, type VaultConfig as V, type StoreOptions as a, type SearchOptions as b, type SearchResult as c, type SyncOptions as d, type SyncResult as e, DEFAULT_CATEGORIES as f, DEFAULT_CONFIG as g, MEMORY_TYPES as h, type VaultMeta as i };
212
+ export { type Category as C, type Document as D, type EmbeddingProvider as E, type HandoffDocument as H, type MemoryType as M, type PatchOptions as P, type RerankProvider as R, type StoreOptions as S, TYPE_TO_CATEGORY as T, type VaultConfig as V, type SearchOptions as a, type SearchResult as b, type SyncOptions as c, type SyncResult as d, type SessionRecap as e, type VaultSearchConfig as f, DEFAULT_CATEGORIES as g, DEFAULT_CONFIG as h, MEMORY_TYPES as i, type PatchMode as j, type SearchBackend as k, type VaultMeta as l };
@@ -81,33 +81,50 @@ Configure the plugin via OpenClaw's config system:
81
81
 
82
82
  ```bash
83
83
  # Set vault path
84
- openclaw config set plugins.clawvault.config.vaultPath ~/my-vault
84
+ openclaw config set plugins.entries.clawvault.config.vaultPath ~/my-vault
85
85
 
86
86
  # View current config
87
- openclaw config get plugins.clawvault.config
87
+ openclaw config get plugins.entries.clawvault
88
88
  ```
89
89
 
90
- Available configuration options:
90
+ Available configuration options (all privileged actions are opt-in):
91
91
 
92
92
  | Key | Type | Default | Description |
93
93
  |-----|------|---------|-------------|
94
94
  | `vaultPath` | string | (auto-detected) | Path to the ClawVault vault directory |
95
- | `autoCheckpoint` | boolean | `true` | Enable automatic checkpointing on session events |
95
+ | `agentVaults` | object | `{}` | Per-agent vault mapping |
96
+ | `allowClawvaultExec` | boolean | `false` | Required gate for all `child_process` calls |
97
+ | `clawvaultBinaryPath` | string | (PATH lookup) | Optional absolute path to `clawvault` binary |
98
+ | `clawvaultBinarySha256` | string | (unset) | Optional SHA-256 executable integrity check |
99
+ | `allowEnvAccess` | boolean | `false` | Allow env fallbacks (`OPENCLAW_*`, `CLAWVAULT_PATH`) |
100
+ | `enableStartupRecovery` | boolean | `false` | Enable `gateway:startup` recovery check |
101
+ | `enableSessionContextInjection` | boolean | `false` | Enable `session:start` recap/context injection |
102
+ | `enableAutoCheckpoint` | boolean | `false` | Enable checkpoint on `command:new` |
103
+ | `enableObserveOnNew` | boolean | `false` | Enable observer flush on `command:new` |
104
+ | `enableHeartbeatObservation` | boolean | `false` | Enable heartbeat-driven observation |
105
+ | `enableCompactionObservation` | boolean | `false` | Enable observer flush on compaction |
106
+ | `enableWeeklyReflection` | boolean | `false` | Enable weekly reflection cron |
107
+ | `enableFactExtraction` | boolean | `false` | Enable local fact extraction/entity graph updates |
108
+ | `autoCheckpoint` | boolean | `false` | Deprecated alias for `enableAutoCheckpoint` |
96
109
  | `contextProfile` | string | `"auto"` | Default context profile (`default`, `planning`, `incident`, `handoff`, `auto`) |
97
110
  | `maxContextResults` | integer | `4` | Maximum context results to inject on session start |
98
- | `observeOnHeartbeat` | boolean | `true` | Enable observation threshold checks on heartbeat |
99
- | `weeklyReflection` | boolean | `true` | Enable weekly reflection on Sunday midnight UTC |
111
+ | `observeOnHeartbeat` | boolean | `false` | Deprecated alias for `enableHeartbeatObservation` |
112
+ | `weeklyReflection` | boolean | `false` | Deprecated alias for `enableWeeklyReflection` |
113
+
114
+ Security details and threat model: see [SECURITY.md](../../SECURITY.md).
100
115
 
101
116
  ### Vault Path Resolution
102
117
 
103
- The hook resolves the vault path in this order:
118
+ When `allowEnvAccess=true`, the hook resolves the vault path in this order:
104
119
 
105
- 1. Plugin config (`plugins.clawvault.config.vaultPath` set via `openclaw config`)
120
+ 1. Plugin config (`plugins.entries.clawvault.config.vaultPath` set via `openclaw config`)
106
121
  2. `OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH` environment variable
107
122
  3. `CLAWVAULT_PATH` environment variable
108
123
  4. Walking up from cwd to find `.clawvault.json`
109
124
  5. Checking `memory/` subdirectory (OpenClaw convention)
110
125
 
126
+ When `allowEnvAccess=false` (default), steps 2 and 3 are skipped.
127
+
111
128
  ### Troubleshooting
112
129
 
113
130
  If `openclaw hooks enable clawvault` fails with hook-not-found, run `openclaw hooks install clawvault` first and verify discovery with `openclaw hooks list --verbose`.
@@ -16,6 +16,11 @@ import { createHash, randomUUID } from 'crypto';
16
16
  import * as fs from 'fs';
17
17
  import * as os from 'os';
18
18
  import * as path from 'path';
19
+ import {
20
+ resolveExecutablePath,
21
+ sanitizeExecArgs,
22
+ verifyExecutableIntegrity
23
+ } from './integrity.js';
19
24
 
20
25
  const MAX_CONTEXT_RESULTS = 4;
21
26
  const MAX_CONTEXT_PROMPT_LENGTH = 500;
@@ -36,6 +41,7 @@ const MAX_FACT_TEXT_LENGTH = 600;
36
41
  const FACT_SENTENCE_SPLIT_RE = /[.!?]+\s+|\r?\n+/;
37
42
  const EXCLUSIVE_FACT_RELATIONS = new Set(['lives_in', 'works_at', 'age']);
38
43
  const ENTITY_TARGET_RELATIONS = new Set(['works_at', 'lives_in', 'partner_name', 'dog_name', 'parent_name']);
44
+ const CLAWVAULT_EXECUTABLE = 'clawvault';
39
45
 
40
46
  // Sanitize string for safe display (prevent prompt injection via control chars)
41
47
  function sanitizeForDisplay(str) {
@@ -107,15 +113,17 @@ function normalizeAbsoluteEnvPath(value) {
107
113
  return resolved;
108
114
  }
109
115
 
110
- function getOpenClawAgentsDir() {
111
- const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
112
- if (stateDir) {
113
- return path.join(stateDir, 'agents');
114
- }
116
+ function getOpenClawAgentsDir(pluginConfig) {
117
+ if (allowsEnvAccess(pluginConfig)) {
118
+ const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
119
+ if (stateDir) {
120
+ return path.join(stateDir, 'agents');
121
+ }
115
122
 
116
- const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
117
- if (openClawHome) {
118
- return path.join(openClawHome, 'agents');
123
+ const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
124
+ if (openClawHome) {
125
+ return path.join(openClawHome, 'agents');
126
+ }
119
127
  }
120
128
 
121
129
  return path.join(os.homedir(), '.openclaw', 'agents');
@@ -155,8 +163,8 @@ function getScaledObservationThresholdBytes(fileSizeBytes) {
155
163
  return LARGE_SESSION_THRESHOLD_BYTES;
156
164
  }
157
165
 
158
- function parseSessionIndex(agentId) {
159
- const sessionsDir = path.join(getOpenClawAgentsDir(), agentId, 'sessions');
166
+ function parseSessionIndex(agentId, pluginConfig) {
167
+ const sessionsDir = path.join(getOpenClawAgentsDir(pluginConfig), agentId, 'sessions');
160
168
  const sessionsJsonPath = path.join(sessionsDir, 'sessions.json');
161
169
  if (!fs.existsSync(sessionsJsonPath)) {
162
170
  return { sessionsDir, index: {} };
@@ -173,9 +181,9 @@ function parseSessionIndex(agentId) {
173
181
  }
174
182
  }
175
183
 
176
- function shouldObserveActiveSessions(vaultPath, agentId) {
184
+ function shouldObserveActiveSessions(vaultPath, agentId, pluginConfig) {
177
185
  const cursors = loadObserveCursors(vaultPath);
178
- const { sessionsDir, index } = parseSessionIndex(agentId);
186
+ const { sessionsDir, index } = parseSessionIndex(agentId, pluginConfig);
179
187
  const entries = Object.entries(index);
180
188
  if (entries.length === 0) {
181
189
  return false;
@@ -457,6 +465,8 @@ function extractPluginConfig(event) {
457
465
  const candidates = [
458
466
  event?.pluginConfig,
459
467
  event?.context?.pluginConfig,
468
+ event?.config?.plugins?.entries?.clawvault?.config,
469
+ event?.context?.config?.plugins?.entries?.clawvault?.config,
460
470
  event?.config?.plugins?.clawvault?.config,
461
471
  event?.context?.config?.plugins?.clawvault?.config
462
472
  ];
@@ -470,6 +480,31 @@ function extractPluginConfig(event) {
470
480
  return {};
471
481
  }
472
482
 
483
+ function isOptInEnabled(pluginConfig, ...keys) {
484
+ for (const key of keys) {
485
+ if (pluginConfig?.[key] === true) return true;
486
+ }
487
+ return false;
488
+ }
489
+
490
+ function allowsEnvAccess(pluginConfig) {
491
+ return isOptInEnabled(pluginConfig, 'allowEnvAccess');
492
+ }
493
+
494
+ function getConfiguredExecutablePath(pluginConfig) {
495
+ const value = pluginConfig?.clawvaultBinaryPath;
496
+ if (typeof value !== 'string') return null;
497
+ const trimmed = value.trim();
498
+ return trimmed || null;
499
+ }
500
+
501
+ function getConfiguredExecutableSha256(pluginConfig) {
502
+ const value = pluginConfig?.clawvaultBinarySha256;
503
+ if (typeof value !== 'string') return null;
504
+ const trimmed = value.trim().toLowerCase();
505
+ return trimmed || null;
506
+ }
507
+
473
508
  // Resolve vault path for a specific agent from agentVaults config
474
509
  function resolveAgentVaultPath(pluginConfig, agentId) {
475
510
  if (!agentId || typeof agentId !== 'string') return null;
@@ -487,11 +522,9 @@ function resolveAgentVaultPath(pluginConfig, agentId) {
487
522
 
488
523
  // Find vault by walking up directories
489
524
  // Supports per-agent vault paths via agentVaults config
490
- function findVaultPath(event, options = {}) {
491
- const pluginConfig = extractPluginConfig(event);
492
-
525
+ function findVaultPath(event, pluginConfig, options = {}) {
493
526
  // Determine agent ID for per-agent vault resolution
494
- const agentId = options.agentId || resolveAgentIdForEvent(event);
527
+ const agentId = options.agentId || resolveAgentIdForEvent(event, pluginConfig);
495
528
 
496
529
  // Check agentVaults first (per-agent vault paths)
497
530
  if (agentId) {
@@ -508,15 +541,17 @@ function findVaultPath(event, options = {}) {
508
541
  if (validated) return validated;
509
542
  }
510
543
 
511
- // Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
512
- if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
513
- const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
514
- if (validated) return validated;
515
- }
544
+ if (allowsEnvAccess(pluginConfig)) {
545
+ // Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
546
+ if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
547
+ const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
548
+ if (validated) return validated;
549
+ }
516
550
 
517
- // Check CLAWVAULT_PATH env
518
- if (process.env.CLAWVAULT_PATH) {
519
- return validateVaultPath(process.env.CLAWVAULT_PATH);
551
+ // Check CLAWVAULT_PATH env
552
+ if (process.env.CLAWVAULT_PATH) {
553
+ return validateVaultPath(process.env.CLAWVAULT_PATH);
554
+ }
520
555
  }
521
556
 
522
557
  // Walk up from cwd
@@ -539,16 +574,54 @@ function findVaultPath(event, options = {}) {
539
574
  }
540
575
 
541
576
  // Run clawvault command safely (no shell)
542
- function runClawvault(args, options = {}) {
577
+ function runClawvault(args, pluginConfig, options = {}) {
578
+ if (!isOptInEnabled(pluginConfig, 'allowClawvaultExec')) {
579
+ return {
580
+ success: false,
581
+ skipped: true,
582
+ output: 'ClawVault CLI execution is disabled. Set allowClawvaultExec=true to enable.',
583
+ code: 0
584
+ };
585
+ }
586
+
543
587
  const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 15000;
588
+ const executablePath = resolveExecutablePath(CLAWVAULT_EXECUTABLE, {
589
+ explicitPath: getConfiguredExecutablePath(pluginConfig)
590
+ });
591
+ if (!executablePath) {
592
+ return {
593
+ success: false,
594
+ output: 'Unable to resolve clawvault executable path. Set clawvaultBinaryPath to an absolute executable path.',
595
+ code: 1
596
+ };
597
+ }
598
+
599
+ const expectedSha256 = getConfiguredExecutableSha256(pluginConfig);
600
+ const integrityResult = verifyExecutableIntegrity(executablePath, expectedSha256);
601
+ if (!integrityResult.ok) {
602
+ return {
603
+ success: false,
604
+ output: `Executable integrity verification failed for ${executablePath}.`,
605
+ code: 1
606
+ };
607
+ }
608
+
609
+ let sanitizedArgs;
544
610
  try {
545
- // Use execFileSync to avoid shell injection
546
- // Arguments are passed as array, not interpolated into shell
547
- const output = execFileSync('clawvault', args, {
611
+ sanitizedArgs = sanitizeExecArgs(args);
612
+ } catch (err) {
613
+ return {
614
+ success: false,
615
+ output: err?.message || 'Invalid command arguments',
616
+ code: 1
617
+ };
618
+ }
619
+
620
+ try {
621
+ const output = execFileSync(executablePath, sanitizedArgs, {
548
622
  encoding: 'utf-8',
549
623
  timeout: timeoutMs,
550
624
  stdio: ['pipe', 'pipe', 'pipe'],
551
- // Explicitly no shell
552
625
  shell: false
553
626
  });
554
627
  return { success: true, output: output.trim(), code: 0 };
@@ -586,23 +659,29 @@ function parseRecoveryOutput(output) {
586
659
  return { hadDeath, workingOn };
587
660
  }
588
661
 
589
- function resolveAgentIdForEvent(event) {
662
+ function resolveAgentIdForEvent(event, pluginConfig) {
590
663
  const fromSessionKey = extractAgentIdFromSessionKey(extractSessionKey(event));
591
664
  if (fromSessionKey) return fromSessionKey;
592
665
 
593
- const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
594
- if (fromEnv) return fromEnv;
666
+ if (allowsEnvAccess(pluginConfig)) {
667
+ const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
668
+ if (fromEnv) return fromEnv;
669
+ }
595
670
 
596
671
  return 'main';
597
672
  }
598
673
 
599
- function runObserverCron(vaultPath, agentId, options = {}) {
674
+ function runObserverCron(vaultPath, agentId, pluginConfig, options = {}) {
600
675
  const args = ['observe', '--cron', '--agent', agentId, '-v', vaultPath];
601
676
  if (Number.isFinite(options.minNewBytes) && Number(options.minNewBytes) > 0) {
602
677
  args.push('--min-new', String(Math.floor(Number(options.minNewBytes))));
603
678
  }
604
679
 
605
- const result = runClawvault(args, { timeoutMs: 120000 });
680
+ const result = runClawvault(args, pluginConfig, { timeoutMs: 120000 });
681
+ if (result.skipped) {
682
+ console.log('[clawvault] Observer cron skipped: allowClawvaultExec is disabled');
683
+ return false;
684
+ }
606
685
  if (!result.success) {
607
686
  console.warn(`[clawvault] Observer cron failed (${options.reason || 'unknown reason'})`);
608
687
  return false;
@@ -1312,7 +1391,12 @@ function isSundayMidnightUtc(date) {
1312
1391
  }
1313
1392
 
1314
1393
  async function handleWeeklyReflect(event) {
1315
- const vaultPath = findVaultPath(event);
1394
+ const pluginConfig = extractPluginConfig(event);
1395
+ if (!isOptInEnabled(pluginConfig, 'enableWeeklyReflection', 'weeklyReflection')) {
1396
+ return;
1397
+ }
1398
+
1399
+ const vaultPath = findVaultPath(event, pluginConfig);
1316
1400
  if (!vaultPath) {
1317
1401
  console.log('[clawvault] No vault found, skipping weekly reflection');
1318
1402
  return;
@@ -1324,7 +1408,11 @@ async function handleWeeklyReflect(event) {
1324
1408
  return;
1325
1409
  }
1326
1410
 
1327
- const result = runClawvault(['reflect', '-v', vaultPath], { timeoutMs: 120000 });
1411
+ const result = runClawvault(['reflect', '-v', vaultPath], pluginConfig, { timeoutMs: 120000 });
1412
+ if (result.skipped) {
1413
+ console.log('[clawvault] Weekly reflection skipped: allowClawvaultExec is disabled');
1414
+ return;
1415
+ }
1328
1416
  if (!result.success) {
1329
1417
  console.warn('[clawvault] Weekly reflection failed');
1330
1418
  return;
@@ -1334,7 +1422,12 @@ async function handleWeeklyReflect(event) {
1334
1422
 
1335
1423
  // Handle gateway startup - check for context death
1336
1424
  async function handleStartup(event) {
1337
- const vaultPath = findVaultPath(event);
1425
+ const pluginConfig = extractPluginConfig(event);
1426
+ if (!isOptInEnabled(pluginConfig, 'enableStartupRecovery')) {
1427
+ return;
1428
+ }
1429
+
1430
+ const vaultPath = findVaultPath(event, pluginConfig);
1338
1431
  if (!vaultPath) {
1339
1432
  console.log('[clawvault] No vault found, skipping recovery check');
1340
1433
  return;
@@ -1343,7 +1436,11 @@ async function handleStartup(event) {
1343
1436
  console.log(`[clawvault] Checking for context death`);
1344
1437
 
1345
1438
  // Pass vault path as separate argument (not interpolated)
1346
- const result = runClawvault(['recover', '--clear', '-v', vaultPath]);
1439
+ const result = runClawvault(['recover', '--clear', '-v', vaultPath], pluginConfig);
1440
+ if (result.skipped) {
1441
+ console.log('[clawvault] Recovery check skipped: allowClawvaultExec is disabled');
1442
+ return;
1443
+ }
1347
1444
 
1348
1445
  if (!result.success) {
1349
1446
  console.warn('[clawvault] Recovery check failed');
@@ -1373,7 +1470,15 @@ async function handleStartup(event) {
1373
1470
 
1374
1471
  // Handle /new command - auto-checkpoint before reset
1375
1472
  async function handleNew(event) {
1376
- const vaultPath = findVaultPath(event);
1473
+ const pluginConfig = extractPluginConfig(event);
1474
+ const autoCheckpointEnabled = isOptInEnabled(pluginConfig, 'enableAutoCheckpoint', 'autoCheckpoint');
1475
+ const observerOnNewEnabled = isOptInEnabled(pluginConfig, 'enableObserveOnNew');
1476
+ const factExtractionEnabled = isOptInEnabled(pluginConfig, 'enableFactExtraction');
1477
+ if (!autoCheckpointEnabled && !observerOnNewEnabled && !factExtractionEnabled) {
1478
+ return;
1479
+ }
1480
+
1481
+ const vaultPath = findVaultPath(event, pluginConfig);
1377
1482
  if (!vaultPath) {
1378
1483
  console.log('[clawvault] No vault found, skipping auto-checkpoint');
1379
1484
  return;
@@ -1387,33 +1492,44 @@ async function handleNew(event) {
1387
1492
  ? event.context.commandSource.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 50)
1388
1493
  : 'cli';
1389
1494
 
1390
- console.log('[clawvault] Auto-checkpoint before /new');
1391
-
1392
- // Pass each argument separately (no shell interpolation)
1393
- const result = runClawvault([
1394
- 'checkpoint',
1395
- '--working-on', `Session reset via /new from ${source}`,
1396
- '--focus', `Pre-reset checkpoint, session: ${sessionKey}`,
1397
- '-v', vaultPath
1398
- ]);
1495
+ if (autoCheckpointEnabled) {
1496
+ console.log('[clawvault] Auto-checkpoint before /new');
1497
+ const result = runClawvault([
1498
+ 'checkpoint',
1499
+ '--working-on', `Session reset via /new from ${source}`,
1500
+ '--focus', `Pre-reset checkpoint, session: ${sessionKey}`,
1501
+ '-v', vaultPath
1502
+ ], pluginConfig);
1399
1503
 
1400
- if (result.success) {
1401
- console.log('[clawvault] Auto-checkpoint created');
1402
- } else {
1403
- console.warn('[clawvault] Auto-checkpoint failed');
1504
+ if (result.skipped) {
1505
+ console.log('[clawvault] Auto-checkpoint skipped: allowClawvaultExec is disabled');
1506
+ } else if (result.success) {
1507
+ console.log('[clawvault] Auto-checkpoint created');
1508
+ } else {
1509
+ console.warn('[clawvault] Auto-checkpoint failed');
1510
+ }
1404
1511
  }
1405
1512
 
1406
- const agentId = resolveAgentIdForEvent(event);
1407
- runObserverCron(vaultPath, agentId, {
1408
- minNewBytes: 1,
1409
- reason: 'command:new flush'
1410
- });
1411
- runFactExtractionForEvent(vaultPath, event, 'command:new');
1513
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1514
+ if (observerOnNewEnabled) {
1515
+ runObserverCron(vaultPath, agentId, pluginConfig, {
1516
+ minNewBytes: 1,
1517
+ reason: 'command:new flush'
1518
+ });
1519
+ }
1520
+ if (factExtractionEnabled) {
1521
+ runFactExtractionForEvent(vaultPath, event, 'command:new');
1522
+ }
1412
1523
  }
1413
1524
 
1414
1525
  // Handle session start - inject dynamic context for first prompt
1415
1526
  async function handleSessionStart(event) {
1416
- const vaultPath = findVaultPath(event);
1527
+ const pluginConfig = extractPluginConfig(event);
1528
+ if (!isOptInEnabled(pluginConfig, 'enableSessionContextInjection')) {
1529
+ return;
1530
+ }
1531
+
1532
+ const vaultPath = findVaultPath(event, pluginConfig);
1417
1533
  if (!vaultPath) {
1418
1534
  console.log('[clawvault] No vault found, skipping context injection');
1419
1535
  return;
@@ -1432,11 +1548,14 @@ async function handleSessionStart(event) {
1432
1548
  recapArgs.push('--agent', agentId);
1433
1549
  }
1434
1550
 
1435
- const recapResult = runClawvault(recapArgs);
1436
- if (!recapResult.success) {
1437
- console.warn('[clawvault] Session recap lookup failed');
1438
- } else {
1551
+ const recapResult = runClawvault(recapArgs, pluginConfig);
1552
+ if (recapResult.skipped) {
1553
+ console.log('[clawvault] Session recap skipped: allowClawvaultExec is disabled');
1554
+ }
1555
+ if (recapResult.success) {
1439
1556
  recapEntries = parseSessionRecapJson(recapResult.output);
1557
+ } else if (!recapResult.skipped) {
1558
+ console.warn('[clawvault] Session recap lookup failed');
1440
1559
  }
1441
1560
  } else {
1442
1561
  console.log('[clawvault] No session key found, skipping session recap');
@@ -1450,12 +1569,14 @@ async function handleSessionStart(event) {
1450
1569
  '--format', 'json',
1451
1570
  '--profile', 'auto',
1452
1571
  '-v', vaultPath
1453
- ]);
1572
+ ], pluginConfig);
1454
1573
 
1455
- if (!contextResult.success) {
1456
- console.warn('[clawvault] Context lookup failed');
1457
- } else {
1574
+ if (contextResult.success) {
1458
1575
  memoryEntries = parseContextJson(contextResult.output);
1576
+ } else if (contextResult.skipped) {
1577
+ console.log('[clawvault] Context lookup skipped: allowClawvaultExec is disabled');
1578
+ } else {
1579
+ console.warn('[clawvault] Context lookup failed');
1459
1580
  }
1460
1581
  } else {
1461
1582
  console.log('[clawvault] No initial prompt, skipping vault memory lookup');
@@ -1475,35 +1596,51 @@ async function handleSessionStart(event) {
1475
1596
 
1476
1597
  // Handle heartbeat events - cheap stat-based trigger for active observation
1477
1598
  async function handleHeartbeat(event) {
1478
- const vaultPath = findVaultPath(event);
1599
+ const pluginConfig = extractPluginConfig(event);
1600
+ if (!isOptInEnabled(pluginConfig, 'enableHeartbeatObservation', 'observeOnHeartbeat')) {
1601
+ return;
1602
+ }
1603
+
1604
+ const vaultPath = findVaultPath(event, pluginConfig);
1479
1605
  if (!vaultPath) {
1480
1606
  console.log('[clawvault] No vault found, skipping heartbeat observation check');
1481
1607
  return;
1482
1608
  }
1483
1609
 
1484
- const agentId = resolveAgentIdForEvent(event);
1485
- if (!shouldObserveActiveSessions(vaultPath, agentId)) {
1610
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1611
+ if (!shouldObserveActiveSessions(vaultPath, agentId, pluginConfig)) {
1486
1612
  console.log('[clawvault] Heartbeat: no sessions crossed active-observe threshold');
1487
1613
  return;
1488
1614
  }
1489
1615
 
1490
- runObserverCron(vaultPath, agentId, { reason: 'heartbeat threshold crossed' });
1616
+ runObserverCron(vaultPath, agentId, pluginConfig, { reason: 'heartbeat threshold crossed' });
1491
1617
  }
1492
1618
 
1493
1619
  // Handle context compaction - force flush any pending session deltas
1494
1620
  async function handleContextCompaction(event) {
1495
- const vaultPath = findVaultPath(event);
1621
+ const pluginConfig = extractPluginConfig(event);
1622
+ const compactionObserveEnabled = isOptInEnabled(pluginConfig, 'enableCompactionObservation');
1623
+ const factExtractionEnabled = isOptInEnabled(pluginConfig, 'enableFactExtraction');
1624
+ if (!compactionObserveEnabled && !factExtractionEnabled) {
1625
+ return;
1626
+ }
1627
+
1628
+ const vaultPath = findVaultPath(event, pluginConfig);
1496
1629
  if (!vaultPath) {
1497
1630
  console.log('[clawvault] No vault found, skipping compaction observation');
1498
1631
  return;
1499
1632
  }
1500
1633
 
1501
- const agentId = resolveAgentIdForEvent(event);
1502
- runObserverCron(vaultPath, agentId, {
1503
- minNewBytes: 1,
1504
- reason: 'context compaction'
1505
- });
1506
- runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
1634
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1635
+ if (compactionObserveEnabled) {
1636
+ runObserverCron(vaultPath, agentId, pluginConfig, {
1637
+ minNewBytes: 1,
1638
+ reason: 'context compaction'
1639
+ });
1640
+ }
1641
+ if (factExtractionEnabled) {
1642
+ runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
1643
+ }
1507
1644
  }
1508
1645
 
1509
1646
  // Main handler - route events