claude-flow 3.6.24 → 3.6.25
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 +8 -2
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +8 -2
- package/v3/@claude-flow/cli/bin/cli.js +21 -0
- package/v3/@claude-flow/cli/bin/mcp-server.js +16 -0
- package/v3/@claude-flow/cli/dist/src/commands/appliance.js +8 -10
- package/v3/@claude-flow/cli/dist/src/commands/guidance.js +1 -5
- package/v3/@claude-flow/cli/dist/src/commands/performance.js +3 -3
- package/v3/@claude-flow/cli/dist/src/commands/process.js +6 -7
- package/v3/@claude-flow/cli/dist/src/commands/verify.js +24 -3
- package/v3/@claude-flow/cli/dist/src/encryption/vault.d.ts +94 -0
- package/v3/@claude-flow/cli/dist/src/encryption/vault.js +172 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.d.ts +67 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.js +74 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/github-tools.js +122 -31
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +7 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +24 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/terminal-tools.js +22 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.d.ts +12 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.js +56 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +17 -16
- package/v3/@claude-flow/cli/dist/src/transfer/ipfs/upload.js +2 -0
- package/v3/@claude-flow/cli/dist/src/update/executor.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/update/executor.js +43 -7
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restricted-permission file helpers.
|
|
3
|
+
*
|
|
4
|
+
* audit_1776853149979: session/memory/terminal stores were written with the
|
|
5
|
+
* process umask, which on most macOS/Linux setups leaves them world-readable
|
|
6
|
+
* (mode 0644). They contain conversation snapshots, agent prompts, and
|
|
7
|
+
* terminal command history — anyone else on the host can read them.
|
|
8
|
+
*
|
|
9
|
+
* These helpers write atomically and force mode 0600 (files) / 0700 (dirs).
|
|
10
|
+
* chmod fails silently on Windows, where POSIX modes don't apply — that's
|
|
11
|
+
* fine, the OS-level ACL surface there is different.
|
|
12
|
+
*
|
|
13
|
+
* ADR-096 Phase 2: optional opt-in encryption-at-rest. When the caller
|
|
14
|
+
* passes `encrypt: true` AND the env-gated vault is enabled, payloads are
|
|
15
|
+
* AES-256-GCM-encrypted before hitting disk. Reads use the magic-byte
|
|
16
|
+
* sniff so legacy plaintext files keep working unchanged during the
|
|
17
|
+
* incremental migration.
|
|
18
|
+
*/
|
|
19
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { decryptBuffer, encryptBuffer, getKey, isEncryptedBlob, isEncryptionEnabled, } from './encryption/vault.js';
|
|
21
|
+
/**
|
|
22
|
+
* Create a directory tree with mode 0700 (owner-only). No-op if exists.
|
|
23
|
+
* Uses recursive: true so missing parents are created with the same mode.
|
|
24
|
+
*/
|
|
25
|
+
export function mkdirRestricted(path) {
|
|
26
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Write a file and tighten its permissions to mode 0600 (owner read/write).
|
|
30
|
+
*
|
|
31
|
+
* Two call signatures, both supported (the legacy positional one keeps
|
|
32
|
+
* existing call sites working without churn):
|
|
33
|
+
*
|
|
34
|
+
* writeFileRestricted(path, data) // plaintext, utf-8
|
|
35
|
+
* writeFileRestricted(path, data, 'utf-8') // legacy: encoding only
|
|
36
|
+
* writeFileRestricted(path, data, { encrypt: true }) // opt-in encryption
|
|
37
|
+
*/
|
|
38
|
+
export function writeFileRestricted(path, data, optsOrEncoding = 'utf-8') {
|
|
39
|
+
const opts = typeof optsOrEncoding === 'string'
|
|
40
|
+
? { encoding: optsOrEncoding }
|
|
41
|
+
: optsOrEncoding;
|
|
42
|
+
const encoding = opts.encoding ?? 'utf-8';
|
|
43
|
+
let payload = data;
|
|
44
|
+
if (opts.encrypt && isEncryptionEnabled()) {
|
|
45
|
+
const plaintext = Buffer.isBuffer(data) ? data : Buffer.from(data, encoding);
|
|
46
|
+
payload = encryptBuffer(plaintext, getKey());
|
|
47
|
+
}
|
|
48
|
+
// For encrypted payloads we always have a Buffer — pass through without an
|
|
49
|
+
// encoding so writeFileSync doesn't try to text-decode it.
|
|
50
|
+
if (Buffer.isBuffer(payload)) {
|
|
51
|
+
writeFileSync(path, payload);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
writeFileSync(path, payload, encoding);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
chmodSync(path, 0o600);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Windows / FS without POSIX modes — silently skip.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function readFileMaybeEncrypted(path, encoding = 'utf-8') {
|
|
64
|
+
const raw = readFileSync(path);
|
|
65
|
+
let plain;
|
|
66
|
+
if (isEncryptedBlob(raw)) {
|
|
67
|
+
plain = decryptBuffer(raw, getKey());
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
plain = raw;
|
|
71
|
+
}
|
|
72
|
+
return encoding === null ? plain : plain.toString(encoding);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=fs-secure.js.map
|
|
@@ -8,7 +8,7 @@ import { getProjectCwd } from './types.js';
|
|
|
8
8
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import { execSync } from 'node:child_process';
|
|
11
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
12
12
|
// Storage paths
|
|
13
13
|
const STORAGE_DIR = '.claude-flow';
|
|
14
14
|
const GITHUB_DIR = 'github';
|
|
@@ -41,7 +41,20 @@ function saveGitHubStore(store) {
|
|
|
41
41
|
ensureGitHubDir();
|
|
42
42
|
writeFileSync(getGitHubPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Run a shell command, return stdout or null on failure.
|
|
46
|
+
*
|
|
47
|
+
* SECURITY (audit_1776853149979): only call this with a STATIC command
|
|
48
|
+
* string (no template-string interpolation of user input). For any
|
|
49
|
+
* caller that needs to pass dynamic / user-supplied values, use
|
|
50
|
+
* runArgv below — it routes through execFileSync with shell:false so
|
|
51
|
+
* backticks, $(...), ;, and friends become literal argv bytes.
|
|
52
|
+
*
|
|
53
|
+
* The shell-string form is preserved here only because the surviving
|
|
54
|
+
* callers (`gh issue list ...`, `git rev-list --count HEAD`, …) use
|
|
55
|
+
* pipes / wc -l and need a shell. Any new caller with user input
|
|
56
|
+
* MUST use runArgv.
|
|
57
|
+
*/
|
|
45
58
|
function run(cmd, cwd) {
|
|
46
59
|
try {
|
|
47
60
|
return execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: cwd || getProjectCwd(), stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -50,6 +63,49 @@ function run(cmd, cwd) {
|
|
|
50
63
|
return null;
|
|
51
64
|
}
|
|
52
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Run a program with an argv array (no shell). Use this for any callsite
|
|
68
|
+
* that mixes user input into the command line — argv elements aren't
|
|
69
|
+
* interpreted by /bin/sh, so shell metacharacters in user-supplied
|
|
70
|
+
* strings stay literal.
|
|
71
|
+
*/
|
|
72
|
+
function runArgv(file, args, cwd) {
|
|
73
|
+
try {
|
|
74
|
+
return execFileSync(file, args, {
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
timeout: 15000,
|
|
77
|
+
cwd: cwd || getProjectCwd(),
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
shell: false,
|
|
80
|
+
}).trim();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Coerce a user-supplied PR / issue / run number to a positive integer.
|
|
88
|
+
* Returns null if the input can't be safely passed as an argv element to
|
|
89
|
+
* gh (which would otherwise accept any string).
|
|
90
|
+
*/
|
|
91
|
+
function toPositiveInt(value) {
|
|
92
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
93
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 2 ** 31)
|
|
94
|
+
return null;
|
|
95
|
+
return n;
|
|
96
|
+
}
|
|
97
|
+
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9 _\-./]{0,63}$/;
|
|
98
|
+
function sanitizeLabels(value) {
|
|
99
|
+
if (!Array.isArray(value))
|
|
100
|
+
return null;
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const v of value) {
|
|
103
|
+
if (typeof v !== 'string' || !LABEL_RE.test(v))
|
|
104
|
+
return null;
|
|
105
|
+
out.push(v);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
53
109
|
/** Check if gh CLI is available */
|
|
54
110
|
function hasGhCli() {
|
|
55
111
|
return run('gh --version') !== null;
|
|
@@ -219,7 +275,17 @@ export const githubTools = [
|
|
|
219
275
|
const headBranch = input.branch || run('git rev-parse --abbrev-ref HEAD') || 'feature';
|
|
220
276
|
const baseBranch = input.baseBranch || 'main';
|
|
221
277
|
const body = input.body || '';
|
|
222
|
-
|
|
278
|
+
// audit_1776853149979: title/body only had length validation, and
|
|
279
|
+
// the inline .replace(/"/g, '\\"') was a porous escape (no handling
|
|
280
|
+
// of `, $(...), \). Routes via argv array now — no shell to
|
|
281
|
+
// interpret metas.
|
|
282
|
+
const result = runArgv('gh', [
|
|
283
|
+
'pr', 'create',
|
|
284
|
+
'--title', title,
|
|
285
|
+
'--base', baseBranch,
|
|
286
|
+
'--head', headBranch,
|
|
287
|
+
'--body', body,
|
|
288
|
+
]);
|
|
223
289
|
if (result) {
|
|
224
290
|
return { success: true, _real: true, action: 'created', url: result };
|
|
225
291
|
}
|
|
@@ -232,9 +298,15 @@ export const githubTools = [
|
|
|
232
298
|
return { success: true, source: 'local-store', action: 'created', pullRequest: pr };
|
|
233
299
|
}
|
|
234
300
|
if (action === 'review') {
|
|
235
|
-
|
|
301
|
+
// audit_1776853149979: prNumber was typed `number` in schema but only
|
|
302
|
+
// cast at runtime, so a string "1; touch /tmp/x" would interpolate
|
|
303
|
+
// into the shell. Coerce + validate as positive integer.
|
|
304
|
+
const prNumber = toPositiveInt(input.prNumber);
|
|
236
305
|
if (gh && prNumber) {
|
|
237
|
-
const raw =
|
|
306
|
+
const raw = runArgv('gh', [
|
|
307
|
+
'pr', 'view', String(prNumber),
|
|
308
|
+
'--json', 'number,title,state,body,additions,deletions,changedFiles,reviews,mergeable,statusCheckRollup',
|
|
309
|
+
]);
|
|
238
310
|
if (raw) {
|
|
239
311
|
try {
|
|
240
312
|
return { success: true, _real: true, action: 'review', pullRequest: JSON.parse(raw) };
|
|
@@ -242,18 +314,18 @@ export const githubTools = [
|
|
|
242
314
|
catch { /* fall through */ }
|
|
243
315
|
}
|
|
244
316
|
}
|
|
245
|
-
return { success: false, error: prNumber ? 'gh CLI not available or PR not found. Install gh: https://cli.github.com' : 'prNumber is required for review.' };
|
|
317
|
+
return { success: false, error: prNumber ? 'gh CLI not available or PR not found. Install gh: https://cli.github.com' : 'prNumber is required (positive integer) for review.' };
|
|
246
318
|
}
|
|
247
319
|
if (action === 'merge') {
|
|
248
|
-
const prNumber = input.prNumber;
|
|
320
|
+
const prNumber = toPositiveInt(input.prNumber);
|
|
249
321
|
if (gh && prNumber) {
|
|
250
|
-
const result =
|
|
322
|
+
const result = runArgv('gh', ['pr', 'merge', String(prNumber), '--merge']);
|
|
251
323
|
if (result !== null) {
|
|
252
324
|
return { success: true, _real: true, action: 'merged', prNumber, mergedAt: new Date().toISOString() };
|
|
253
325
|
}
|
|
254
326
|
}
|
|
255
327
|
// Fallback: local store
|
|
256
|
-
const prKey = Object.keys(store.prs).find(k => k.includes(String(prNumber)));
|
|
328
|
+
const prKey = prNumber ? Object.keys(store.prs).find(k => k.includes(String(prNumber))) : undefined;
|
|
257
329
|
if (prKey && store.prs[prKey]) {
|
|
258
330
|
store.prs[prKey].status = 'merged';
|
|
259
331
|
saveGitHubStore(store);
|
|
@@ -261,14 +333,14 @@ export const githubTools = [
|
|
|
261
333
|
return { success: true, source: 'local-store', action: 'merged', prNumber, mergedAt: new Date().toISOString() };
|
|
262
334
|
}
|
|
263
335
|
if (action === 'close') {
|
|
264
|
-
const prNumber = input.prNumber;
|
|
336
|
+
const prNumber = toPositiveInt(input.prNumber);
|
|
265
337
|
if (gh && prNumber) {
|
|
266
|
-
const result =
|
|
338
|
+
const result = runArgv('gh', ['pr', 'close', String(prNumber)]);
|
|
267
339
|
if (result !== null) {
|
|
268
340
|
return { success: true, _real: true, action: 'closed', prNumber, closedAt: new Date().toISOString() };
|
|
269
341
|
}
|
|
270
342
|
}
|
|
271
|
-
const prKey = Object.keys(store.prs).find(k => k.includes(String(prNumber)));
|
|
343
|
+
const prKey = prNumber ? Object.keys(store.prs).find(k => k.includes(String(prNumber))) : undefined;
|
|
272
344
|
if (prKey && store.prs[prKey]) {
|
|
273
345
|
store.prs[prKey].status = 'closed';
|
|
274
346
|
saveGitHubStore(store);
|
|
@@ -336,10 +408,16 @@ export const githubTools = [
|
|
|
336
408
|
if (action === 'create') {
|
|
337
409
|
const title = input.title || 'New Issue';
|
|
338
410
|
const body = input.body || '';
|
|
339
|
-
|
|
411
|
+
// audit_1776853149979: labels was joined into a shell string with no
|
|
412
|
+
// validation of the label content. sanitizeLabels rejects anything
|
|
413
|
+
// outside [A-Za-z0-9 _\-./] and caps each label at 64 chars.
|
|
414
|
+
const labels = sanitizeLabels(input.labels) ?? [];
|
|
340
415
|
if (gh) {
|
|
341
|
-
const
|
|
342
|
-
|
|
416
|
+
const argv = ['issue', 'create', '--title', title, '--body', body];
|
|
417
|
+
if (labels.length > 0) {
|
|
418
|
+
argv.push('--label', labels.join(','));
|
|
419
|
+
}
|
|
420
|
+
const result = runArgv('gh', argv);
|
|
343
421
|
if (result) {
|
|
344
422
|
return { success: true, _real: true, action: 'created', url: result };
|
|
345
423
|
}
|
|
@@ -351,37 +429,45 @@ export const githubTools = [
|
|
|
351
429
|
return { success: true, source: 'local-store', action: 'created', issue };
|
|
352
430
|
}
|
|
353
431
|
if (action === 'update') {
|
|
354
|
-
const issueNumber = input.issueNumber;
|
|
432
|
+
const issueNumber = toPositiveInt(input.issueNumber);
|
|
355
433
|
if (gh && issueNumber) {
|
|
356
|
-
const
|
|
434
|
+
const argv = ['issue', 'edit', String(issueNumber)];
|
|
357
435
|
if (input.title)
|
|
358
|
-
|
|
359
|
-
if (input.labels)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
436
|
+
argv.push('--title', input.title);
|
|
437
|
+
if (input.labels) {
|
|
438
|
+
const labels = sanitizeLabels(input.labels);
|
|
439
|
+
if (labels === null)
|
|
440
|
+
return { success: false, error: 'labels contains disallowed characters' };
|
|
441
|
+
if (labels.length > 0)
|
|
442
|
+
argv.push('--add-label', labels.join(','));
|
|
443
|
+
}
|
|
444
|
+
if (argv.length > 3) {
|
|
445
|
+
const result = runArgv('gh', argv);
|
|
363
446
|
if (result !== null)
|
|
364
447
|
return { success: true, _real: true, action: 'updated', issueNumber };
|
|
365
448
|
}
|
|
366
449
|
}
|
|
367
|
-
const issueKey = Object.keys(store.issues).find(k => k.includes(String(issueNumber)));
|
|
450
|
+
const issueKey = issueNumber ? Object.keys(store.issues).find(k => k.includes(String(issueNumber))) : undefined;
|
|
368
451
|
if (issueKey && store.issues[issueKey]) {
|
|
369
452
|
if (input.title)
|
|
370
453
|
store.issues[issueKey].title = input.title;
|
|
371
|
-
if (input.labels)
|
|
372
|
-
|
|
454
|
+
if (input.labels) {
|
|
455
|
+
const labels = sanitizeLabels(input.labels);
|
|
456
|
+
if (labels !== null)
|
|
457
|
+
store.issues[issueKey].labels = labels;
|
|
458
|
+
}
|
|
373
459
|
saveGitHubStore(store);
|
|
374
460
|
}
|
|
375
461
|
return { success: true, source: 'local-store', action: 'updated', issueNumber };
|
|
376
462
|
}
|
|
377
463
|
if (action === 'close') {
|
|
378
|
-
const issueNumber = input.issueNumber;
|
|
464
|
+
const issueNumber = toPositiveInt(input.issueNumber);
|
|
379
465
|
if (gh && issueNumber) {
|
|
380
|
-
const result =
|
|
466
|
+
const result = runArgv('gh', ['issue', 'close', String(issueNumber)]);
|
|
381
467
|
if (result !== null)
|
|
382
468
|
return { success: true, _real: true, action: 'closed', issueNumber, closedAt: new Date().toISOString() };
|
|
383
469
|
}
|
|
384
|
-
const issueKey = Object.keys(store.issues).find(k => k.includes(String(issueNumber)));
|
|
470
|
+
const issueKey = issueNumber ? Object.keys(store.issues).find(k => k.includes(String(issueNumber))) : undefined;
|
|
385
471
|
if (issueKey && store.issues[issueKey]) {
|
|
386
472
|
store.issues[issueKey].status = 'closed';
|
|
387
473
|
saveGitHubStore(store);
|
|
@@ -450,7 +536,12 @@ export const githubTools = [
|
|
|
450
536
|
if (action === 'status') {
|
|
451
537
|
const workflowId = input.workflowId;
|
|
452
538
|
if (workflowId) {
|
|
453
|
-
|
|
539
|
+
// workflowId is already validated by validateIdentifier above, but
|
|
540
|
+
// route through argv anyway for consistency / defense-in-depth.
|
|
541
|
+
const raw = runArgv('gh', [
|
|
542
|
+
'run', 'view', workflowId,
|
|
543
|
+
'--json', 'databaseId,displayTitle,status,conclusion,jobs',
|
|
544
|
+
]);
|
|
454
545
|
if (raw) {
|
|
455
546
|
try {
|
|
456
547
|
return { success: true, _real: true, run: JSON.parse(raw) };
|
|
@@ -471,7 +562,7 @@ export const githubTools = [
|
|
|
471
562
|
const workflowId = input.workflowId;
|
|
472
563
|
const ref = input.ref || 'main';
|
|
473
564
|
if (workflowId) {
|
|
474
|
-
const result =
|
|
565
|
+
const result = runArgv('gh', ['workflow', 'run', workflowId, '--ref', ref]);
|
|
475
566
|
if (result !== null)
|
|
476
567
|
return { success: true, _real: true, action: 'triggered', workflowId, ref };
|
|
477
568
|
}
|
|
@@ -480,7 +571,7 @@ export const githubTools = [
|
|
|
480
571
|
if (action === 'cancel') {
|
|
481
572
|
const workflowId = input.workflowId;
|
|
482
573
|
if (workflowId) {
|
|
483
|
-
const result =
|
|
574
|
+
const result = runArgv('gh', ['run', 'cancel', workflowId]);
|
|
484
575
|
if (result !== null)
|
|
485
576
|
return { success: true, _real: true, action: 'cancelled', runId: workflowId };
|
|
486
577
|
}
|
|
@@ -1388,8 +1388,8 @@ export const hooksPretrain = {
|
|
|
1388
1388
|
const repoPath = resolve(params.path || '.');
|
|
1389
1389
|
const depth = params.depth || 'medium';
|
|
1390
1390
|
const startTime = performance.now();
|
|
1391
|
-
// Real file scanning — count files by extension, extract patterns
|
|
1392
|
-
|
|
1391
|
+
// Real file scanning — count files by extension, extract patterns.
|
|
1392
|
+
// (readdirSync/statSync already imported statically at the top.)
|
|
1393
1393
|
const extCounts = {};
|
|
1394
1394
|
let filesAnalyzed = 0;
|
|
1395
1395
|
let totalLines = 0;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module v3/cli/mcp-tools/memory-tools
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
13
|
+
import { homedir } from 'os';
|
|
13
14
|
import { join, resolve } from 'path';
|
|
14
15
|
import { validateIdentifier } from './validate-input.js';
|
|
15
16
|
// #1604: Align with memory-initializer.ts — single source of truth is .swarm/memory.db
|
|
@@ -598,7 +599,6 @@ export const memoryTools = [
|
|
|
598
599
|
handler: async (input) => {
|
|
599
600
|
await ensureInitialized();
|
|
600
601
|
const { storeEntry } = await getMemoryFunctions();
|
|
601
|
-
const { homedir } = await import('os');
|
|
602
602
|
const ns = input.namespace || 'claude-memories';
|
|
603
603
|
if (input.namespace) {
|
|
604
604
|
const vNs = validateIdentifier(ns, 'namespace');
|
|
@@ -613,15 +613,13 @@ export const memoryTools = [
|
|
|
613
613
|
// Scan all projects
|
|
614
614
|
if (existsSync(claudeProjectsDir)) {
|
|
615
615
|
try {
|
|
616
|
-
const
|
|
617
|
-
const { readdirSync: readDir } = await import('fs');
|
|
618
|
-
for (const project of readDir(claudeProjectsDir, { withFileTypes: true })) {
|
|
616
|
+
for (const project of readdirSync(claudeProjectsDir, { withFileTypes: true })) {
|
|
619
617
|
if (!project.isDirectory())
|
|
620
618
|
continue;
|
|
621
619
|
const memDir = join(claudeProjectsDir, project.name, 'memory');
|
|
622
620
|
if (!existsSync(memDir))
|
|
623
621
|
continue;
|
|
624
|
-
for (const file of
|
|
622
|
+
for (const file of readdirSync(memDir).filter((f) => f.endsWith('.md'))) {
|
|
625
623
|
memoryFiles.push({ path: join(memDir, file), project: project.name, file });
|
|
626
624
|
}
|
|
627
625
|
}
|
|
@@ -636,8 +634,7 @@ export const memoryTools = [
|
|
|
636
634
|
const memDir = join(claudeProjectsDir, projectHash, 'memory');
|
|
637
635
|
if (existsSync(memDir)) {
|
|
638
636
|
try {
|
|
639
|
-
const
|
|
640
|
-
for (const file of readDir(memDir).filter((f) => f.endsWith('.md'))) {
|
|
637
|
+
for (const file of readdirSync(memDir).filter((f) => f.endsWith('.md'))) {
|
|
641
638
|
memoryFiles.push({ path: join(memDir, file), project: projectHash, file });
|
|
642
639
|
}
|
|
643
640
|
}
|
|
@@ -704,21 +701,19 @@ export const memoryTools = [
|
|
|
704
701
|
inputSchema: { type: 'object', properties: {} },
|
|
705
702
|
handler: async () => {
|
|
706
703
|
await ensureInitialized();
|
|
707
|
-
const { homedir } = await import('os');
|
|
708
704
|
// Count Claude memory files
|
|
709
705
|
const claudeProjectsDir = join(homedir(), '.claude', 'projects');
|
|
710
706
|
let claudeFiles = 0;
|
|
711
707
|
let claudeProjects = 0;
|
|
712
708
|
if (existsSync(claudeProjectsDir)) {
|
|
713
709
|
try {
|
|
714
|
-
const {
|
|
715
|
-
for (const project of readDir(claudeProjectsDir, { withFileTypes: true })) {
|
|
710
|
+
for (const project of readdirSync(claudeProjectsDir, { withFileTypes: true })) {
|
|
716
711
|
if (!project.isDirectory())
|
|
717
712
|
continue;
|
|
718
713
|
const memDir = join(claudeProjectsDir, project.name, 'memory');
|
|
719
714
|
if (!existsSync(memDir))
|
|
720
715
|
continue;
|
|
721
|
-
const files =
|
|
716
|
+
const files = readdirSync(memDir).filter((f) => f.endsWith('.md'));
|
|
722
717
|
if (files.length > 0) {
|
|
723
718
|
claudeProjects++;
|
|
724
719
|
claudeFiles += files.length;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tool definitions for session management with file persistence.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readFileSync,
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { getProjectCwd } from './types.js';
|
|
9
|
+
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
9
10
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
10
11
|
// Storage paths
|
|
11
12
|
const STORAGE_DIR = '.claude-flow';
|
|
@@ -21,14 +22,17 @@ function getSessionPath(sessionId) {
|
|
|
21
22
|
function ensureSessionDir() {
|
|
22
23
|
const dir = getSessionDir();
|
|
23
24
|
if (!existsSync(dir)) {
|
|
24
|
-
|
|
25
|
+
mkdirRestricted(dir);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
function loadSession(sessionId) {
|
|
28
29
|
try {
|
|
29
30
|
const path = getSessionPath(sessionId);
|
|
30
31
|
if (existsSync(path)) {
|
|
31
|
-
|
|
32
|
+
// ADR-096 Phase 2: readFileMaybeEncrypted transparently handles both
|
|
33
|
+
// legacy plaintext sessions and post-migration encrypted ones via the
|
|
34
|
+
// RFE1 magic-byte sniff.
|
|
35
|
+
const data = readFileMaybeEncrypted(path, 'utf-8');
|
|
32
36
|
return JSON.parse(data);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -39,7 +43,12 @@ function loadSession(sessionId) {
|
|
|
39
43
|
}
|
|
40
44
|
function saveSession(session) {
|
|
41
45
|
ensureSessionDir();
|
|
42
|
-
|
|
46
|
+
// audit_1776853149979: session JSON contains memory snapshots and agent
|
|
47
|
+
// prompts — restrict to owner read/write.
|
|
48
|
+
// ADR-096 Phase 2: opt-in encrypt-at-rest. The encrypt flag is honored
|
|
49
|
+
// only when CLAUDE_FLOW_ENCRYPT_AT_REST is set; otherwise the legacy
|
|
50
|
+
// plaintext path runs unchanged.
|
|
51
|
+
writeFileRestricted(getSessionPath(session.sessionId), JSON.stringify(session, null, 2), { encrypt: true });
|
|
43
52
|
}
|
|
44
53
|
function listSessions() {
|
|
45
54
|
ensureSessionDir();
|
|
@@ -48,7 +57,9 @@ function listSessions() {
|
|
|
48
57
|
const sessions = [];
|
|
49
58
|
for (const file of files) {
|
|
50
59
|
try {
|
|
51
|
-
|
|
60
|
+
// ADR-096 Phase 2: same magic-byte sniff for the listing path so a
|
|
61
|
+
// mixed plaintext+encrypted dir still enumerates cleanly.
|
|
62
|
+
const data = readFileMaybeEncrypted(join(dir, file), 'utf-8');
|
|
52
63
|
sessions.push(JSON.parse(data));
|
|
53
64
|
}
|
|
54
65
|
catch {
|
|
@@ -192,12 +203,13 @@ export const sessionTools = [
|
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
if (session) {
|
|
195
|
-
// Restore data to respective stores (legacy JSON for backward compat)
|
|
206
|
+
// Restore data to respective stores (legacy JSON for backward compat).
|
|
207
|
+
// audit_1776853149979: tighten perms on the restored stores too.
|
|
196
208
|
if (session.data?.memory) {
|
|
197
209
|
const memoryDir = join(getProjectCwd(), STORAGE_DIR, 'memory');
|
|
198
210
|
if (!existsSync(memoryDir))
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
mkdirRestricted(memoryDir);
|
|
212
|
+
writeFileRestricted(join(memoryDir, 'store.json'), JSON.stringify(session.data.memory, null, 2));
|
|
201
213
|
// Also populate active sql.js SQLite database so memory-tools can find entries
|
|
202
214
|
try {
|
|
203
215
|
const { storeEntry } = await import('../memory/memory-initializer.js');
|
|
@@ -224,14 +236,14 @@ export const sessionTools = [
|
|
|
224
236
|
if (session.data?.tasks) {
|
|
225
237
|
const taskDir = join(getProjectCwd(), STORAGE_DIR, 'tasks');
|
|
226
238
|
if (!existsSync(taskDir))
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
mkdirRestricted(taskDir);
|
|
240
|
+
writeFileRestricted(join(taskDir, 'store.json'), JSON.stringify(session.data.tasks, null, 2));
|
|
229
241
|
}
|
|
230
242
|
if (session.data?.agents) {
|
|
231
243
|
const agentDir = join(getProjectCwd(), STORAGE_DIR, 'agents');
|
|
232
244
|
if (!existsSync(agentDir))
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
mkdirRestricted(agentDir);
|
|
246
|
+
writeFileRestricted(join(agentDir, 'store.json'), JSON.stringify(session.data.agents, null, 2));
|
|
235
247
|
}
|
|
236
248
|
return {
|
|
237
249
|
sessionId: session.sessionId,
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Terminal session management with real command execution.
|
|
5
5
|
*/
|
|
6
6
|
import { getProjectCwd } from './types.js';
|
|
7
|
-
import { existsSync
|
|
8
|
-
import {
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
9
|
+
import { validateEnv, validateIdentifier, validatePath, validateText } from './validate-input.js';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { execSync } from 'node:child_process';
|
|
11
12
|
// Storage paths
|
|
@@ -21,14 +22,17 @@ function getTerminalPath() {
|
|
|
21
22
|
function ensureTerminalDir() {
|
|
22
23
|
const dir = getTerminalDir();
|
|
23
24
|
if (!existsSync(dir)) {
|
|
24
|
-
|
|
25
|
+
mkdirRestricted(dir);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
function loadTerminalStore() {
|
|
28
29
|
try {
|
|
29
30
|
const path = getTerminalPath();
|
|
30
31
|
if (existsSync(path)) {
|
|
31
|
-
|
|
32
|
+
// ADR-096 Phase 3: readFileMaybeEncrypted handles both legacy
|
|
33
|
+
// plaintext stores and post-migration encrypted ones via the RFE1
|
|
34
|
+
// magic-byte sniff.
|
|
35
|
+
return JSON.parse(readFileMaybeEncrypted(path, 'utf-8'));
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
catch {
|
|
@@ -38,7 +42,12 @@ function loadTerminalStore() {
|
|
|
38
42
|
}
|
|
39
43
|
function saveTerminalStore(store) {
|
|
40
44
|
ensureTerminalDir();
|
|
41
|
-
|
|
45
|
+
// audit_1776853149979: terminal command history can contain credentials
|
|
46
|
+
// pasted into commands; restrict to owner read/write (mode 0600).
|
|
47
|
+
// ADR-096 Phase 3: opt-in AES-256-GCM encrypt-at-rest. Honored only
|
|
48
|
+
// when CLAUDE_FLOW_ENCRYPT_AT_REST is set; otherwise legacy plaintext
|
|
49
|
+
// path runs unchanged.
|
|
50
|
+
writeFileRestricted(getTerminalPath(), JSON.stringify(store, null, 2), { encrypt: true });
|
|
42
51
|
}
|
|
43
52
|
export const terminalTools = [
|
|
44
53
|
{
|
|
@@ -54,7 +63,7 @@ export const terminalTools = [
|
|
|
54
63
|
},
|
|
55
64
|
},
|
|
56
65
|
handler: async (input) => {
|
|
57
|
-
// Validate user-provided input (#1425)
|
|
66
|
+
// Validate user-provided input (#1425, audit_1776853149979)
|
|
58
67
|
if (input.name) {
|
|
59
68
|
const v = validateText(input.name, 'name', 256);
|
|
60
69
|
if (!v.valid)
|
|
@@ -65,6 +74,12 @@ export const terminalTools = [
|
|
|
65
74
|
if (!v.valid)
|
|
66
75
|
return { success: false, error: v.error };
|
|
67
76
|
}
|
|
77
|
+
// env is merged into execSync's process env on every command; reject
|
|
78
|
+
// loader/runtime hijack vars (LD_PRELOAD, NODE_OPTIONS, …) and enforce
|
|
79
|
+
// POSIX-shaped names + null-byte-free values.
|
|
80
|
+
const vEnv = validateEnv(input.env, 'env');
|
|
81
|
+
if (!vEnv.valid)
|
|
82
|
+
return { success: false, error: vEnv.error };
|
|
68
83
|
const store = loadTerminalStore();
|
|
69
84
|
const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
70
85
|
const session = {
|
|
@@ -75,7 +90,7 @@ export const terminalTools = [
|
|
|
75
90
|
lastActivity: new Date().toISOString(),
|
|
76
91
|
workingDir: input.workingDir || getProjectCwd(),
|
|
77
92
|
history: [],
|
|
78
|
-
env:
|
|
93
|
+
env: vEnv.sanitized,
|
|
79
94
|
};
|
|
80
95
|
store.sessions[id] = session;
|
|
81
96
|
saveTerminalStore(store);
|
|
@@ -34,6 +34,18 @@ export declare function validatePath(value: unknown, label: string): ValidationR
|
|
|
34
34
|
* Allows most characters but rejects shell metacharacters that could cause injection.
|
|
35
35
|
*/
|
|
36
36
|
export declare function validateText(value: unknown, label: string, maxLen?: number): ValidationResult;
|
|
37
|
+
export interface EnvValidationResult {
|
|
38
|
+
valid: boolean;
|
|
39
|
+
sanitized: Record<string, string>;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a Record<string,string> of environment variables: enforce POSIX
|
|
44
|
+
* names, reject hijack-prone names (LD_PRELOAD, NODE_OPTIONS, …), forbid null
|
|
45
|
+
* bytes in values, and cap value length so a malicious caller can't bloat the
|
|
46
|
+
* stored session past reasonable bounds.
|
|
47
|
+
*/
|
|
48
|
+
export declare function validateEnv(value: unknown, label?: string): EnvValidationResult;
|
|
37
49
|
/**
|
|
38
50
|
* Assert validation or throw with a structured error.
|
|
39
51
|
*/
|