@claude-flow/cli 3.71.2 → 3.71.4

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 (44) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +224 -53
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/intelligence.cjs +23 -1
  4. package/.claude/settings.json +2 -1
  5. package/README.md +2 -0
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/daemon.js +20 -1
  8. package/dist/src/commands/hive-mind-worker.d.ts +10 -0
  9. package/dist/src/commands/hive-mind-worker.js +20 -0
  10. package/dist/src/commands/hive-mind.js +27 -4
  11. package/dist/src/commands/memory.js +4 -3
  12. package/dist/src/init/helpers-generator.js +16 -1
  13. package/dist/src/init/settings-generator.js +4 -0
  14. package/dist/src/mcp-tools/agent-tools.js +4 -4
  15. package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
  16. package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
  17. package/dist/src/mcp-tools/agentdb-tools.js +3 -3
  18. package/dist/src/mcp-tools/browser-argv.d.ts +18 -0
  19. package/dist/src/mcp-tools/browser-argv.js +59 -0
  20. package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
  21. package/dist/src/mcp-tools/browser-tools.js +13 -6
  22. package/dist/src/mcp-tools/claims-tools.js +16 -9
  23. package/dist/src/mcp-tools/hooks-tools.js +11 -1
  24. package/dist/src/mcp-tools/memory-tools.js +49 -32
  25. package/dist/src/mcp-tools/neural-tools.js +8 -2
  26. package/dist/src/mcp-tools/path-containment.d.ts +34 -0
  27. package/dist/src/mcp-tools/path-containment.js +133 -0
  28. package/dist/src/mcp-tools/record-store.d.ts +7 -0
  29. package/dist/src/mcp-tools/record-store.js +19 -2
  30. package/dist/src/mcp-tools/session-tools.js +4 -38
  31. package/dist/src/mcp-tools/swarm-tools.js +4 -0
  32. package/dist/src/mcp-tools/task-tools.js +11 -0
  33. package/dist/src/mcp-tools/terminal-tools.js +7 -2
  34. package/dist/src/mcp-tools/validate-input.d.ts +2 -0
  35. package/dist/src/mcp-tools/validate-input.js +14 -0
  36. package/dist/src/missions/schemas.d.ts +10 -10
  37. package/dist/src/mods/headless-plugins.d.ts +16 -0
  38. package/dist/src/mods/headless-plugins.js +30 -1
  39. package/dist/src/services/daemon-autostart.d.ts +24 -0
  40. package/dist/src/services/daemon-autostart.js +55 -1
  41. package/dist/src/services/headless-worker-executor.d.ts +15 -0
  42. package/dist/src/services/headless-worker-executor.js +28 -0
  43. package/package.json +1 -1
  44. package/plugins/ruflo-console/dist/tui.mjs +132 -132
@@ -12,6 +12,7 @@
12
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
13
13
  import { homedir } from 'os';
14
14
  import { basename, dirname, join, resolve } from 'path';
15
+ import { containedReadPath, containedWritePath, dbPathError, isLocalCliCall } from './path-containment.js';
15
16
  import { execFileSync } from 'node:child_process';
16
17
  import { createHash } from 'crypto';
17
18
  import { getProjectCwd } from './types.js';
@@ -27,10 +28,21 @@ const LEGACY_MEMORY_DIR = '.claude-flow/memory';
27
28
  * deliberately not an implicit `CLAUDE_FLOW_DB_PATH` override because the
28
29
  * native/encrypted CLI store and the AgentDB sibling are separate backends.
29
30
  */
30
- function resolveDbPathOption(inputDbPath) {
31
- return typeof inputDbPath === 'string' && inputDbPath.trim().length > 0
32
- ? resolve(inputDbPath)
33
- : undefined;
31
+ /**
32
+ * Resolve an explicit `dbPath`. MCP callers may only name a SQLite database
33
+ * file, so the memory tools cannot be aimed at and overwrite an unrelated file;
34
+ * the local CLI (`--path`) keeps operator-chosen paths as-is.
35
+ */
36
+ function resolveDbPathOption(inputDbPath, context) {
37
+ if (typeof inputDbPath !== 'string' || inputDbPath.trim().length === 0)
38
+ return undefined;
39
+ const dbPath = resolve(inputDbPath);
40
+ if (!isLocalCliCall(context)) {
41
+ const error = dbPathError(dbPath);
42
+ if (error)
43
+ throw new Error(error);
44
+ }
45
+ return dbPath;
34
46
  }
35
47
  const MIGRATION_MARKER = '.migrated-to-sqlite';
36
48
  function getMemoryDir() {
@@ -462,12 +474,12 @@ export const memoryTools = [
462
474
  },
463
475
  required: ['key', 'value'],
464
476
  },
465
- handler: async (input) => {
477
+ handler: async (input, context) => {
466
478
  const missingKey = missingRequiredString(input, 'key', 'memory_store');
467
479
  if (missingKey) {
468
480
  return { success: false, key: input.key, stored: false, hasEmbedding: false, ...missingKey };
469
481
  }
470
- const dbPath = resolveDbPathOption(input.dbPath);
482
+ const dbPath = resolveDbPathOption(input.dbPath, context);
471
483
  if (!dbPath)
472
484
  await ensureInitialized();
473
485
  const { storeEntry } = await getMemoryFunctions();
@@ -549,12 +561,12 @@ export const memoryTools = [
549
561
  },
550
562
  required: ['key'],
551
563
  },
552
- handler: async (input) => {
564
+ handler: async (input, context) => {
553
565
  const missingKey = missingRequiredString(input, 'key', 'memory_retrieve');
554
566
  if (missingKey) {
555
567
  return { key: input.key, namespace: input.namespace, value: null, found: false, ...missingKey };
556
568
  }
557
- const dbPath = resolveDbPathOption(input.dbPath);
569
+ const dbPath = resolveDbPathOption(input.dbPath, context);
558
570
  if (!dbPath)
559
571
  await ensureInitialized();
560
572
  const { getEntry } = await getMemoryFunctions();
@@ -624,12 +636,12 @@ export const memoryTools = [
624
636
  },
625
637
  required: ['query'],
626
638
  },
627
- handler: async (input) => {
639
+ handler: async (input, context) => {
628
640
  const missingQuery = missingRequiredString(input, 'query', 'memory_search');
629
641
  if (missingQuery) {
630
642
  return { query: input.query, results: [], total: 0, ...missingQuery };
631
643
  }
632
- const dbPath = resolveDbPathOption(input.dbPath);
644
+ const dbPath = resolveDbPathOption(input.dbPath, context);
633
645
  if (!dbPath)
634
646
  await ensureInitialized();
635
647
  const { searchEntries } = await getMemoryFunctions();
@@ -810,12 +822,12 @@ export const memoryTools = [
810
822
  },
811
823
  required: ['key'],
812
824
  },
813
- handler: async (input) => {
825
+ handler: async (input, context) => {
814
826
  const missingKey = missingRequiredString(input, 'key', 'memory_delete');
815
827
  if (missingKey) {
816
828
  return { success: false, key: input.key, namespace: input.namespace, deleted: false, ...missingKey };
817
829
  }
818
- const dbPath = resolveDbPathOption(input.dbPath);
830
+ const dbPath = resolveDbPathOption(input.dbPath, context);
819
831
  if (!dbPath)
820
832
  await ensureInitialized();
821
833
  const { deleteEntry } = await getMemoryFunctions();
@@ -862,8 +874,8 @@ export const memoryTools = [
862
874
  dbPath: { type: 'string', description: 'Explicit SQLite store to read/write; omit to use the MCP default (the native/AgentDB sibling).' },
863
875
  },
864
876
  },
865
- handler: async (input) => {
866
- const dbPath = resolveDbPathOption(input.dbPath);
877
+ handler: async (input, context) => {
878
+ const dbPath = resolveDbPathOption(input.dbPath, context);
867
879
  if (!dbPath)
868
880
  await ensureInitialized();
869
881
  const { listEntries } = await getMemoryFunctions();
@@ -939,11 +951,11 @@ export const memoryTools = [
939
951
  dbPath: { type: 'string', description: 'Explicit SQLite store to read/write; omit to use the MCP default (the native/AgentDB sibling).' },
940
952
  },
941
953
  },
942
- handler: async (input) => {
954
+ handler: async (input, context) => {
943
955
  if (input.dbPath !== undefined && (typeof input.dbPath !== 'string' || !input.dbPath.trim())) {
944
956
  return memoryStatsUnavailable('dbPath must be a non-empty string');
945
957
  }
946
- const dbPath = resolveDbPathOption(input.dbPath);
958
+ const dbPath = resolveDbPathOption(input.dbPath, context);
947
959
  // An explicit read must not initialize or migrate the unrelated default store.
948
960
  if (!dbPath)
949
961
  await ensureInitialized();
@@ -1648,18 +1660,19 @@ export const memoryTools = [
1648
1660
  },
1649
1661
  required: ['outputPath'],
1650
1662
  },
1651
- handler: async (input) => {
1652
- const requestedDbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
1653
- ? resolve(input.dbPath)
1654
- : undefined;
1663
+ handler: async (input, context) => {
1664
+ const requestedOutput = String(input.outputPath ?? '');
1665
+ if (!requestedOutput)
1666
+ return { error: 'outputPath is required' };
1667
+ const outputPath = isLocalCliCall(context) ? resolve(requestedOutput) : containedWritePath(requestedOutput, ['.json']);
1668
+ if (!outputPath)
1669
+ return { error: 'outputPath must be a .json file inside the project directory (not a symlink, not in .git)' };
1670
+ const requestedDbPath = resolveDbPathOption(input.dbPath, context);
1655
1671
  // An explicit file selection is a single-store read. Do not initialize or
1656
1672
  // migrate the unrelated default store just to export a snapshot.
1657
1673
  if (!requestedDbPath)
1658
1674
  await ensureInitialized();
1659
1675
  const { listEntries } = await getMemoryFunctions();
1660
- const outputPath = String(input.outputPath ?? '');
1661
- if (!outputPath)
1662
- return { error: 'outputPath is required' };
1663
1676
  const namespace = input.namespace ? String(input.namespace) : undefined;
1664
1677
  if (namespace) {
1665
1678
  const v = validateIdentifier(namespace, 'namespace');
@@ -1770,25 +1783,29 @@ export const memoryTools = [
1770
1783
  },
1771
1784
  required: ['inputPath'],
1772
1785
  },
1773
- handler: async (input) => {
1774
- const dbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
1775
- ? resolve(input.dbPath)
1776
- : undefined;
1786
+ handler: async (input, context) => {
1787
+ const dbPath = resolveDbPathOption(input.dbPath, context);
1777
1788
  await ensureInitialized(dbPath);
1778
1789
  const { storeEntry } = await getMemoryFunctions();
1779
1790
  const t0 = Date.now();
1780
1791
  // Values are re-embedded on import; count the vectors actually written
1781
1792
  // rather than reporting a constant 0 next to entries that show a vector.
1782
1793
  let vectors = 0;
1783
- const inputPath = String(input.inputPath ?? '');
1784
- if (!inputPath || !existsSync(inputPath))
1785
- return { error: `File not found: ${inputPath || '(empty)'}` };
1794
+ const requestedInput = String(input.inputPath ?? '');
1795
+ const inputPath = !requestedInput ? null
1796
+ : isLocalCliCall(context) ? (existsSync(requestedInput) ? resolve(requestedInput) : null)
1797
+ : containedReadPath(requestedInput);
1798
+ if (!inputPath) {
1799
+ return { error: isLocalCliCall(context) || !requestedInput
1800
+ ? `File not found: ${requestedInput || '(empty)'}`
1801
+ : 'inputPath must be an existing regular file inside the project directory (not a symlink)' };
1802
+ }
1786
1803
  let doc;
1787
1804
  try {
1788
1805
  doc = JSON.parse(readFileSync(inputPath, 'utf-8'));
1789
1806
  }
1790
- catch (e) {
1791
- return { error: `Invalid export JSON: ${e.message}` };
1807
+ catch {
1808
+ return { error: 'inputPath is not a valid memory export (JSON parse failed)' };
1792
1809
  }
1793
1810
  const entries = Array.isArray(doc.entries) ? doc.entries : [];
1794
1811
  const nsOverride = input.namespace ? String(input.namespace) : undefined;
@@ -12,6 +12,7 @@
12
12
  * Note: For production neural features, use @claude-flow/neural module
13
13
  */
14
14
  import { getProjectCwd } from './types.js';
15
+ import { nullPrototypeRecords } from './record-store.js';
15
16
  import { validateIdentifier, validateText } from './validate-input.js';
16
17
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
18
  import { join } from 'node:path';
@@ -223,13 +224,18 @@ function loadNeuralStore() {
223
224
  try {
224
225
  const path = getNeuralPath();
225
226
  if (existsSync(path)) {
226
- return JSON.parse(readFileSync(path, 'utf-8'));
227
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
228
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
229
+ parsed.models = nullPrototypeRecords(parsed.models);
230
+ parsed.patterns = nullPrototypeRecords(parsed.patterns);
231
+ return parsed;
232
+ }
227
233
  }
228
234
  }
229
235
  catch {
230
236
  // Return empty store
231
237
  }
232
- return { models: {}, patterns: {}, version: '3.0.0' };
238
+ return { models: nullPrototypeRecords(undefined), patterns: nullPrototypeRecords(undefined), version: '3.0.0' };
233
239
  }
234
240
  /**
235
241
  * ADR-176 flywheel: expose the stored patterns (id/name/content) so the
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Path checks for MCP tools that read or write a caller-named file.
3
+ *
4
+ * Paths in tool input come from whoever drives the MCP server, including a
5
+ * model following injected instructions. A tool that writes or reads such a
6
+ * path must keep it inside the project directory, and must not follow a
7
+ * symlink out of it.
8
+ */
9
+ /**
10
+ * Resolve a caller-named output file inside the project. The real parent
11
+ * directory must be inside the real project root and outside `.git`, and an
12
+ * existing target must be a regular, non-executable file — not a symlink.
13
+ * `extensions`, when given, lists the allowed lower-case file extensions.
14
+ */
15
+ export declare function containedWritePath(p: string, extensions?: readonly string[]): string | null;
16
+ /** Resolve a caller-named input file inside the project: a regular file, not a symlink. */
17
+ export declare function containedReadPath(p: string): string | null;
18
+ /**
19
+ * Check a caller-named memory database path. It may live outside the project
20
+ * (the CLI's `--path` flag passes one), so this does not contain it; instead
21
+ * it refuses anything that is not plainly a SQLite database (plaintext, or
22
+ * encrypted at rest per ADR-096), so the memory tools cannot be pointed at and
23
+ * overwrite an unrelated file.
24
+ */
25
+ export declare function dbPathError(p: string): string | null;
26
+ /**
27
+ * Marks a tool call made by the local CLI on the operator's behalf (for
28
+ * example `ruflo memory export --output ~/backup.json`). MCP requests arrive
29
+ * as JSON and cannot carry a symbol key, so this cannot be forged remotely.
30
+ */
31
+ export declare const LOCAL_CLI_CALL: unique symbol;
32
+ export declare function localCliContext(): Record<string, unknown>;
33
+ export declare function isLocalCliCall(context: unknown): boolean;
34
+ //# sourceMappingURL=path-containment.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Path checks for MCP tools that read or write a caller-named file.
3
+ *
4
+ * Paths in tool input come from whoever drives the MCP server, including a
5
+ * model following injected instructions. A tool that writes or reads such a
6
+ * path must keep it inside the project directory, and must not follow a
7
+ * symlink out of it.
8
+ */
9
+ import { closeSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
10
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
11
+ import { isNonProjectDir } from '@claude-flow/cli-core/mcp-tools/types';
12
+ import { getProjectCwd } from './types.js';
13
+ import { existingFileIsEncrypted } from '../fs-secure.js';
14
+ function lstatOrNull(p) {
15
+ try {
16
+ return lstatSync(p);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function projectRoot() {
23
+ const root = realpathSync(getProjectCwd());
24
+ // A server started in a home or filesystem root has no project to contain
25
+ // writes to; "inside the root" would mean the whole home directory.
26
+ return isNonProjectDir(root) ? null : root;
27
+ }
28
+ function insideRoot(root, target) {
29
+ const rel = relative(root, target);
30
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
31
+ return false;
32
+ return !rel.split(sep).includes('.git');
33
+ }
34
+ /**
35
+ * Resolve a caller-named output file inside the project. The real parent
36
+ * directory must be inside the real project root and outside `.git`, and an
37
+ * existing target must be a regular, non-executable file — not a symlink.
38
+ * `extensions`, when given, lists the allowed lower-case file extensions.
39
+ */
40
+ export function containedWritePath(p, extensions) {
41
+ if (!p || p.includes('\0'))
42
+ return null;
43
+ try {
44
+ const root = projectRoot();
45
+ if (!root)
46
+ return null;
47
+ const abs = resolve(root, p);
48
+ if (extensions && !extensions.includes(extname(abs).toLowerCase()))
49
+ return null;
50
+ const target = join(realpathSync(dirname(abs)), basename(abs));
51
+ if (!insideRoot(root, target))
52
+ return null;
53
+ const existing = lstatOrNull(target);
54
+ if (existing && (existing.isSymbolicLink() || !existing.isFile() || (existing.mode & 0o111) !== 0))
55
+ return null;
56
+ return target;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /** Resolve a caller-named input file inside the project: a regular file, not a symlink. */
63
+ export function containedReadPath(p) {
64
+ if (!p || p.includes('\0'))
65
+ return null;
66
+ try {
67
+ const root = projectRoot();
68
+ if (!root)
69
+ return null;
70
+ const abs = resolve(root, p);
71
+ const existing = lstatOrNull(abs);
72
+ if (!existing || existing.isSymbolicLink() || !existing.isFile())
73
+ return null;
74
+ const target = realpathSync(abs);
75
+ return insideRoot(root, target) ? target : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3'];
82
+ const SQLITE_HEADER = 'SQLite format 3\0';
83
+ /**
84
+ * Check a caller-named memory database path. It may live outside the project
85
+ * (the CLI's `--path` flag passes one), so this does not contain it; instead
86
+ * it refuses anything that is not plainly a SQLite database (plaintext, or
87
+ * encrypted at rest per ADR-096), so the memory tools cannot be pointed at and
88
+ * overwrite an unrelated file.
89
+ */
90
+ export function dbPathError(p) {
91
+ if (p.includes('\0'))
92
+ return 'dbPath contains a null byte';
93
+ if (!DB_EXTENSIONS.includes(extname(p).toLowerCase())) {
94
+ return `dbPath must end in ${DB_EXTENSIONS.join(', ')}`;
95
+ }
96
+ const existing = lstatOrNull(p);
97
+ if (!existing)
98
+ return null;
99
+ if (existing.isSymbolicLink())
100
+ return 'dbPath must not be a symlink';
101
+ if (!existing.isFile())
102
+ return 'dbPath must be a regular file';
103
+ if (existing.size === 0 || existingFileIsEncrypted(p) === true)
104
+ return null;
105
+ const header = Buffer.alloc(SQLITE_HEADER.length);
106
+ let fd;
107
+ try {
108
+ fd = openSync(p, 'r');
109
+ readSync(fd, header, 0, header.length, 0);
110
+ }
111
+ catch {
112
+ return 'dbPath could not be read';
113
+ }
114
+ finally {
115
+ if (fd !== undefined)
116
+ closeSync(fd);
117
+ }
118
+ return header.toString('latin1') === SQLITE_HEADER ? null : 'dbPath is not a SQLite database';
119
+ }
120
+ /**
121
+ * Marks a tool call made by the local CLI on the operator's behalf (for
122
+ * example `ruflo memory export --output ~/backup.json`). MCP requests arrive
123
+ * as JSON and cannot carry a symbol key, so this cannot be forged remotely.
124
+ */
125
+ export const LOCAL_CLI_CALL = Symbol('ruflo.localCliCall');
126
+ export function localCliContext() {
127
+ return { [LOCAL_CLI_CALL]: true };
128
+ }
129
+ export function isLocalCliCall(context) {
130
+ return !!context && typeof context === 'object'
131
+ && context[LOCAL_CLI_CALL] === true;
132
+ }
133
+ //# sourceMappingURL=path-containment.js.map
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Give a record map a null prototype. Record keys come from MCP tool input; on
3
+ * a plain object `__proto__` or `constructor` resolve to Object.prototype or a
4
+ * built-in, which a handler would then write into. A null-prototype map only
5
+ * ever returns its own records.
6
+ */
7
+ export declare function nullPrototypeRecords<M extends object>(map: M | undefined | null): M;
1
8
  /** Only an absent store is empty. Failed reads must never authorize replacement. */
2
9
  export declare function readRecordStore<T>(path: string, collection: string, empty: () => T): T;
3
10
  //# sourceMappingURL=record-store.d.ts.map
@@ -1,4 +1,17 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ /**
3
+ * Give a record map a null prototype. Record keys come from MCP tool input; on
4
+ * a plain object `__proto__` or `constructor` resolve to Object.prototype or a
5
+ * built-in, which a handler would then write into. A null-prototype map only
6
+ * ever returns its own records.
7
+ */
8
+ export function nullPrototypeRecords(map) {
9
+ if (map && typeof map === 'object' && !Array.isArray(map)) {
10
+ Object.setPrototypeOf(map, null);
11
+ return map;
12
+ }
13
+ return Object.create(null);
14
+ }
2
15
  /** Only an absent store is empty. Failed reads must never authorize replacement. */
3
16
  export function readRecordStore(path, collection, empty) {
4
17
  let text;
@@ -6,8 +19,11 @@ export function readRecordStore(path, collection, empty) {
6
19
  text = readFileSync(path, 'utf-8');
7
20
  }
8
21
  catch (error) {
9
- if (error.code === 'ENOENT')
10
- return empty();
22
+ if (error.code === 'ENOENT') {
23
+ const store = empty();
24
+ store[collection] = nullPrototypeRecords(store[collection]);
25
+ return store;
26
+ }
11
27
  throw error;
12
28
  }
13
29
  const store = JSON.parse(text);
@@ -16,6 +32,7 @@ export function readRecordStore(path, collection, empty) {
16
32
  || !records || typeof records !== 'object' || Array.isArray(records)) {
17
33
  throw new Error(`Invalid ${collection} store at ${path}; existing data was preserved`);
18
34
  }
35
+ nullPrototypeRecords(records);
19
36
  return store;
20
37
  }
21
38
  //# sourceMappingURL=record-store.js.map
@@ -3,11 +3,12 @@
3
3
  *
4
4
  * Tool definitions for session management with file persistence.
5
5
  */
6
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { gunzipSync } from 'node:zlib';
9
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
9
+ import { join } from 'node:path';
10
10
  import { getProjectCwd } from './types.js';
11
+ import { containedWritePath } from './path-containment.js';
11
12
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
12
13
  import { validateIdentifier, validateText } from './validate-input.js';
13
14
  function isRecord(value) {
@@ -351,41 +352,6 @@ function isSessionRecordLike(value) {
351
352
  const v = value;
352
353
  return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
353
354
  }
354
- /**
355
- * SECURITY: `outputPath` comes from MCP tool input. Resolve it against the
356
- * project root and require the real (symlink-resolved) parent directory to stay
357
- * inside the real project root, and refuse an existing symlink as the target,
358
- * so session_export cannot write files elsewhere on the host. The CLI's
359
- * `session export` writes its own file and does not pass outputPath.
360
- */
361
- function containedExportPath(outputPath) {
362
- if (!outputPath || outputPath.includes('\0'))
363
- return null;
364
- try {
365
- const root = realpathSync(getProjectCwd());
366
- const abs = resolve(root, outputPath);
367
- const parent = realpathSync(dirname(abs));
368
- const target = join(parent, basename(abs));
369
- const rel = relative(root, target);
370
- if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
371
- return null;
372
- const existing = lstatOrNull(target);
373
- if (existing && (existing.isSymbolicLink() || !existing.isFile()))
374
- return null;
375
- return target;
376
- }
377
- catch {
378
- return null;
379
- }
380
- }
381
- function lstatOrNull(p) {
382
- try {
383
- return lstatSync(p);
384
- }
385
- catch {
386
- return null;
387
- }
388
- }
389
355
  export const sessionTools = [
390
356
  {
391
357
  name: 'session_save',
@@ -740,7 +706,7 @@ export const sessionTools = [
740
706
  let path = null;
741
707
  const outputPath = input.outputPath ? String(input.outputPath) : null;
742
708
  if (outputPath) {
743
- const target = containedExportPath(outputPath);
709
+ const target = containedWritePath(outputPath);
744
710
  if (!target)
745
711
  return { sessionId, error: 'outputPath must be a file inside the project directory (no symlinks)' };
746
712
  try {
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
8
8
  import { join } from 'node:path';
9
+ import { nullPrototypeRecords } from './record-store.js';
9
10
  import { getProjectCwd } from './types.js';
10
11
  import { validateIdentifier } from './validate-input.js';
11
12
  import { createApscState, isApscAgentEligible, normalizeApscConfig, recordApscSignal, } from '../services/pheromone-adaptive.js';
@@ -92,6 +93,9 @@ export function loadSwarmStore() {
92
93
  }
93
94
  }
94
95
  catch { /* fall through with default */ }
96
+ if (!store || typeof store !== 'object' || Array.isArray(store))
97
+ store = { swarms: {}, version: '3.0.0' };
98
+ store.swarms = nullPrototypeRecords(store.swarms);
95
99
  // #1799 — reconcile orphans on every load and persist if anything changed.
96
100
  // Cheap (process.kill(pid, 0) is sub-millisecond) and means
97
101
  // `swarm_status`/`swarm_health` never see ghost "running" entries.
@@ -117,6 +117,10 @@ export const taskTools = [
117
117
  const vDesc = validateText(input.description, 'description');
118
118
  if (!vDesc.valid)
119
119
  return { success: false, error: vDesc.error };
120
+ if (input.assignTo !== undefined && (!Array.isArray(input.assignTo)
121
+ || !input.assignTo.every(id => validateIdentifier(id, 'assignTo').valid))) {
122
+ return { success: false, error: 'assignTo must be an array of agent ids' };
123
+ }
120
124
  const store = loadTaskStore();
121
125
  const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
122
126
  const task = {
@@ -134,6 +138,13 @@ export const taskTools = [
134
138
  };
135
139
  store.tasks[taskId] = task;
136
140
  saveTaskStore(store);
141
+ // Assigning at creation must mark the workers busy, as task_assign and task_update do;
142
+ // otherwise they read idle while holding the task, and task_complete or task_cancel,
143
+ // which only release a worker whose currentTask is this task, never free them either.
144
+ updateAgents(task.assignedTo, (agent) => {
145
+ agent.status = 'busy';
146
+ agent.currentTask = taskId;
147
+ });
137
148
  return {
138
149
  taskId,
139
150
  type: task.type,
@@ -4,6 +4,7 @@
4
4
  * Terminal session management with real command execution.
5
5
  */
6
6
  import { getProjectCwd } from './types.js';
7
+ import { nullPrototypeRecords } from './record-store.js';
7
8
  import { existsSync } from 'node:fs';
8
9
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
9
10
  import { validateEnv, validateIdentifier, validatePath, validateText } from './validate-input.js';
@@ -33,13 +34,17 @@ function loadTerminalStore() {
33
34
  // ADR-096 Phase 3: readFileMaybeEncrypted handles both legacy
34
35
  // plaintext stores and post-migration encrypted ones via the RFE1
35
36
  // magic-byte sniff.
36
- return JSON.parse(readFileMaybeEncrypted(path, 'utf-8'));
37
+ const parsed = JSON.parse(readFileMaybeEncrypted(path, 'utf-8'));
38
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
39
+ parsed.sessions = nullPrototypeRecords(parsed.sessions);
40
+ return parsed;
41
+ }
37
42
  }
38
43
  }
39
44
  catch {
40
45
  // Return empty store
41
46
  }
42
- return { sessions: {}, version: '3.0.0' };
47
+ return { sessions: nullPrototypeRecords(undefined), version: '3.0.0' };
43
48
  }
44
49
  function saveTerminalStore(store) {
45
50
  ensureTerminalDir();
@@ -5,5 +5,7 @@
5
5
  * Was a 256-line byte-identical copy. Loads @claude-flow/security validators
6
6
  * when available, with lightweight fallback otherwise.
7
7
  */
8
+ import { type ValidationResult } from '@claude-flow/cli-core/mcp-tools/validate-input';
8
9
  export * from '@claude-flow/cli-core/mcp-tools/validate-input';
10
+ export declare function validateIdentifier(value: unknown, label: string): ValidationResult;
9
11
  //# sourceMappingURL=validate-input.d.ts.map
@@ -5,5 +5,19 @@
5
5
  * Was a 256-line byte-identical copy. Loads @claude-flow/security validators
6
6
  * when available, with lightweight fallback otherwise.
7
7
  */
8
+ import { validateIdentifier as coreValidateIdentifier, } from '@claude-flow/cli-core/mcp-tools/validate-input';
8
9
  export * from '@claude-flow/cli-core/mcp-tools/validate-input';
10
+ /**
11
+ * Names that resolve to Object.prototype or a built-in when used as a key on a
12
+ * plain object. Identifiers key the JSON record stores, so they are refused.
13
+ * Move this check into cli-core on its next release.
14
+ */
15
+ const RESERVED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
16
+ export function validateIdentifier(value, label) {
17
+ const result = coreValidateIdentifier(value, label);
18
+ if (result.valid && RESERVED_KEYS.has(result.sanitized)) {
19
+ return { valid: false, sanitized: '', error: `${label} must not be a reserved name (${result.sanitized})` };
20
+ }
21
+ return result;
22
+ }
9
23
  //# sourceMappingURL=validate-input.js.map
@@ -77,15 +77,15 @@ export declare const missionRecordSchema: z.ZodObject<{
77
77
  updatedAt: string;
78
78
  schemaVersion: 1;
79
79
  state: "completed" | "cancelled" | "failed" | "blocked" | "running" | "paused" | "queued" | "draft" | "planned" | "awaitingAuthorization" | "pauseRequested" | "verifying" | "cancelRequested";
80
- acceptance: {
81
- revision: number;
82
- criteriaDigest: string;
83
- };
84
80
  plan: {
85
81
  digest: string;
86
82
  revision: number;
87
83
  taskGraphRef: string;
88
84
  };
85
+ acceptance: {
86
+ revision: number;
87
+ criteriaDigest: string;
88
+ };
89
89
  objective: string;
90
90
  missionId: string;
91
91
  tenantId: string;
@@ -103,15 +103,15 @@ export declare const missionRecordSchema: z.ZodObject<{
103
103
  updatedAt: string;
104
104
  schemaVersion: 1;
105
105
  state: "completed" | "cancelled" | "failed" | "blocked" | "running" | "paused" | "queued" | "draft" | "planned" | "awaitingAuthorization" | "pauseRequested" | "verifying" | "cancelRequested";
106
- acceptance: {
107
- revision: number;
108
- criteriaDigest: string;
109
- };
110
106
  plan: {
111
107
  digest: string;
112
108
  revision: number;
113
109
  taskGraphRef: string;
114
110
  };
111
+ acceptance: {
112
+ revision: number;
113
+ criteriaDigest: string;
114
+ };
115
115
  objective: string;
116
116
  missionId: string;
117
117
  tenantId: string;
@@ -580,7 +580,6 @@ export declare const planInputSchema: z.ZodObject<{
580
580
  } | undefined;
581
581
  }>;
582
582
  }, "strict", z.ZodTypeAny, {
583
- requestId: string;
584
583
  plan: {
585
584
  tasks: {
586
585
  id: string;
@@ -617,10 +616,10 @@ export declare const planInputSchema: z.ZodObject<{
617
616
  baseline?: string | undefined;
618
617
  }[];
619
618
  };
619
+ requestId: string;
620
620
  missionId: string;
621
621
  expectedRevision: number;
622
622
  }, {
623
- requestId: string;
624
623
  plan: {
625
624
  tasks: {
626
625
  id: string;
@@ -657,6 +656,7 @@ export declare const planInputSchema: z.ZodObject<{
657
656
  capabilities?: string[] | undefined;
658
657
  } | undefined;
659
658
  };
659
+ requestId: string;
660
660
  missionId: string;
661
661
  expectedRevision: number;
662
662
  }>;