@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.
- package/.claude/helpers/auto-memory-hook.mjs +224 -53
- package/.claude/helpers/helpers.manifest.json +4 -4
- package/.claude/helpers/intelligence.cjs +23 -1
- package/.claude/settings.json +2 -1
- package/README.md +2 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/daemon.js +20 -1
- package/dist/src/commands/hive-mind-worker.d.ts +10 -0
- package/dist/src/commands/hive-mind-worker.js +20 -0
- package/dist/src/commands/hive-mind.js +27 -4
- package/dist/src/commands/memory.js +4 -3
- package/dist/src/init/helpers-generator.js +16 -1
- package/dist/src/init/settings-generator.js +4 -0
- package/dist/src/mcp-tools/agent-tools.js +4 -4
- package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
- package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
- package/dist/src/mcp-tools/agentdb-tools.js +3 -3
- package/dist/src/mcp-tools/browser-argv.d.ts +18 -0
- package/dist/src/mcp-tools/browser-argv.js +59 -0
- package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
- package/dist/src/mcp-tools/browser-tools.js +13 -6
- package/dist/src/mcp-tools/claims-tools.js +16 -9
- package/dist/src/mcp-tools/hooks-tools.js +11 -1
- package/dist/src/mcp-tools/memory-tools.js +49 -32
- package/dist/src/mcp-tools/neural-tools.js +8 -2
- package/dist/src/mcp-tools/path-containment.d.ts +34 -0
- package/dist/src/mcp-tools/path-containment.js +133 -0
- package/dist/src/mcp-tools/record-store.d.ts +7 -0
- package/dist/src/mcp-tools/record-store.js +19 -2
- package/dist/src/mcp-tools/session-tools.js +4 -38
- package/dist/src/mcp-tools/swarm-tools.js +4 -0
- package/dist/src/mcp-tools/task-tools.js +11 -0
- package/dist/src/mcp-tools/terminal-tools.js +7 -2
- package/dist/src/mcp-tools/validate-input.d.ts +2 -0
- package/dist/src/mcp-tools/validate-input.js +14 -0
- package/dist/src/missions/schemas.d.ts +10 -10
- package/dist/src/mods/headless-plugins.d.ts +16 -0
- package/dist/src/mods/headless-plugins.js +30 -1
- package/dist/src/services/daemon-autostart.d.ts +24 -0
- package/dist/src/services/daemon-autostart.js +55 -1
- package/dist/src/services/headless-worker-executor.d.ts +15 -0
- package/dist/src/services/headless-worker-executor.js +28 -0
- package/package.json +1 -1
- 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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
1653
|
-
|
|
1654
|
-
:
|
|
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 =
|
|
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
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
|
1791
|
-
return { error:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
}>;
|