claude-flow 3.6.23 → 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.
Files changed (27) hide show
  1. package/README.md +8 -2
  2. package/package.json +1 -1
  3. package/v3/@claude-flow/cli/README.md +8 -2
  4. package/v3/@claude-flow/cli/bin/cli.js +21 -0
  5. package/v3/@claude-flow/cli/bin/mcp-server.js +16 -0
  6. package/v3/@claude-flow/cli/dist/src/commands/appliance.js +8 -10
  7. package/v3/@claude-flow/cli/dist/src/commands/guidance.js +1 -5
  8. package/v3/@claude-flow/cli/dist/src/commands/performance.js +3 -3
  9. package/v3/@claude-flow/cli/dist/src/commands/process.js +6 -7
  10. package/v3/@claude-flow/cli/dist/src/commands/verify.js +24 -3
  11. package/v3/@claude-flow/cli/dist/src/encryption/vault.d.ts +94 -0
  12. package/v3/@claude-flow/cli/dist/src/encryption/vault.js +172 -0
  13. package/v3/@claude-flow/cli/dist/src/fs-secure.d.ts +67 -0
  14. package/v3/@claude-flow/cli/dist/src/fs-secure.js +74 -0
  15. package/v3/@claude-flow/cli/dist/src/mcp-tools/github-tools.js +122 -31
  16. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +2 -2
  17. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +7 -12
  18. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +24 -12
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/terminal-tools.js +22 -7
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.d.ts +12 -0
  21. package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.js +56 -0
  22. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +33 -3
  23. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +17 -16
  24. package/v3/@claude-flow/cli/dist/src/transfer/ipfs/upload.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/update/executor.d.ts +1 -0
  26. package/v3/@claude-flow/cli/dist/src/update/executor.js +43 -7
  27. package/v3/@claude-flow/cli/package.json +1 -1
@@ -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
  */
@@ -252,6 +252,7 @@ async function getRegistry(dbPath) {
252
252
  // (separate from the main memory.db so the audit trail
253
253
  // is isolated). Best-effort: if better-sqlite3 isn't
254
254
  // resolvable in this env, skip cleanly.
255
+ let attestationInst = null;
255
256
  if (!reg.get('attestationLog')) {
256
257
  try {
257
258
  const attestationFile = path.join(adbDir, 'dist/src/security/AttestationLog.js');
@@ -267,6 +268,7 @@ async function getRegistry(dbPath) {
267
268
  const Ctor = mod.AttestationLog;
268
269
  if (typeof Ctor === 'function') {
269
270
  const inst = new Ctor({ db });
271
+ attestationInst = inst;
270
272
  if (typeof reg.set === 'function')
271
273
  reg.set('attestationLog', inst);
272
274
  else
@@ -276,6 +278,36 @@ async function getRegistry(dbPath) {
276
278
  }
277
279
  catch { /* better-sqlite3 missing or schema init failed — skip silently */ }
278
280
  }
281
+ // ADR-095 G7 follow-up: GuardedVectorBackend wraps the
282
+ // existing vectorBackend with mutationGuard + attestationLog
283
+ // for proof-gated state mutations (ADR-060). All three
284
+ // dependencies are reachable here — vectorBackend is in
285
+ // the baseline init, mutationGuard was just activated, and
286
+ // attestationLog is constructed above. Skip if any piece
287
+ // is missing rather than constructing with undefined.
288
+ if (!reg.get('guardedVectorBackend')) {
289
+ try {
290
+ const gvbFile = path.join(adbDir, 'dist/src/backends/ruvector/GuardedVectorBackend.js');
291
+ if (fs.existsSync(gvbFile)) {
292
+ const inner = reg.get('vectorBackend');
293
+ const guard = reg.get('mutationGuard');
294
+ const log = attestationInst ?? reg.get('attestationLog');
295
+ if (inner && guard) {
296
+ const url = pathToFileURL(gvbFile).href;
297
+ const mod = await import(url);
298
+ const Ctor = mod.GuardedVectorBackend;
299
+ if (typeof Ctor === 'function') {
300
+ const inst = new Ctor(inner, guard, log);
301
+ if (typeof reg.set === 'function')
302
+ reg.set('guardedVectorBackend', inst);
303
+ else
304
+ reg._controllers = { ...(reg._controllers || {}), guardedVectorBackend: inst };
305
+ }
306
+ }
307
+ }
308
+ }
309
+ catch { /* GuardedVectorBackend optional */ }
310
+ }
279
311
  }
280
312
  }
281
313
  catch { /* G7 wiring optional */ }
@@ -284,9 +316,7 @@ async function getRegistry(dbPath) {
284
316
  // path doesn't tear down the rest of the post-init wiring.
285
317
  await Promise.allSettled([intelligencePromise, agentdbPromise]);
286
318
  // Remaining disabled controllers tracked in ADR-095 G7 for
287
- // per-controller activation ADRs (each needs config / key
288
- // material that we don't pass blindly):
289
- // - guardedVectorBackend (secured backend — needs key material)
319
+ // per-controller activation ADRs:
290
320
  // - graphAdapter (graph DB adapter — needs graph DB connection)
291
321
  }
292
322
  catch {
@@ -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 = fs.readFileSync(dbPath);
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 = fs.readFileSync(dbPath);
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
- fs.writeFileSync(dbPath, Buffer.from(data));
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
- fs.writeFileSync(dbPath, buffer);
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
- fs.writeFileSync(dbPath, sqliteHeader);
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 = fs.readFileSync(dbPath);
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
- fs.writeFileSync(dbPath, Buffer.from(data));
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 = fs.readFileSync(dbPath);
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
- fs.writeFileSync(dbPath, Buffer.from(data));
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 = fs.readFileSync(dbPath);
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 = fs.readFileSync(dbPath);
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 = fs.readFileSync(dbPath);
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 = fs.readFileSync(dbPath);
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
- fs.writeFileSync(dbPath, Buffer.from(data));
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 = fs.readFileSync(dbPath);
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
- fs.writeFileSync(dbPath, Buffer.from(data));
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 { execSync } from 'child_process';
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
- // Execute npm install
63
- const installCmd = `npm install ${update.package}@${update.latestVersion} --save-exact`;
64
- execSync(installCmd, {
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
- // Install the previous version
144
- const installCmd = `npm install ${lastUpdate.package}@${lastUpdate.fromVersion} --save-exact`;
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.23",
3
+ "version": "3.6.25",
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",