clawvault 3.4.0 → 3.5.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/CHANGELOG.md +543 -0
- package/LICENSE +21 -0
- package/README.md +26 -26
- package/SKILL.md +369 -0
- package/dist/{chunk-X3SPPUFG.js → chunk-JI7VUQV7.js} +118 -132
- package/dist/{chunk-QYQAGBTM.js → chunk-QUFQBAHP.js} +148 -125
- package/dist/cli/index.js +1 -1
- package/dist/commands/compat.js +1 -1
- package/dist/commands/observe.js +1 -1
- package/dist/commands/status.js +4 -4
- package/dist/index.js +11 -8
- package/dist/openclaw-plugin.js +6 -1
- package/docs/clawhub-security-release-playbook.md +75 -0
- package/docs/getting-started/installation.md +99 -0
- package/docs/openclaw-plugin-usage.md +152 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +26 -8
- package/bin/command-registration.test.js +0 -179
- package/bin/command-runtime.test.js +0 -154
- package/bin/help-contract.test.js +0 -55
- package/bin/register-config-route-commands.test.js +0 -121
- package/bin/register-core-commands.test.js +0 -80
- package/bin/register-kanban-commands.test.js +0 -83
- package/bin/register-project-commands.test.js +0 -206
- package/bin/register-query-commands.test.js +0 -80
- package/bin/register-resilience-commands.test.js +0 -81
- package/bin/register-task-commands.test.js +0 -69
- package/bin/register-template-commands.test.js +0 -87
- package/bin/test-helpers/cli-command-fixtures.js +0 -120
- package/dashboard/lib/graph-diff.test.js +0 -75
- package/dashboard/lib/vault-parser.test.js +0 -254
- package/hooks/clawvault/HOOK.md +0 -130
- package/hooks/clawvault/handler.js +0 -1696
- package/hooks/clawvault/handler.test.js +0 -576
- package/hooks/clawvault/integrity.js +0 -112
- package/hooks/clawvault/integrity.test.js +0 -32
- package/hooks/clawvault/openclaw.plugin.json +0 -190
|
@@ -1,576 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
|
|
6
|
-
const { execFileSyncMock, resolveExecutablePathMock, verifyExecutableIntegrityMock, sanitizeExecArgsMock } = vi.hoisted(() => ({
|
|
7
|
-
execFileSyncMock: vi.fn(),
|
|
8
|
-
resolveExecutablePathMock: vi.fn(),
|
|
9
|
-
verifyExecutableIntegrityMock: vi.fn(),
|
|
10
|
-
sanitizeExecArgsMock: vi.fn()
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock('child_process', () => ({
|
|
14
|
-
execFileSync: execFileSyncMock
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('./integrity.js', () => ({
|
|
18
|
-
resolveExecutablePath: resolveExecutablePathMock,
|
|
19
|
-
verifyExecutableIntegrity: verifyExecutableIntegrityMock,
|
|
20
|
-
sanitizeExecArgs: sanitizeExecArgsMock
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
function makeVaultFixture() {
|
|
24
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-hook-'));
|
|
25
|
-
fs.writeFileSync(path.join(root, '.clawvault.json'), JSON.stringify({ name: 'test' }), 'utf-8');
|
|
26
|
-
return root;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeOpenClawSessionFixture(agentId, sessionId, transcriptBytes = 0) {
|
|
30
|
-
const stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-openclaw-'));
|
|
31
|
-
const sessionsDir = path.join(stateRoot, 'agents', agentId, 'sessions');
|
|
32
|
-
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
33
|
-
fs.writeFileSync(
|
|
34
|
-
path.join(sessionsDir, 'sessions.json'),
|
|
35
|
-
JSON.stringify({
|
|
36
|
-
[`agent:${agentId}:main`]: {
|
|
37
|
-
sessionId,
|
|
38
|
-
updatedAt: Date.now()
|
|
39
|
-
}
|
|
40
|
-
}),
|
|
41
|
-
'utf-8'
|
|
42
|
-
);
|
|
43
|
-
const transcriptPath = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
44
|
-
const payload = transcriptBytes > 0 ? 'x'.repeat(transcriptBytes) : '';
|
|
45
|
-
fs.writeFileSync(transcriptPath, payload, 'utf-8');
|
|
46
|
-
return { stateRoot, sessionsDir, transcriptPath };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function loadHandler() {
|
|
50
|
-
vi.resetModules();
|
|
51
|
-
const mod = await import('./handler.js');
|
|
52
|
-
return mod.default;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
vi.clearAllMocks();
|
|
57
|
-
resolveExecutablePathMock.mockReset();
|
|
58
|
-
verifyExecutableIntegrityMock.mockReset();
|
|
59
|
-
sanitizeExecArgsMock.mockReset();
|
|
60
|
-
delete process.env.CLAWVAULT_PATH;
|
|
61
|
-
delete process.env.OPENCLAW_STATE_DIR;
|
|
62
|
-
delete process.env.OPENCLAW_HOME;
|
|
63
|
-
delete process.env.OPENCLAW_AGENT_ID;
|
|
64
|
-
delete process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH;
|
|
65
|
-
});
|
|
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
|
-
|
|
89
|
-
describe('clawvault hook handler', () => {
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
setupIntegrityDefaults();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('injects recovery warning on gateway startup when death detected', async () => {
|
|
95
|
-
const vaultPath = makeVaultFixture();
|
|
96
|
-
|
|
97
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
98
|
-
if (args[0] === 'recover') {
|
|
99
|
-
return '⚠️ CONTEXT DEATH DETECTED\nWorking on: ship memory graph';
|
|
100
|
-
}
|
|
101
|
-
return '';
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const handler = await loadHandler();
|
|
105
|
-
const event = {
|
|
106
|
-
type: 'gateway',
|
|
107
|
-
action: 'startup',
|
|
108
|
-
pluginConfig: securePluginConfig(vaultPath),
|
|
109
|
-
messages: [{ role: 'user', content: 'hello' }]
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
await handler(event);
|
|
113
|
-
|
|
114
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
115
|
-
'/usr/local/bin/clawvault',
|
|
116
|
-
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
117
|
-
expect.objectContaining({ shell: false })
|
|
118
|
-
);
|
|
119
|
-
const injected = event.messages.find((message) => message.role === 'system');
|
|
120
|
-
expect(injected?.content).toContain('Context death detected');
|
|
121
|
-
expect(injected?.content).toContain('ship memory graph');
|
|
122
|
-
|
|
123
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
124
|
-
});
|
|
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
|
-
|
|
158
|
-
it('supports alias event names for command:new', async () => {
|
|
159
|
-
const vaultPath = makeVaultFixture();
|
|
160
|
-
execFileSyncMock.mockReturnValue('');
|
|
161
|
-
|
|
162
|
-
const handler = await loadHandler();
|
|
163
|
-
await handler({
|
|
164
|
-
event: 'command:new',
|
|
165
|
-
sessionKey: 'agent:clawdious:main',
|
|
166
|
-
pluginConfig: securePluginConfig(vaultPath),
|
|
167
|
-
context: { commandSource: 'cli' }
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
171
|
-
'/usr/local/bin/clawvault',
|
|
172
|
-
expect.arrayContaining(['checkpoint', '--working-on']),
|
|
173
|
-
expect.objectContaining({ shell: false })
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('injects recap and memory context on session start alias event', async () => {
|
|
180
|
-
const vaultPath = makeVaultFixture();
|
|
181
|
-
|
|
182
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
183
|
-
if (args[0] === 'session-recap') {
|
|
184
|
-
return JSON.stringify({
|
|
185
|
-
messages: [
|
|
186
|
-
{ role: 'user', text: 'Need a migration plan.' },
|
|
187
|
-
{ role: 'assistant', text: 'Suggested phased rollout.' }
|
|
188
|
-
]
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
if (args[0] === 'context') {
|
|
192
|
-
return JSON.stringify({
|
|
193
|
-
context: [
|
|
194
|
-
{
|
|
195
|
-
title: 'Use Postgres',
|
|
196
|
-
age: '1 day ago',
|
|
197
|
-
snippet: 'Selected Postgres for durability.'
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
return '';
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const handler = await loadHandler();
|
|
206
|
-
const event = {
|
|
207
|
-
eventName: 'session:start',
|
|
208
|
-
sessionKey: 'agent:clawdious:main',
|
|
209
|
-
pluginConfig: securePluginConfig(vaultPath),
|
|
210
|
-
context: { initialPrompt: 'Need migration plan' },
|
|
211
|
-
messages: [{ role: 'user', content: 'Need migration plan' }]
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
await handler(event);
|
|
215
|
-
|
|
216
|
-
const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
|
|
217
|
-
expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
|
|
218
|
-
|
|
219
|
-
const injected = event.messages.find((message) => message.role === 'system');
|
|
220
|
-
expect(injected?.content).toContain('Session context restored');
|
|
221
|
-
expect(injected?.content).toContain('Recent conversation');
|
|
222
|
-
expect(injected?.content).toContain('Relevant memories');
|
|
223
|
-
expect(injected?.content).toContain('Use Postgres');
|
|
224
|
-
|
|
225
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('delegates profile selection to context auto mode for urgent prompts', async () => {
|
|
229
|
-
const vaultPath = makeVaultFixture();
|
|
230
|
-
|
|
231
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
232
|
-
if (args[0] === 'session-recap') {
|
|
233
|
-
return JSON.stringify({ messages: [] });
|
|
234
|
-
}
|
|
235
|
-
if (args[0] === 'context') {
|
|
236
|
-
return JSON.stringify({ context: [] });
|
|
237
|
-
}
|
|
238
|
-
return '';
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const handler = await loadHandler();
|
|
242
|
-
await handler({
|
|
243
|
-
eventName: 'session:start',
|
|
244
|
-
sessionKey: 'agent:clawdious:main',
|
|
245
|
-
pluginConfig: securePluginConfig(vaultPath),
|
|
246
|
-
context: { initialPrompt: 'URGENT outage: rollback failed in production' },
|
|
247
|
-
messages: [{ role: 'user', content: 'URGENT outage: rollback failed in production' }]
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const contextCall = execFileSyncMock.mock.calls.find((call) => call[1]?.[0] === 'context');
|
|
251
|
-
expect(contextCall?.[1]).toEqual(expect.arrayContaining(['--profile', 'auto']));
|
|
252
|
-
|
|
253
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('triggers active observation on heartbeat when threshold is crossed', async () => {
|
|
257
|
-
const vaultPath = makeVaultFixture();
|
|
258
|
-
const sessionId = 'heartbeat-session-1';
|
|
259
|
-
const openClawFixture = makeOpenClawSessionFixture('main', sessionId, 70 * 1024);
|
|
260
|
-
process.env.OPENCLAW_STATE_DIR = openClawFixture.stateRoot;
|
|
261
|
-
|
|
262
|
-
fs.mkdirSync(path.join(vaultPath, '.clawvault'), { recursive: true });
|
|
263
|
-
fs.writeFileSync(
|
|
264
|
-
path.join(vaultPath, '.clawvault', 'observe-cursors.json'),
|
|
265
|
-
JSON.stringify({
|
|
266
|
-
[sessionId]: {
|
|
267
|
-
lastObservedOffset: 0,
|
|
268
|
-
lastObservedAt: '2026-02-14T00:00:00.000Z',
|
|
269
|
-
sessionKey: 'agent:main:main',
|
|
270
|
-
lastFileSize: 0
|
|
271
|
-
}
|
|
272
|
-
}),
|
|
273
|
-
'utf-8'
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
execFileSyncMock.mockReturnValue('');
|
|
277
|
-
|
|
278
|
-
const handler = await loadHandler();
|
|
279
|
-
await handler({
|
|
280
|
-
type: 'gateway',
|
|
281
|
-
action: 'heartbeat',
|
|
282
|
-
pluginConfig: securePluginConfig(vaultPath, { allowEnvAccess: true })
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
286
|
-
'/usr/local/bin/clawvault',
|
|
287
|
-
expect.arrayContaining(['observe', '--cron', '--agent', 'main']),
|
|
288
|
-
expect.objectContaining({ shell: false })
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
292
|
-
fs.rmSync(openClawFixture.stateRoot, { recursive: true, force: true });
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('forces active observation flush on compaction events', async () => {
|
|
296
|
-
const vaultPath = makeVaultFixture();
|
|
297
|
-
execFileSyncMock.mockReturnValue('');
|
|
298
|
-
|
|
299
|
-
const handler = await loadHandler();
|
|
300
|
-
await handler({
|
|
301
|
-
eventName: 'compaction:memoryFlush',
|
|
302
|
-
sessionKey: 'agent:clawdious:main',
|
|
303
|
-
pluginConfig: securePluginConfig(vaultPath)
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
307
|
-
'/usr/local/bin/clawvault',
|
|
308
|
-
expect.arrayContaining(['observe', '--cron', '--min-new', '1']),
|
|
309
|
-
expect.objectContaining({ shell: false })
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('runs weekly reflection on cron.weekly at Sunday midnight', async () => {
|
|
316
|
-
const vaultPath = makeVaultFixture();
|
|
317
|
-
execFileSyncMock.mockReturnValue('');
|
|
318
|
-
|
|
319
|
-
const handler = await loadHandler();
|
|
320
|
-
await handler({
|
|
321
|
-
eventName: 'cron.weekly',
|
|
322
|
-
timestamp: '2026-02-15T00:00:00.000Z',
|
|
323
|
-
pluginConfig: securePluginConfig(vaultPath)
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
327
|
-
'/usr/local/bin/clawvault',
|
|
328
|
-
expect.arrayContaining(['reflect', '-v', vaultPath]),
|
|
329
|
-
expect.objectContaining({ shell: false })
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('uses vaultPath from plugin config when provided in event', async () => {
|
|
336
|
-
const vaultPath = makeVaultFixture();
|
|
337
|
-
|
|
338
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
339
|
-
if (args[0] === 'recover') {
|
|
340
|
-
return 'Clean startup';
|
|
341
|
-
}
|
|
342
|
-
return '';
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const handler = await loadHandler();
|
|
346
|
-
const event = {
|
|
347
|
-
type: 'gateway',
|
|
348
|
-
action: 'startup',
|
|
349
|
-
pluginConfig: securePluginConfig(vaultPath),
|
|
350
|
-
messages: []
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
await handler(event);
|
|
354
|
-
|
|
355
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
356
|
-
'/usr/local/bin/clawvault',
|
|
357
|
-
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
358
|
-
expect.objectContaining({ shell: false })
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('uses vaultPath from context.pluginConfig when provided', async () => {
|
|
365
|
-
const vaultPath = makeVaultFixture();
|
|
366
|
-
|
|
367
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
368
|
-
if (args[0] === 'recover') {
|
|
369
|
-
return 'Clean startup';
|
|
370
|
-
}
|
|
371
|
-
return '';
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
const handler = await loadHandler();
|
|
375
|
-
const event = {
|
|
376
|
-
type: 'gateway',
|
|
377
|
-
action: 'startup',
|
|
378
|
-
context: {
|
|
379
|
-
pluginConfig: securePluginConfig(vaultPath)
|
|
380
|
-
},
|
|
381
|
-
messages: []
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
await handler(event);
|
|
385
|
-
|
|
386
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
387
|
-
'/usr/local/bin/clawvault',
|
|
388
|
-
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
389
|
-
expect.objectContaining({ shell: false })
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('uses vaultPath from OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env var', async () => {
|
|
396
|
-
const vaultPath = makeVaultFixture();
|
|
397
|
-
process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH = vaultPath;
|
|
398
|
-
|
|
399
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
400
|
-
if (args[0] === 'recover') {
|
|
401
|
-
return 'Clean startup';
|
|
402
|
-
}
|
|
403
|
-
return '';
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const handler = await loadHandler();
|
|
407
|
-
const event = {
|
|
408
|
-
type: 'gateway',
|
|
409
|
-
action: 'startup',
|
|
410
|
-
pluginConfig: securePluginConfig(vaultPath, { allowEnvAccess: true }),
|
|
411
|
-
messages: []
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
await handler(event);
|
|
415
|
-
|
|
416
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
417
|
-
'/usr/local/bin/clawvault',
|
|
418
|
-
expect.arrayContaining(['recover', '--clear', '-v', vaultPath]),
|
|
419
|
-
expect.objectContaining({ shell: false })
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
delete process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH;
|
|
423
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('uses per-agent vault path from agentVaults config', async () => {
|
|
427
|
-
const agent1Vault = makeVaultFixture();
|
|
428
|
-
const agent2Vault = makeVaultFixture();
|
|
429
|
-
const fallbackVault = makeVaultFixture();
|
|
430
|
-
|
|
431
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
432
|
-
if (args[0] === 'recover') {
|
|
433
|
-
return 'Clean startup';
|
|
434
|
-
}
|
|
435
|
-
return '';
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
const handler = await loadHandler();
|
|
439
|
-
const event = {
|
|
440
|
-
type: 'gateway',
|
|
441
|
-
action: 'startup',
|
|
442
|
-
sessionKey: 'agent:agent1:main',
|
|
443
|
-
pluginConfig: securePluginConfig(fallbackVault, {
|
|
444
|
-
agentVaults: {
|
|
445
|
-
agent1: agent1Vault,
|
|
446
|
-
agent2: agent2Vault
|
|
447
|
-
}
|
|
448
|
-
}),
|
|
449
|
-
messages: []
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
await handler(event);
|
|
453
|
-
|
|
454
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
455
|
-
'/usr/local/bin/clawvault',
|
|
456
|
-
expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
|
|
457
|
-
expect.objectContaining({ shell: false })
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
fs.rmSync(agent1Vault, { recursive: true, force: true });
|
|
461
|
-
fs.rmSync(agent2Vault, { recursive: true, force: true });
|
|
462
|
-
fs.rmSync(fallbackVault, { recursive: true, force: true });
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('falls back to vaultPath when agent not in agentVaults', async () => {
|
|
466
|
-
const agent1Vault = makeVaultFixture();
|
|
467
|
-
const fallbackVault = makeVaultFixture();
|
|
468
|
-
|
|
469
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
470
|
-
if (args[0] === 'recover') {
|
|
471
|
-
return 'Clean startup';
|
|
472
|
-
}
|
|
473
|
-
return '';
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
const handler = await loadHandler();
|
|
477
|
-
const event = {
|
|
478
|
-
type: 'gateway',
|
|
479
|
-
action: 'startup',
|
|
480
|
-
sessionKey: 'agent:unknown-agent:main',
|
|
481
|
-
pluginConfig: securePluginConfig(fallbackVault, {
|
|
482
|
-
agentVaults: {
|
|
483
|
-
agent1: agent1Vault
|
|
484
|
-
}
|
|
485
|
-
}),
|
|
486
|
-
messages: []
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
await handler(event);
|
|
490
|
-
|
|
491
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
492
|
-
'/usr/local/bin/clawvault',
|
|
493
|
-
expect.arrayContaining(['recover', '--clear', '-v', fallbackVault]),
|
|
494
|
-
expect.objectContaining({ shell: false })
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
fs.rmSync(agent1Vault, { recursive: true, force: true });
|
|
498
|
-
fs.rmSync(fallbackVault, { recursive: true, force: true });
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it('uses agentVaults from context.pluginConfig', async () => {
|
|
502
|
-
const agent1Vault = makeVaultFixture();
|
|
503
|
-
const fallbackVault = makeVaultFixture();
|
|
504
|
-
|
|
505
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
506
|
-
if (args[0] === 'recover') {
|
|
507
|
-
return 'Clean startup';
|
|
508
|
-
}
|
|
509
|
-
return '';
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
const handler = await loadHandler();
|
|
513
|
-
const event = {
|
|
514
|
-
type: 'gateway',
|
|
515
|
-
action: 'startup',
|
|
516
|
-
sessionKey: 'agent:agent1:main',
|
|
517
|
-
context: {
|
|
518
|
-
pluginConfig: securePluginConfig(fallbackVault, {
|
|
519
|
-
agentVaults: {
|
|
520
|
-
agent1: agent1Vault
|
|
521
|
-
}
|
|
522
|
-
})
|
|
523
|
-
},
|
|
524
|
-
messages: []
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
await handler(event);
|
|
528
|
-
|
|
529
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
530
|
-
'/usr/local/bin/clawvault',
|
|
531
|
-
expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
|
|
532
|
-
expect.objectContaining({ shell: false })
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
fs.rmSync(agent1Vault, { recursive: true, force: true });
|
|
536
|
-
fs.rmSync(fallbackVault, { recursive: true, force: true });
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it('uses OPENCLAW_AGENT_ID env var for agent resolution when session key not available', async () => {
|
|
540
|
-
const agent1Vault = makeVaultFixture();
|
|
541
|
-
const fallbackVault = makeVaultFixture();
|
|
542
|
-
process.env.OPENCLAW_AGENT_ID = 'agent1';
|
|
543
|
-
|
|
544
|
-
execFileSyncMock.mockImplementation((_command, args) => {
|
|
545
|
-
if (args[0] === 'recover') {
|
|
546
|
-
return 'Clean startup';
|
|
547
|
-
}
|
|
548
|
-
return '';
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const handler = await loadHandler();
|
|
552
|
-
const event = {
|
|
553
|
-
type: 'gateway',
|
|
554
|
-
action: 'startup',
|
|
555
|
-
pluginConfig: securePluginConfig(fallbackVault, {
|
|
556
|
-
vaultPath: fallbackVault,
|
|
557
|
-
agentVaults: {
|
|
558
|
-
agent1: agent1Vault
|
|
559
|
-
},
|
|
560
|
-
allowEnvAccess: true
|
|
561
|
-
}),
|
|
562
|
-
messages: []
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
await handler(event);
|
|
566
|
-
|
|
567
|
-
expect(execFileSyncMock).toHaveBeenCalledWith(
|
|
568
|
-
'/usr/local/bin/clawvault',
|
|
569
|
-
expect.arrayContaining(['recover', '--clear', '-v', agent1Vault]),
|
|
570
|
-
expect.objectContaining({ shell: false })
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
fs.rmSync(agent1Vault, { recursive: true, force: true });
|
|
574
|
-
fs.rmSync(fallbackVault, { recursive: true, force: true });
|
|
575
|
-
});
|
|
576
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
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
|
-
}
|