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
@@ -1,16 +1,25 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
 
6
- const { execFileSyncMock } = vi.hoisted(() => ({
7
- execFileSyncMock: vi.fn()
6
+ const { execFileSyncMock, resolveExecutablePathMock, verifyExecutableIntegrityMock, sanitizeExecArgsMock } = vi.hoisted(() => ({
7
+ execFileSyncMock: vi.fn(),
8
+ resolveExecutablePathMock: vi.fn(),
9
+ verifyExecutableIntegrityMock: vi.fn(),
10
+ sanitizeExecArgsMock: vi.fn()
8
11
  }));
9
12
 
10
13
  vi.mock('child_process', () => ({
11
14
  execFileSync: execFileSyncMock
12
15
  }));
13
16
 
17
+ vi.mock('./integrity.js', () => ({
18
+ resolveExecutablePath: resolveExecutablePathMock,
19
+ verifyExecutableIntegrity: verifyExecutableIntegrityMock,
20
+ sanitizeExecArgs: sanitizeExecArgsMock
21
+ }));
22
+
14
23
  function makeVaultFixture() {
15
24
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-hook-'));
16
25
  fs.writeFileSync(path.join(root, '.clawvault.json'), JSON.stringify({ name: 'test' }), 'utf-8');
@@ -45,16 +54,45 @@ async function loadHandler() {
45
54
 
46
55
  afterEach(() => {
47
56
  vi.clearAllMocks();
57
+ resolveExecutablePathMock.mockReset();
58
+ verifyExecutableIntegrityMock.mockReset();
59
+ sanitizeExecArgsMock.mockReset();
48
60
  delete process.env.CLAWVAULT_PATH;
49
61
  delete process.env.OPENCLAW_STATE_DIR;
50
62
  delete process.env.OPENCLAW_HOME;
51
63
  delete process.env.OPENCLAW_AGENT_ID;
64
+ delete process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH;
52
65
  });
53
66
 
67
+ function securePluginConfig(vaultPath, overrides = {}) {
68
+ return {
69
+ vaultPath,
70
+ allowClawvaultExec: true,
71
+ enableStartupRecovery: true,
72
+ enableSessionContextInjection: true,
73
+ enableAutoCheckpoint: true,
74
+ enableObserveOnNew: true,
75
+ enableHeartbeatObservation: true,
76
+ enableCompactionObservation: true,
77
+ enableWeeklyReflection: true,
78
+ enableFactExtraction: true,
79
+ ...overrides
80
+ };
81
+ }
82
+
83
+ function setupIntegrityDefaults() {
84
+ resolveExecutablePathMock.mockReturnValue('/usr/local/bin/clawvault');
85
+ verifyExecutableIntegrityMock.mockReturnValue({ ok: true, actualSha256: 'a'.repeat(64) });
86
+ sanitizeExecArgsMock.mockImplementation((args) => args);
87
+ }
88
+
54
89
  describe('clawvault hook handler', () => {
90
+ beforeEach(() => {
91
+ setupIntegrityDefaults();
92
+ });
93
+
55
94
  it('injects recovery warning on gateway startup when death detected', async () => {
56
95
  const vaultPath = makeVaultFixture();
57
- process.env.CLAWVAULT_PATH = vaultPath;
58
96
 
59
97
  execFileSyncMock.mockImplementation((_command, args) => {
60
98
  if (args[0] === 'recover') {
@@ -67,13 +105,14 @@ describe('clawvault hook handler', () => {
67
105
  const event = {
68
106
  type: 'gateway',
69
107
  action: 'startup',
108
+ pluginConfig: securePluginConfig(vaultPath),
70
109
  messages: [{ role: 'user', content: 'hello' }]
71
110
  };
72
111
 
73
112
  await handler(event);
74
113
 
75
114
  expect(execFileSyncMock).toHaveBeenCalledWith(
76
- 'clawvault',
115
+ '/usr/local/bin/clawvault',
77
116
  expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
78
117
  expect.objectContaining({ shell: false })
79
118
  );
@@ -84,20 +123,52 @@ describe('clawvault hook handler', () => {
84
123
  fs.rmSync(vaultPath, { recursive: true, force: true });
85
124
  });
86
125
 
126
+ it('does not execute clawvault commands unless allowClawvaultExec is true', async () => {
127
+ const vaultPath = makeVaultFixture();
128
+ const handler = await loadHandler();
129
+ const event = {
130
+ type: 'gateway',
131
+ action: 'startup',
132
+ pluginConfig: securePluginConfig(vaultPath, { allowClawvaultExec: false }),
133
+ messages: []
134
+ };
135
+
136
+ await handler(event);
137
+ expect(execFileSyncMock).not.toHaveBeenCalled();
138
+ fs.rmSync(vaultPath, { recursive: true, force: true });
139
+ });
140
+
141
+ it('fails closed when configured executable hash does not match', async () => {
142
+ const vaultPath = makeVaultFixture();
143
+ verifyExecutableIntegrityMock.mockReturnValue({ ok: false, actualSha256: 'b'.repeat(64) });
144
+ const handler = await loadHandler();
145
+ await handler({
146
+ type: 'gateway',
147
+ action: 'startup',
148
+ pluginConfig: securePluginConfig(vaultPath, {
149
+ clawvaultBinarySha256: 'a'.repeat(64)
150
+ }),
151
+ messages: []
152
+ });
153
+
154
+ expect(execFileSyncMock).not.toHaveBeenCalled();
155
+ fs.rmSync(vaultPath, { recursive: true, force: true });
156
+ });
157
+
87
158
  it('supports alias event names for command:new', async () => {
88
159
  const vaultPath = makeVaultFixture();
89
- process.env.CLAWVAULT_PATH = vaultPath;
90
160
  execFileSyncMock.mockReturnValue('');
91
161
 
92
162
  const handler = await loadHandler();
93
163
  await handler({
94
164
  event: 'command:new',
95
165
  sessionKey: 'agent:clawdious:main',
166
+ pluginConfig: securePluginConfig(vaultPath),
96
167
  context: { commandSource: 'cli' }
97
168
  });
98
169
 
99
170
  expect(execFileSyncMock).toHaveBeenCalledWith(
100
- 'clawvault',
171
+ '/usr/local/bin/clawvault',
101
172
  expect.arrayContaining(['checkpoint', '--working-on']),
102
173
  expect.objectContaining({ shell: false })
103
174
  );
@@ -107,7 +178,6 @@ describe('clawvault hook handler', () => {
107
178
 
108
179
  it('injects recap and memory context on session start alias event', async () => {
109
180
  const vaultPath = makeVaultFixture();
110
- process.env.CLAWVAULT_PATH = vaultPath;
111
181
 
112
182
  execFileSyncMock.mockImplementation((_command, args) => {
113
183
  if (args[0] === 'session-recap') {
@@ -136,6 +206,7 @@ describe('clawvault hook handler', () => {
136
206
  const event = {
137
207
  eventName: 'session:start',
138
208
  sessionKey: 'agent:clawdious:main',
209
+ pluginConfig: securePluginConfig(vaultPath),
139
210
  context: { initialPrompt: 'Need migration plan' },
140
211
  messages: [{ role: 'user', content: 'Need migration plan' }]
141
212
  };
@@ -156,7 +227,6 @@ describe('clawvault hook handler', () => {
156
227
 
157
228
  it('delegates profile selection to context auto mode for urgent prompts', async () => {
158
229
  const vaultPath = makeVaultFixture();
159
- process.env.CLAWVAULT_PATH = vaultPath;
160
230
 
161
231
  execFileSyncMock.mockImplementation((_command, args) => {
162
232
  if (args[0] === 'session-recap') {
@@ -172,6 +242,7 @@ describe('clawvault hook handler', () => {
172
242
  await handler({
173
243
  eventName: 'session:start',
174
244
  sessionKey: 'agent:clawdious:main',
245
+ pluginConfig: securePluginConfig(vaultPath),
175
246
  context: { initialPrompt: 'URGENT outage: rollback failed in production' },
176
247
  messages: [{ role: 'user', content: 'URGENT outage: rollback failed in production' }]
177
248
  });
@@ -186,7 +257,6 @@ describe('clawvault hook handler', () => {
186
257
  const vaultPath = makeVaultFixture();
187
258
  const sessionId = 'heartbeat-session-1';
188
259
  const openClawFixture = makeOpenClawSessionFixture('main', sessionId, 70 * 1024);
189
- process.env.CLAWVAULT_PATH = vaultPath;
190
260
  process.env.OPENCLAW_STATE_DIR = openClawFixture.stateRoot;
191
261
 
192
262
  fs.mkdirSync(path.join(vaultPath, '.clawvault'), { recursive: true });
@@ -208,11 +278,12 @@ describe('clawvault hook handler', () => {
208
278
  const handler = await loadHandler();
209
279
  await handler({
210
280
  type: 'gateway',
211
- action: 'heartbeat'
281
+ action: 'heartbeat',
282
+ pluginConfig: securePluginConfig(vaultPath, { allowEnvAccess: true })
212
283
  });
213
284
 
214
285
  expect(execFileSyncMock).toHaveBeenCalledWith(
215
- 'clawvault',
286
+ '/usr/local/bin/clawvault',
216
287
  expect.arrayContaining(['observe', '--cron', '--agent', 'main']),
217
288
  expect.objectContaining({ shell: false })
218
289
  );
@@ -223,17 +294,17 @@ describe('clawvault hook handler', () => {
223
294
 
224
295
  it('forces active observation flush on compaction events', async () => {
225
296
  const vaultPath = makeVaultFixture();
226
- process.env.CLAWVAULT_PATH = vaultPath;
227
297
  execFileSyncMock.mockReturnValue('');
228
298
 
229
299
  const handler = await loadHandler();
230
300
  await handler({
231
301
  eventName: 'compaction:memoryFlush',
232
- sessionKey: 'agent:clawdious:main'
302
+ sessionKey: 'agent:clawdious:main',
303
+ pluginConfig: securePluginConfig(vaultPath)
233
304
  });
234
305
 
235
306
  expect(execFileSyncMock).toHaveBeenCalledWith(
236
- 'clawvault',
307
+ '/usr/local/bin/clawvault',
237
308
  expect.arrayContaining(['observe', '--cron', '--min-new', '1']),
238
309
  expect.objectContaining({ shell: false })
239
310
  );
@@ -243,17 +314,17 @@ describe('clawvault hook handler', () => {
243
314
 
244
315
  it('runs weekly reflection on cron.weekly at Sunday midnight', async () => {
245
316
  const vaultPath = makeVaultFixture();
246
- process.env.CLAWVAULT_PATH = vaultPath;
247
317
  execFileSyncMock.mockReturnValue('');
248
318
 
249
319
  const handler = await loadHandler();
250
320
  await handler({
251
321
  eventName: 'cron.weekly',
252
- timestamp: '2026-02-15T00:00:00.000Z'
322
+ timestamp: '2026-02-15T00:00:00.000Z',
323
+ pluginConfig: securePluginConfig(vaultPath)
253
324
  });
254
325
 
255
326
  expect(execFileSyncMock).toHaveBeenCalledWith(
256
- 'clawvault',
327
+ '/usr/local/bin/clawvault',
257
328
  expect.arrayContaining(['reflect', '-v', vaultPath]),
258
329
  expect.objectContaining({ shell: false })
259
330
  );
@@ -275,16 +346,14 @@ describe('clawvault hook handler', () => {
275
346
  const event = {
276
347
  type: 'gateway',
277
348
  action: 'startup',
278
- pluginConfig: {
279
- vaultPath
280
- },
349
+ pluginConfig: securePluginConfig(vaultPath),
281
350
  messages: []
282
351
  };
283
352
 
284
353
  await handler(event);
285
354
 
286
355
  expect(execFileSyncMock).toHaveBeenCalledWith(
287
- 'clawvault',
356
+ '/usr/local/bin/clawvault',
288
357
  expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
289
358
  expect.objectContaining({ shell: false })
290
359
  );
@@ -307,9 +376,7 @@ describe('clawvault hook handler', () => {
307
376
  type: 'gateway',
308
377
  action: 'startup',
309
378
  context: {
310
- pluginConfig: {
311
- vaultPath
312
- }
379
+ pluginConfig: securePluginConfig(vaultPath)
313
380
  },
314
381
  messages: []
315
382
  };
@@ -317,7 +384,7 @@ describe('clawvault hook handler', () => {
317
384
  await handler(event);
318
385
 
319
386
  expect(execFileSyncMock).toHaveBeenCalledWith(
320
- 'clawvault',
387
+ '/usr/local/bin/clawvault',
321
388
  expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
322
389
  expect.objectContaining({ shell: false })
323
390
  );
@@ -340,13 +407,14 @@ describe('clawvault hook handler', () => {
340
407
  const event = {
341
408
  type: 'gateway',
342
409
  action: 'startup',
410
+ pluginConfig: securePluginConfig(vaultPath, { allowEnvAccess: true }),
343
411
  messages: []
344
412
  };
345
413
 
346
414
  await handler(event);
347
415
 
348
416
  expect(execFileSyncMock).toHaveBeenCalledWith(
349
- 'clawvault',
417
+ '/usr/local/bin/clawvault',
350
418
  expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
351
419
  expect.objectContaining({ shell: false })
352
420
  );
@@ -372,20 +440,19 @@ describe('clawvault hook handler', () => {
372
440
  type: 'gateway',
373
441
  action: 'startup',
374
442
  sessionKey: 'agent:agent1:main',
375
- pluginConfig: {
376
- vaultPath: fallbackVault,
443
+ pluginConfig: securePluginConfig(fallbackVault, {
377
444
  agentVaults: {
378
445
  agent1: agent1Vault,
379
446
  agent2: agent2Vault
380
447
  }
381
- },
448
+ }),
382
449
  messages: []
383
450
  };
384
451
 
385
452
  await handler(event);
386
453
 
387
454
  expect(execFileSyncMock).toHaveBeenCalledWith(
388
- 'clawvault',
455
+ '/usr/local/bin/clawvault',
389
456
  expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
390
457
  expect.objectContaining({ shell: false })
391
458
  );
@@ -411,19 +478,18 @@ describe('clawvault hook handler', () => {
411
478
  type: 'gateway',
412
479
  action: 'startup',
413
480
  sessionKey: 'agent:unknown-agent:main',
414
- pluginConfig: {
415
- vaultPath: fallbackVault,
481
+ pluginConfig: securePluginConfig(fallbackVault, {
416
482
  agentVaults: {
417
483
  agent1: agent1Vault
418
484
  }
419
- },
485
+ }),
420
486
  messages: []
421
487
  };
422
488
 
423
489
  await handler(event);
424
490
 
425
491
  expect(execFileSyncMock).toHaveBeenCalledWith(
426
- 'clawvault',
492
+ '/usr/local/bin/clawvault',
427
493
  expect.arrayContaining(['recover', '--clear', '-v', fallbackVault]),
428
494
  expect.objectContaining({ shell: false })
429
495
  );
@@ -449,12 +515,11 @@ describe('clawvault hook handler', () => {
449
515
  action: 'startup',
450
516
  sessionKey: 'agent:agent1:main',
451
517
  context: {
452
- pluginConfig: {
453
- vaultPath: fallbackVault,
518
+ pluginConfig: securePluginConfig(fallbackVault, {
454
519
  agentVaults: {
455
520
  agent1: agent1Vault
456
521
  }
457
- }
522
+ })
458
523
  },
459
524
  messages: []
460
525
  };
@@ -462,7 +527,7 @@ describe('clawvault hook handler', () => {
462
527
  await handler(event);
463
528
 
464
529
  expect(execFileSyncMock).toHaveBeenCalledWith(
465
- 'clawvault',
530
+ '/usr/local/bin/clawvault',
466
531
  expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
467
532
  expect.objectContaining({ shell: false })
468
533
  );
@@ -487,19 +552,20 @@ describe('clawvault hook handler', () => {
487
552
  const event = {
488
553
  type: 'gateway',
489
554
  action: 'startup',
490
- pluginConfig: {
555
+ pluginConfig: securePluginConfig(fallbackVault, {
491
556
  vaultPath: fallbackVault,
492
557
  agentVaults: {
493
558
  agent1: agent1Vault
494
- }
495
- },
559
+ },
560
+ allowEnvAccess: true
561
+ }),
496
562
  messages: []
497
563
  };
498
564
 
499
565
  await handler(event);
500
566
 
501
567
  expect(execFileSyncMock).toHaveBeenCalledWith(
502
- 'clawvault',
568
+ '/usr/local/bin/clawvault',
503
569
  expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
504
570
  expect.objectContaining({ shell: false })
505
571
  );
@@ -0,0 +1,112 @@
1
+ import { createHash } from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const WINDOWS_PATHEXT_DEFAULT = '.EXE;.CMD;.BAT;.COM';
6
+
7
+ function splitPathEnv(pathEnv) {
8
+ if (typeof pathEnv !== 'string' || !pathEnv.trim()) {
9
+ return [];
10
+ }
11
+ return pathEnv
12
+ .split(path.delimiter)
13
+ .map((entry) => entry.trim())
14
+ .filter(Boolean);
15
+ }
16
+
17
+ function listExecutableCandidates(commandName) {
18
+ if (process.platform !== 'win32') {
19
+ return [commandName];
20
+ }
21
+
22
+ const ext = path.extname(commandName);
23
+ if (ext) {
24
+ return [commandName];
25
+ }
26
+
27
+ const pathext = splitPathEnv(process.env.PATHEXT || WINDOWS_PATHEXT_DEFAULT)
28
+ .map((candidate) => candidate.toLowerCase());
29
+ if (pathext.length === 0) {
30
+ return [commandName];
31
+ }
32
+ return pathext.map((candidate) => `${commandName}${candidate}`);
33
+ }
34
+
35
+ function isExecutablePath(candidatePath) {
36
+ try {
37
+ const stats = fs.statSync(candidatePath);
38
+ if (!stats.isFile()) return false;
39
+ if (process.platform === 'win32') return true;
40
+ return (stats.mode & 0o111) !== 0;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function resolveFromPath(commandName, pathEnv) {
47
+ const candidates = listExecutableCandidates(commandName);
48
+ const directories = splitPathEnv(pathEnv);
49
+ for (const directory of directories) {
50
+ for (const candidate of candidates) {
51
+ const absoluteCandidate = path.resolve(directory, candidate);
52
+ if (isExecutablePath(absoluteCandidate)) {
53
+ return absoluteCandidate;
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export function resolveExecutablePath(commandName, options = {}) {
61
+ if (typeof commandName !== 'string' || !commandName.trim()) {
62
+ return null;
63
+ }
64
+
65
+ const explicitPath = typeof options.explicitPath === 'string'
66
+ ? options.explicitPath.trim()
67
+ : '';
68
+ if (explicitPath) {
69
+ const absolute = path.resolve(explicitPath);
70
+ return isExecutablePath(absolute) ? absolute : null;
71
+ }
72
+
73
+ if (commandName.includes(path.sep)) {
74
+ const absolute = path.resolve(commandName);
75
+ return isExecutablePath(absolute) ? absolute : null;
76
+ }
77
+
78
+ return resolveFromPath(commandName, process.env.PATH || '');
79
+ }
80
+
81
+ export function sanitizeExecArgs(args) {
82
+ if (!Array.isArray(args)) {
83
+ throw new Error('Arguments must be an array');
84
+ }
85
+ return args.map((value, index) => {
86
+ if (typeof value !== 'string') {
87
+ throw new Error(`Argument ${index} is not a string`);
88
+ }
89
+ if (value.includes('\0')) {
90
+ throw new Error(`Argument ${index} contains a null byte`);
91
+ }
92
+ return value;
93
+ });
94
+ }
95
+
96
+ export function verifyExecutableIntegrity(executablePath, expectedSha256) {
97
+ if (typeof expectedSha256 !== 'string' || !expectedSha256.trim()) {
98
+ return { ok: true, actualSha256: null };
99
+ }
100
+
101
+ const normalizedExpected = expectedSha256.trim().toLowerCase();
102
+ if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) {
103
+ return { ok: false, actualSha256: null };
104
+ }
105
+
106
+ const payload = fs.readFileSync(executablePath);
107
+ const actualSha256 = createHash('sha256').update(payload).digest('hex').toLowerCase();
108
+ return {
109
+ ok: actualSha256 === normalizedExpected,
110
+ actualSha256
111
+ };
112
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createHash } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import {
5
+ resolveExecutablePath,
6
+ sanitizeExecArgs,
7
+ verifyExecutableIntegrity
8
+ } from './integrity.js';
9
+
10
+ describe('hook executable integrity helpers', () => {
11
+ it('resolves an explicit executable path', () => {
12
+ const resolved = resolveExecutablePath('clawvault', { explicitPath: process.execPath });
13
+ expect(resolved).toBe(process.execPath);
14
+ });
15
+
16
+ it('rejects non-array arguments', () => {
17
+ expect(() => sanitizeExecArgs('not-an-array')).toThrow('Arguments must be an array');
18
+ });
19
+
20
+ it('rejects null-byte arguments', () => {
21
+ expect(() => sanitizeExecArgs(['ok', 'bad\0arg'])).toThrow('contains a null byte');
22
+ });
23
+
24
+ it('verifies expected executable sha256', () => {
25
+ const expected = createHash('sha256')
26
+ .update(fs.readFileSync(process.execPath))
27
+ .digest('hex');
28
+ const result = verifyExecutableIntegrity(process.execPath, expected);
29
+ expect(result.ok).toBe(true);
30
+ expect(result.actualSha256).toBe(expected);
31
+ });
32
+ });