claude-flow 3.6.24 → 3.6.26
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/doctor.js +90 -2
- 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
- package/.claude/scheduled_tasks.lock +0 -1
|
@@ -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
|
*/
|
|
@@ -104,6 +104,62 @@ export function validateText(value, label, maxLen = 10_000) {
|
|
|
104
104
|
const sanitized = value.replace(/\0/g, '');
|
|
105
105
|
return { valid: true, sanitized };
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Names that let an attacker pivot a child process before any user code runs:
|
|
109
|
+
* shared-library injection on Linux/macOS, Node hooks, and command resolution.
|
|
110
|
+
*
|
|
111
|
+
* audit_1776853149979: terminal_create previously merged caller-supplied env
|
|
112
|
+
* straight into execSync's environment for every subsequent command in the
|
|
113
|
+
* session. Setting LD_PRELOAD or NODE_OPTIONS via that path is functionally
|
|
114
|
+
* equivalent to remote code execution, so the env input needs an allowlist
|
|
115
|
+
* shape and a denylist on these specific names.
|
|
116
|
+
*/
|
|
117
|
+
const DENYLISTED_ENV_NAMES = new Set([
|
|
118
|
+
'LD_PRELOAD',
|
|
119
|
+
'LD_LIBRARY_PATH',
|
|
120
|
+
'LD_AUDIT',
|
|
121
|
+
'DYLD_INSERT_LIBRARIES',
|
|
122
|
+
'DYLD_LIBRARY_PATH',
|
|
123
|
+
'DYLD_FALLBACK_LIBRARY_PATH',
|
|
124
|
+
'DYLD_FORCE_FLAT_NAMESPACE',
|
|
125
|
+
'NODE_OPTIONS',
|
|
126
|
+
'NODE_PATH',
|
|
127
|
+
]);
|
|
128
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
129
|
+
/**
|
|
130
|
+
* Validate a Record<string,string> of environment variables: enforce POSIX
|
|
131
|
+
* names, reject hijack-prone names (LD_PRELOAD, NODE_OPTIONS, …), forbid null
|
|
132
|
+
* bytes in values, and cap value length so a malicious caller can't bloat the
|
|
133
|
+
* stored session past reasonable bounds.
|
|
134
|
+
*/
|
|
135
|
+
export function validateEnv(value, label = 'env') {
|
|
136
|
+
if (value === undefined || value === null) {
|
|
137
|
+
return { valid: true, sanitized: {} };
|
|
138
|
+
}
|
|
139
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
140
|
+
return { valid: false, sanitized: {}, error: `${label} must be an object of string→string` };
|
|
141
|
+
}
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const [name, rawVal] of Object.entries(value)) {
|
|
144
|
+
if (!ENV_NAME_RE.test(name)) {
|
|
145
|
+
return { valid: false, sanitized: {}, error: `${label} key "${name}" is not a valid POSIX env name` };
|
|
146
|
+
}
|
|
147
|
+
if (DENYLISTED_ENV_NAMES.has(name)) {
|
|
148
|
+
return { valid: false, sanitized: {}, error: `${label} key "${name}" is denylisted (loader/runtime hijack)` };
|
|
149
|
+
}
|
|
150
|
+
if (typeof rawVal !== 'string') {
|
|
151
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] must be a string` };
|
|
152
|
+
}
|
|
153
|
+
if (rawVal.length > 32_768) {
|
|
154
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] exceeds 32768 characters` };
|
|
155
|
+
}
|
|
156
|
+
if (rawVal.includes('\0')) {
|
|
157
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] contains a null byte` };
|
|
158
|
+
}
|
|
159
|
+
out[name] = rawVal;
|
|
160
|
+
}
|
|
161
|
+
return { valid: true, sanitized: out };
|
|
162
|
+
}
|
|
107
163
|
/**
|
|
108
164
|
* Assert validation or throw with a structured error.
|
|
109
165
|
*/
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
+
import { readFileMaybeEncrypted, writeFileRestricted } from '../fs-secure.js';
|
|
13
14
|
// ADR-053: Lazy import of AgentDB v3 bridge
|
|
14
15
|
let _bridge;
|
|
15
16
|
async function getBridge() {
|
|
@@ -403,7 +404,7 @@ export async function getHNSWIndex(options) {
|
|
|
403
404
|
try {
|
|
404
405
|
const initSqlJs = (await import('sql.js')).default;
|
|
405
406
|
const SQL = await initSqlJs();
|
|
406
|
-
const fileBuffer =
|
|
407
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
407
408
|
const sqlDb = new SQL.Database(fileBuffer);
|
|
408
409
|
// Load all entries with embeddings
|
|
409
410
|
const result = sqlDb.exec(`
|
|
@@ -828,7 +829,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
828
829
|
}
|
|
829
830
|
const initSqlJs = (await import('sql.js')).default;
|
|
830
831
|
const SQL = await initSqlJs();
|
|
831
|
-
const fileBuffer =
|
|
832
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
832
833
|
const db = new SQL.Database(fileBuffer);
|
|
833
834
|
// Get current columns in memory_entries
|
|
834
835
|
const tableInfo = db.exec("PRAGMA table_info(memory_entries)");
|
|
@@ -865,7 +866,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
865
866
|
if (modified) {
|
|
866
867
|
// Save updated database
|
|
867
868
|
const data = db.export();
|
|
868
|
-
|
|
869
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
869
870
|
}
|
|
870
871
|
db.close();
|
|
871
872
|
return { success: true, columnsAdded };
|
|
@@ -1035,7 +1036,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1035
1036
|
// Save to file
|
|
1036
1037
|
const data = db.export();
|
|
1037
1038
|
const buffer = Buffer.from(data);
|
|
1038
|
-
|
|
1039
|
+
writeFileRestricted(dbPath, buffer, { encrypt: true });
|
|
1039
1040
|
// Close database
|
|
1040
1041
|
db.close();
|
|
1041
1042
|
// Also create schema file for reference
|
|
@@ -1101,7 +1102,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1101
1102
|
sqliteHeader[25] = 0x40;
|
|
1102
1103
|
sqliteHeader[26] = 0x20; // min embedded payload
|
|
1103
1104
|
sqliteHeader[27] = 0x20; // leaf payload
|
|
1104
|
-
|
|
1105
|
+
writeFileRestricted(dbPath, sqliteHeader, { encrypt: true });
|
|
1105
1106
|
// ADR-053: Activate ControllerRegistry even on fallback path
|
|
1106
1107
|
const controllerResult = await activateControllerRegistry(dbPath, verbose);
|
|
1107
1108
|
return {
|
|
@@ -1546,7 +1547,7 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1546
1547
|
const SQL = await initSqlJs();
|
|
1547
1548
|
const fs = await import('fs');
|
|
1548
1549
|
// Load database
|
|
1549
|
-
const fileBuffer =
|
|
1550
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1550
1551
|
const db = new SQL.Database(fileBuffer);
|
|
1551
1552
|
// Test 1: Schema verification
|
|
1552
1553
|
const schemaStart = Date.now();
|
|
@@ -1679,7 +1680,7 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1679
1680
|
db.run(`DELETE FROM memory_entries WHERE id = ?`, [testId]);
|
|
1680
1681
|
// Save changes
|
|
1681
1682
|
const data = db.export();
|
|
1682
|
-
|
|
1683
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
1683
1684
|
db.close();
|
|
1684
1685
|
const passed = tests.filter(t => t.passed).length;
|
|
1685
1686
|
const failed = tests.filter(t => !t.passed).length;
|
|
@@ -1740,7 +1741,7 @@ export async function storeEntry(options) {
|
|
|
1740
1741
|
await ensureSchemaColumns(dbPath);
|
|
1741
1742
|
const initSqlJs = (await import('sql.js')).default;
|
|
1742
1743
|
const SQL = await initSqlJs();
|
|
1743
|
-
const fileBuffer =
|
|
1744
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1744
1745
|
const db = new SQL.Database(fileBuffer);
|
|
1745
1746
|
const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
1746
1747
|
const now = Date.now();
|
|
@@ -1782,7 +1783,7 @@ export async function storeEntry(options) {
|
|
|
1782
1783
|
]);
|
|
1783
1784
|
// Save
|
|
1784
1785
|
const data = db.export();
|
|
1785
|
-
|
|
1786
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
1786
1787
|
db.close();
|
|
1787
1788
|
// Add to HNSW index for faster future searches
|
|
1788
1789
|
if (embeddingJson) {
|
|
@@ -1843,7 +1844,7 @@ export async function searchEntries(options) {
|
|
|
1843
1844
|
// Rerank candidates with exact cosine similarity from SQLite
|
|
1844
1845
|
const initSqlJs = (await import('sql.js')).default;
|
|
1845
1846
|
const SQL = await initSqlJs();
|
|
1846
|
-
const fileBuffer =
|
|
1847
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1847
1848
|
const db = new SQL.Database(fileBuffer);
|
|
1848
1849
|
const reranked = [];
|
|
1849
1850
|
for (const candidate of rabitqCandidates) {
|
|
@@ -1893,7 +1894,7 @@ export async function searchEntries(options) {
|
|
|
1893
1894
|
// Fall back to brute-force SQLite search
|
|
1894
1895
|
const initSqlJs = (await import('sql.js')).default;
|
|
1895
1896
|
const SQL = await initSqlJs();
|
|
1896
|
-
const fileBuffer =
|
|
1897
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1897
1898
|
const db = new SQL.Database(fileBuffer);
|
|
1898
1899
|
// Get entries with embeddings
|
|
1899
1900
|
const searchStmt = db.prepare(effectiveNamespace !== 'all'
|
|
@@ -2004,7 +2005,7 @@ export async function listEntries(options) {
|
|
|
2004
2005
|
await ensureSchemaColumns(dbPath);
|
|
2005
2006
|
const initSqlJs = (await import('sql.js')).default;
|
|
2006
2007
|
const SQL = await initSqlJs();
|
|
2007
|
-
const fileBuffer =
|
|
2008
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2008
2009
|
const db = new SQL.Database(fileBuffer);
|
|
2009
2010
|
// Get total count
|
|
2010
2011
|
const countStmt = namespace
|
|
@@ -2089,7 +2090,7 @@ export async function getEntry(options) {
|
|
|
2089
2090
|
await ensureSchemaColumns(dbPath);
|
|
2090
2091
|
const initSqlJs = (await import('sql.js')).default;
|
|
2091
2092
|
const SQL = await initSqlJs();
|
|
2092
|
-
const fileBuffer =
|
|
2093
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2093
2094
|
const db = new SQL.Database(fileBuffer);
|
|
2094
2095
|
// Find entry by key
|
|
2095
2096
|
const getStmt = db.prepare(`
|
|
@@ -2120,7 +2121,7 @@ export async function getEntry(options) {
|
|
|
2120
2121
|
`, [String(id)]);
|
|
2121
2122
|
// Save updated database
|
|
2122
2123
|
const data = db.export();
|
|
2123
|
-
|
|
2124
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
2124
2125
|
db.close();
|
|
2125
2126
|
let tags = [];
|
|
2126
2127
|
if (tagsJson) {
|
|
@@ -2200,7 +2201,7 @@ export async function deleteEntry(options) {
|
|
|
2200
2201
|
await ensureSchemaColumns(dbPath);
|
|
2201
2202
|
const initSqlJs = (await import('sql.js')).default;
|
|
2202
2203
|
const SQL = await initSqlJs();
|
|
2203
|
-
const fileBuffer =
|
|
2204
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2204
2205
|
const db = new SQL.Database(fileBuffer);
|
|
2205
2206
|
// Check if entry exists first
|
|
2206
2207
|
const checkStmt = db.prepare(`
|
|
@@ -2249,7 +2250,7 @@ export async function deleteEntry(options) {
|
|
|
2249
2250
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
2250
2251
|
// Save updated database
|
|
2251
2252
|
const data = db.export();
|
|
2252
|
-
|
|
2253
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
2253
2254
|
db.close();
|
|
2254
2255
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
2255
2256
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
@@ -279,6 +279,8 @@ export async function checkContent(cid, gateway = 'https://w3s.link') {
|
|
|
279
279
|
try {
|
|
280
280
|
const response = await fetch(`${gateway}/ipfs/${cid}`, {
|
|
281
281
|
method: 'HEAD',
|
|
282
|
+
// audit_1776853149979: HEAD probe should never hang; 10s upper bound.
|
|
283
|
+
signal: AbortSignal.timeout(10000),
|
|
282
284
|
});
|
|
283
285
|
if (response.ok) {
|
|
284
286
|
const size = parseInt(response.headers.get('content-length') || '0', 10);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { UpdateCheckResult } from './checker.js';
|
|
6
6
|
import { ValidationResult } from './validator.js';
|
|
7
|
+
export declare function isSafePackageSpec(pkg: string, version: string): boolean;
|
|
7
8
|
export interface UpdateHistoryEntry {
|
|
8
9
|
timestamp: string;
|
|
9
10
|
package: string;
|
|
@@ -2,11 +2,27 @@
|
|
|
2
2
|
* Update executor - performs actual package updates
|
|
3
3
|
* Includes rollback capability
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import { validateUpdate } from './validator.js';
|
|
10
|
+
/**
|
|
11
|
+
* audit_1776853149979: package name and version come from npm-view output and
|
|
12
|
+
* the update-history.json file (writable by anyone with FS access). Both
|
|
13
|
+
* previously interpolated straight into a shell string for `npm install`.
|
|
14
|
+
* These regexes pre-flight values so a hostile package name can't slip
|
|
15
|
+
* shell metacharacters through, even though execFileSync below already
|
|
16
|
+
* eliminates the shell.
|
|
17
|
+
*/
|
|
18
|
+
// First char of the unscoped name forbids `-` to defang CLI-flag confusion
|
|
19
|
+
// when the spec is passed to npm (npm install -evil@1.0.0 looks flag-shaped).
|
|
20
|
+
const SAFE_PKG_RE = /^(@[a-zA-Z0-9_\-]+\/)?[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,213}$/;
|
|
21
|
+
// semver / dist-tag / range chars only — no shell metas.
|
|
22
|
+
const SAFE_VERSION_RE = /^[a-zA-Z0-9._\-+~^*xX]{1,64}$/;
|
|
23
|
+
export function isSafePackageSpec(pkg, version) {
|
|
24
|
+
return SAFE_PKG_RE.test(pkg) && SAFE_VERSION_RE.test(version);
|
|
25
|
+
}
|
|
10
26
|
const HISTORY_FILE = path.join(os.homedir(), '.claude-flow', 'update-history.json');
|
|
11
27
|
const MAX_HISTORY_ENTRIES = 100;
|
|
12
28
|
function ensureDir() {
|
|
@@ -58,13 +74,25 @@ export async function executeUpdate(update, installedPackages, dryRun = false) {
|
|
|
58
74
|
validation,
|
|
59
75
|
};
|
|
60
76
|
}
|
|
77
|
+
// audit_1776853149979: validate package + version regex before any exec.
|
|
78
|
+
if (!isSafePackageSpec(update.package, update.latestVersion)) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
package: update.package,
|
|
82
|
+
version: update.latestVersion,
|
|
83
|
+
error: `Refusing to install: package or version contains disallowed characters (pkg="${update.package}", version="${update.latestVersion}")`,
|
|
84
|
+
validation,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
61
87
|
try {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
// audit_1776853149979: switched to execFileSync('npm', argv) — no shell,
|
|
89
|
+
// so even if validation regressed, metas in update.package would stay
|
|
90
|
+
// literal in the argv slot.
|
|
91
|
+
execFileSync('npm', ['install', `${update.package}@${update.latestVersion}`, '--save-exact'], {
|
|
65
92
|
encoding: 'utf-8',
|
|
66
93
|
stdio: 'pipe',
|
|
67
94
|
timeout: 60000, // 1 minute timeout
|
|
95
|
+
shell: false,
|
|
68
96
|
});
|
|
69
97
|
// Record successful update
|
|
70
98
|
recordUpdate({
|
|
@@ -139,13 +167,21 @@ export async function rollbackUpdate(packageName) {
|
|
|
139
167
|
: 'No rollback available',
|
|
140
168
|
};
|
|
141
169
|
}
|
|
170
|
+
// audit_1776853149979: history entries can be tampered with by anyone who
|
|
171
|
+
// can write update-history.json — gate before exec.
|
|
172
|
+
if (!isSafePackageSpec(lastUpdate.package, lastUpdate.fromVersion)) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
message: `Refusing to rollback: package or version contains disallowed characters (pkg="${lastUpdate.package}", version="${lastUpdate.fromVersion}")`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
142
178
|
try {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
execSync(installCmd, {
|
|
179
|
+
// execFileSync, no shell.
|
|
180
|
+
execFileSync('npm', ['install', `${lastUpdate.package}@${lastUpdate.fromVersion}`, '--save-exact'], {
|
|
146
181
|
encoding: 'utf-8',
|
|
147
182
|
stdio: 'pipe',
|
|
148
183
|
timeout: 60000,
|
|
184
|
+
shell: false,
|
|
149
185
|
});
|
|
150
186
|
// Record the rollback
|
|
151
187
|
recordUpdate({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"sessionId":"36428a63-dfb2-42a4-a159-cf8be916193e","pid":71027,"procStart":"Sun May 3 23:00:23 2026","acquiredAt":1777849234814}
|