clawvault 3.2.0 → 3.3.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.
- package/README.md +54 -14
- package/bin/clawvault.js +0 -2
- package/bin/command-registration.test.js +13 -1
- package/bin/help-contract.test.js +14 -0
- package/bin/register-core-commands.js +88 -0
- package/bin/register-core-commands.test.js +80 -0
- package/bin/register-maintenance-commands.js +57 -6
- package/bin/register-query-commands.js +10 -28
- package/bin/test-helpers/cli-command-fixtures.js +1 -0
- package/dist/chunk-2PKBIKDH.js +130 -0
- package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
- package/dist/{chunk-77Q5CSPJ.js → chunk-7SWP5FKU.js} +33 -701
- package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
- package/dist/{chunk-23YDQ3QU.js → chunk-BLQXXX7Q.js} +6 -6
- package/dist/chunk-CSHO3PJB.js +684 -0
- package/dist/{chunk-SLXOR3CC.js → chunk-DOIUYIXV.js} +2 -2
- package/dist/{chunk-NCKFNBHJ.js → chunk-DVOUSOR3.js} +79 -5
- package/dist/{chunk-CLJTREDS.js → chunk-ECGJYWNA.js} +193 -41
- package/dist/{chunk-BUEW6IIK.js → chunk-EL6UBSX5.js} +5 -5
- package/dist/{chunk-6FH3IULF.js → chunk-FZ5I2NF7.js} +1 -1
- package/dist/{chunk-ZN54U2OZ.js → chunk-GFCHWMGD.js} +3 -3
- package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
- package/dist/chunk-H3JZIB5O.js +322 -0
- package/dist/chunk-HEHO7SMV.js +51 -0
- package/dist/{chunk-STCQGCEQ.js → chunk-HGDDW24U.js} +3 -3
- package/dist/chunk-J3YUXVID.js +907 -0
- package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
- package/dist/{chunk-W4SPAEE7.js → chunk-OFOCU2V4.js} +5 -4
- package/dist/chunk-PTWPPVC7.js +972 -0
- package/dist/{chunk-QSHD36LH.js → chunk-QFWERBDP.js} +2 -2
- package/dist/{chunk-QSRRMEYM.js → chunk-S7N7HI5E.js} +1 -1
- package/dist/{chunk-PBACDKKP.js → chunk-T7E764W3.js} +3 -3
- package/dist/chunk-TDWFBDAQ.js +1016 -0
- package/dist/{chunk-ESVS6K2B.js → chunk-TWMI3SNN.js} +6 -5
- package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
- package/dist/{chunk-ESFLMDRB.js → chunk-VXAGOLDP.js} +3 -3
- package/dist/chunk-YCUVAOFC.js +158 -0
- package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
- package/dist/chunk-ZKWPCBYT.js +600 -0
- package/dist/cli/index.js +24 -24
- package/dist/commands/archive.js +2 -2
- package/dist/commands/benchmark.d.ts +12 -0
- package/dist/commands/benchmark.js +12 -0
- package/dist/commands/context.js +6 -5
- package/dist/commands/doctor.d.ts +8 -3
- package/dist/commands/doctor.js +6 -20
- package/dist/commands/embed.js +5 -4
- package/dist/commands/entities.js +1 -1
- package/dist/commands/graph.js +2 -2
- package/dist/commands/inbox.d.ts +23 -0
- package/dist/commands/inbox.js +11 -0
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +3 -3
- package/dist/commands/link.js +6 -6
- package/dist/commands/maintain.d.ts +32 -0
- package/dist/commands/maintain.js +12 -0
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.js +9 -8
- package/dist/commands/rebuild-embeddings.js +47 -16
- package/dist/commands/rebuild.js +7 -6
- package/dist/commands/reflect.js +5 -5
- package/dist/commands/replay.js +8 -7
- package/dist/commands/setup.js +3 -2
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +17 -15
- package/dist/commands/status.js +26 -24
- package/dist/commands/sync-bd.js +2 -2
- package/dist/commands/tailscale.js +2 -2
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +8 -7
- package/dist/index.d.ts +168 -16
- package/dist/index.js +271 -108
- package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
- package/dist/lib/config.js +1 -1
- package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
- package/hooks/clawvault/HOOK.md +22 -5
- package/hooks/clawvault/handler.js +213 -78
- package/hooks/clawvault/handler.test.js +109 -43
- package/hooks/clawvault/integrity.js +112 -0
- package/hooks/clawvault/integrity.test.js +32 -0
- package/hooks/clawvault/openclaw.plugin.json +133 -15
- package/openclaw.plugin.json +126 -20
- package/package.json +2 -2
- package/bin/register-workgraph-commands.js +0 -1368
- package/dist/chunk-33VSQP4J.js +0 -37
- package/dist/chunk-4BQTQMJP.js +0 -93
- package/dist/chunk-EK6S23ZB.js +0 -469
- package/dist/chunk-GAOWA7GR.js +0 -501
- package/dist/chunk-GGA32J2R.js +0 -784
- package/dist/chunk-MM6QGW3P.js +0 -207
- package/dist/chunk-QVEERJSP.js +0 -152
- package/dist/chunk-U4O6C46S.js +0 -154
- package/dist/chunk-VSL7KY3M.js +0 -189
- package/dist/chunk-WMGIIABP.js +0 -15
- package/dist/commands/workgraph.d.ts +0 -124
- package/dist/commands/workgraph.js +0 -38
- package/dist/ledger-B7g7jhqG.d.ts +0 -44
- package/dist/registry-BR4326o0.d.ts +0 -30
- package/dist/store-CA-6sKCJ.d.ts +0 -34
- package/dist/thread-B9LhXNU0.d.ts +0 -41
- package/dist/workgraph/index.d.ts +0 -5
- package/dist/workgraph/index.js +0 -23
- package/dist/workgraph/ledger.d.ts +0 -2
- package/dist/workgraph/ledger.js +0 -25
- package/dist/workgraph/registry.d.ts +0 -2
- package/dist/workgraph/registry.js +0 -19
- package/dist/workgraph/store.d.ts +0 -2
- package/dist/workgraph/store.js +0 -25
- package/dist/workgraph/thread.d.ts +0 -2
- package/dist/workgraph/thread.js +0 -25
- package/dist/workgraph/types.d.ts +0 -54
- 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
|
+
});
|