@claude-flow/cli 3.71.3 → 3.72.0
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 +17 -0
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +23 -10
- package/.claude/helpers/intelligence.cjs +23 -1
- package/.claude/helpers/statusline.cjs +29 -14
- package/README.md +18 -3
- package/bin/cli.js +13 -18
- package/bin/mcp-server.js +13 -18
- package/catalog-manifest.json +2 -2
- package/dist/src/appliance/rvfa-distribution.js +2 -2
- package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
- package/dist/src/benchmarks/gaia-critic.js +1 -7
- package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
- package/dist/src/benchmarks/gaia-decomposer.js +1 -8
- package/dist/src/commands/agent-wasm.js +2 -2
- package/dist/src/commands/analyze.js +1 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/commands/doctor.js +3 -2
- package/dist/src/commands/guidance.js +9 -9
- package/dist/src/commands/hooks.js +13 -8
- package/dist/src/commands/init.js +14 -16
- package/dist/src/commands/memory.js +4 -3
- package/dist/src/commands/neural.js +8 -6
- package/dist/src/commands/route.js +1 -1
- package/dist/src/commands/ruvector/backup.js +7 -5
- package/dist/src/commands/ruvector/benchmark.js +6 -1
- package/dist/src/commands/ruvector/import.js +19 -5
- package/dist/src/commands/ruvector/init.js +1 -1
- package/dist/src/commands/start.js +1 -1
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/task.js +18 -1
- package/dist/src/commands/update.js +1 -1
- package/dist/src/funnel/local-signals.js +7 -2
- package/dist/src/init/helpers-generator.js +16 -1
- package/dist/src/mcp-protocol.d.ts +90 -0
- package/dist/src/mcp-protocol.js +179 -0
- package/dist/src/mcp-server.d.ts +1 -0
- package/dist/src/mcp-server.js +20 -22
- package/dist/src/mcp-tools/agent-execute-core.js +9 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
- package/dist/src/mcp-tools/agent-tools.js +63 -12
- 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/browser-argv.d.ts +27 -0
- package/dist/src/mcp-tools/browser-argv.js +92 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
- package/dist/src/mcp-tools/browser-tools.js +25 -8
- package/dist/src/mcp-tools/coordination-tools.js +5 -4
- package/dist/src/mcp-tools/embeddings-tools.js +12 -4
- package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
- package/dist/src/mcp-tools/glob-exclusions.js +81 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
- package/dist/src/mcp-tools/hooks-tools.js +12 -5
- package/dist/src/mcp-tools/import-limits.d.ts +4 -0
- package/dist/src/mcp-tools/import-limits.js +28 -0
- package/dist/src/mcp-tools/memory-tools.js +68 -72
- 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/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +15 -0
- package/dist/src/mcp-tools/record-store.js +117 -3
- package/dist/src/mcp-tools/security-tools.d.ts +9 -0
- package/dist/src/mcp-tools/security-tools.js +39 -3
- package/dist/src/mcp-tools/session-tools.js +9 -41
- package/dist/src/mcp-tools/swarm-tools.js +36 -14
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +16 -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/mcp-tools/workflow-tools.js +13 -12
- package/dist/src/mcp-tools/x-federation-join.js +55 -14
- package/dist/src/memory/intelligence.d.ts +11 -0
- package/dist/src/memory/intelligence.js +84 -6
- package/dist/src/memory/memory-initializer.d.ts +6 -2
- package/dist/src/memory/memory-initializer.js +15 -6
- package/dist/src/ruvector/model-router.d.ts +6 -1
- package/dist/src/ruvector/model-router.js +16 -4
- package/dist/src/services/worker-daemon.js +30 -12
- package/dist/src/transfer/storage/gcs.js +3 -2
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Give a record map a null prototype. Record keys come from MCP tool input; on
|
|
5
|
+
* a plain object `__proto__` or `constructor` resolve to Object.prototype or a
|
|
6
|
+
* built-in, which a handler would then write into. A null-prototype map only
|
|
7
|
+
* ever returns its own records.
|
|
8
|
+
*/
|
|
9
|
+
export function nullPrototypeRecords(map) {
|
|
10
|
+
if (map && typeof map === 'object' && !Array.isArray(map)) {
|
|
11
|
+
Object.setPrototypeOf(map, null);
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
return Object.create(null);
|
|
15
|
+
}
|
|
2
16
|
/** Only an absent store is empty. Failed reads must never authorize replacement. */
|
|
3
17
|
export function readRecordStore(path, collection, empty) {
|
|
4
18
|
let text;
|
|
@@ -6,8 +20,11 @@ export function readRecordStore(path, collection, empty) {
|
|
|
6
20
|
text = readFileSync(path, 'utf-8');
|
|
7
21
|
}
|
|
8
22
|
catch (error) {
|
|
9
|
-
if (error.code === 'ENOENT')
|
|
10
|
-
|
|
23
|
+
if (error.code === 'ENOENT') {
|
|
24
|
+
const store = empty();
|
|
25
|
+
store[collection] = nullPrototypeRecords(store[collection]);
|
|
26
|
+
return store;
|
|
27
|
+
}
|
|
11
28
|
throw error;
|
|
12
29
|
}
|
|
13
30
|
const store = JSON.parse(text);
|
|
@@ -16,6 +33,103 @@ export function readRecordStore(path, collection, empty) {
|
|
|
16
33
|
|| !records || typeof records !== 'object' || Array.isArray(records)) {
|
|
17
34
|
throw new Error(`Invalid ${collection} store at ${path}; existing data was preserved`);
|
|
18
35
|
}
|
|
36
|
+
nullPrototypeRecords(records);
|
|
19
37
|
return store;
|
|
20
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* JSON object store read for workflow, hive-mind, and swarm (#4737).
|
|
41
|
+
* Same rule as {@link readRecordStore}: only a missing file is empty.
|
|
42
|
+
* A parse failure, a non-object document, or a document `accept` rejects is
|
|
43
|
+
* copied to `${path}.corrupt-<timestamp>` and thrown, so a later save cannot
|
|
44
|
+
* replace the original.
|
|
45
|
+
*/
|
|
46
|
+
export function readPreservedJsonStore(path, label, empty, accept = () => true) {
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(path, 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === 'ENOENT')
|
|
53
|
+
return empty();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
62
|
+
refuseCorruptStore(path, label, why);
|
|
63
|
+
}
|
|
64
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !accept(parsed)) {
|
|
65
|
+
refuseCorruptStore(path, label);
|
|
66
|
+
}
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
function refuseCorruptStore(path, label, detail) {
|
|
70
|
+
let where = 'existing data was preserved';
|
|
71
|
+
try {
|
|
72
|
+
where = `corrupt file backed up to ${backupCorruptStore(path)}; existing data was preserved`;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
76
|
+
where = `backup failed (${why}); existing data was preserved`;
|
|
77
|
+
}
|
|
78
|
+
const suffix = detail ? ` (${detail})` : '';
|
|
79
|
+
throw new Error(`Invalid ${label} store at ${path}${suffix}; ${where}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Copy `path` to `${path}.corrupt-<timestamp>` without removing the original.
|
|
83
|
+
* A backup with identical bytes is reused, so polling a corrupt store does not
|
|
84
|
+
* add a copy on every read.
|
|
85
|
+
*/
|
|
86
|
+
function backupCorruptStore(path) {
|
|
87
|
+
const existing = existingIdenticalBackup(path);
|
|
88
|
+
if (existing)
|
|
89
|
+
return existing;
|
|
90
|
+
const stamp = Date.now();
|
|
91
|
+
let backup = `${path}.corrupt-${stamp}`;
|
|
92
|
+
let n = 0;
|
|
93
|
+
while (existsSync(backup)) {
|
|
94
|
+
n += 1;
|
|
95
|
+
backup = `${path}.corrupt-${stamp}-${n}`;
|
|
96
|
+
}
|
|
97
|
+
copyFileSync(path, backup);
|
|
98
|
+
try {
|
|
99
|
+
chmodSync(backup, statSync(path).mode & 0o777);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* content is already copied; mode is best-effort */
|
|
103
|
+
}
|
|
104
|
+
return backup;
|
|
105
|
+
}
|
|
106
|
+
function existingIdenticalBackup(path) {
|
|
107
|
+
const dir = dirname(path);
|
|
108
|
+
const prefix = `${basename(path)}.corrupt-`;
|
|
109
|
+
let current;
|
|
110
|
+
try {
|
|
111
|
+
current = readFileSync(path);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = readdirSync(dir).filter((name) => name.startsWith(prefix));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const candidate = join(dir, name);
|
|
125
|
+
try {
|
|
126
|
+
if (statSync(candidate).size === current.length && readFileSync(candidate).equals(current))
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* unreadable candidate: keep looking */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
21
135
|
//# sourceMappingURL=record-store.js.map
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
* Created with ❤️ by ruv.io
|
|
11
11
|
*/
|
|
12
12
|
import type { MCPTool } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Pattern-pack selection for the shared defender (#4785, ADR-517), read once
|
|
15
|
+
* per process from RUFLO_AIDEFENCE_PACKS:
|
|
16
|
+
* unset/empty current default (option omitted)
|
|
17
|
+
* true|false all enabled packs | built-in patterns only
|
|
18
|
+
* a,b,c exactly the named packs
|
|
19
|
+
* Anything else is refused rather than silently falling back.
|
|
20
|
+
*/
|
|
21
|
+
export declare function aidefencePackSelection(raw?: string | undefined): boolean | string[] | undefined;
|
|
13
22
|
/**
|
|
14
23
|
* Export all security tools
|
|
15
24
|
*/
|
|
@@ -33,6 +33,42 @@ const wrapperStats = {
|
|
|
33
33
|
quickScanCalls: 0,
|
|
34
34
|
quickScanThreats: 0,
|
|
35
35
|
};
|
|
36
|
+
const KNOWN_PACKS = new Set([
|
|
37
|
+
'core',
|
|
38
|
+
'tool_invocation',
|
|
39
|
+
'exfil_url',
|
|
40
|
+
'encoded_instruction',
|
|
41
|
+
'slack_markup_forgery',
|
|
42
|
+
'instruction_override_i18n',
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Pattern-pack selection for the shared defender (#4785, ADR-517), read once
|
|
46
|
+
* per process from RUFLO_AIDEFENCE_PACKS:
|
|
47
|
+
* unset/empty current default (option omitted)
|
|
48
|
+
* true|false all enabled packs | built-in patterns only
|
|
49
|
+
* a,b,c exactly the named packs
|
|
50
|
+
* Anything else is refused rather than silently falling back.
|
|
51
|
+
*/
|
|
52
|
+
export function aidefencePackSelection(raw = process.env.RUFLO_AIDEFENCE_PACKS) {
|
|
53
|
+
const value = raw?.trim();
|
|
54
|
+
if (!value)
|
|
55
|
+
return undefined;
|
|
56
|
+
const lower = value.toLowerCase();
|
|
57
|
+
if (lower === 'true')
|
|
58
|
+
return true;
|
|
59
|
+
if (lower === 'false')
|
|
60
|
+
return false;
|
|
61
|
+
const names = value.split(',').map(name => name.trim()).filter(Boolean);
|
|
62
|
+
const unknown = names.filter(name => !KNOWN_PACKS.has(name));
|
|
63
|
+
if (names.length === 0 || unknown.length > 0) {
|
|
64
|
+
throw new Error(`Invalid RUFLO_AIDEFENCE_PACKS=${JSON.stringify(value)}: use true, false, or a comma list of ${[...KNOWN_PACKS].join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
return [...new Set(names)];
|
|
67
|
+
}
|
|
68
|
+
function aidefenceOptions() {
|
|
69
|
+
const packs = aidefencePackSelection();
|
|
70
|
+
return packs === undefined ? { enableLearning: true } : { enableLearning: true, packs };
|
|
71
|
+
}
|
|
36
72
|
/**
|
|
37
73
|
* Get or create AIDefence instance (throws if unavailable)
|
|
38
74
|
*/
|
|
@@ -44,7 +80,7 @@ async function getAIDefence() {
|
|
|
44
80
|
// First attempt - try to load via dynamic import (ESM)
|
|
45
81
|
try {
|
|
46
82
|
const aidefence = await import(packageName);
|
|
47
|
-
const instance = aidefence.createAIDefence(
|
|
83
|
+
const instance = aidefence.createAIDefence(aidefenceOptions());
|
|
48
84
|
if (!instance) {
|
|
49
85
|
throw new Error('createAIDefence returned null');
|
|
50
86
|
}
|
|
@@ -81,7 +117,7 @@ async function getAIDefence() {
|
|
|
81
117
|
// the module cache is stable across the await boundary.
|
|
82
118
|
try {
|
|
83
119
|
const aidefence = await import(packageName);
|
|
84
|
-
const instance = aidefence.createAIDefence(
|
|
120
|
+
const instance = aidefence.createAIDefence(aidefenceOptions());
|
|
85
121
|
if (instance) {
|
|
86
122
|
aidefenceInstance = instance;
|
|
87
123
|
console.error(`[claude-flow] ${packageName} loaded after install (resolver path)`);
|
|
@@ -95,7 +131,7 @@ async function getAIDefence() {
|
|
|
95
131
|
const modulePath = require.resolve(packageName);
|
|
96
132
|
const cacheBust = `?t=${Date.now()}`;
|
|
97
133
|
const aidefence = await import(`file://${modulePath}${cacheBust}`);
|
|
98
|
-
const instance = aidefence.createAIDefence(
|
|
134
|
+
const instance = aidefence.createAIDefence(aidefenceOptions());
|
|
99
135
|
if (!instance) {
|
|
100
136
|
throw new Error('createAIDefence returned null after install');
|
|
101
137
|
}
|
|
@@ -3,11 +3,13 @@
|
|
|
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';
|
|
12
|
+
import { MAX_IMPORT_BYTES, readImportFile } from './import-limits.js';
|
|
11
13
|
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
12
14
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
13
15
|
function isRecord(value) {
|
|
@@ -351,41 +353,6 @@ function isSessionRecordLike(value) {
|
|
|
351
353
|
const v = value;
|
|
352
354
|
return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
|
|
353
355
|
}
|
|
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
356
|
export const sessionTools = [
|
|
390
357
|
{
|
|
391
358
|
name: 'session_save',
|
|
@@ -740,7 +707,7 @@ export const sessionTools = [
|
|
|
740
707
|
let path = null;
|
|
741
708
|
const outputPath = input.outputPath ? String(input.outputPath) : null;
|
|
742
709
|
if (outputPath) {
|
|
743
|
-
const target =
|
|
710
|
+
const target = containedWritePath(outputPath);
|
|
744
711
|
if (!target)
|
|
745
712
|
return { sessionId, error: 'outputPath must be a file inside the project directory (no symlinks)' };
|
|
746
713
|
try {
|
|
@@ -763,7 +730,7 @@ export const sessionTools = [
|
|
|
763
730
|
inputSchema: {
|
|
764
731
|
type: 'object',
|
|
765
732
|
properties: {
|
|
766
|
-
inputPath: { type: 'string', description: 'Path to the session JSON file to import (or pass data)' },
|
|
733
|
+
inputPath: { type: 'string', description: 'Path to the session JSON file to import (or pass data); file and gzip output are each limited to 16 MiB' },
|
|
767
734
|
data: { type: 'object', description: 'The session record itself, as written by session_export (or pass inputPath)' },
|
|
768
735
|
name: { type: 'string', description: 'Override the imported session name' },
|
|
769
736
|
activate: { type: 'boolean', description: 'Restore the imported session into the active stores' },
|
|
@@ -781,8 +748,9 @@ export const sessionTools = [
|
|
|
781
748
|
if (!existsSync(inputPath))
|
|
782
749
|
return { error: `File not found: ${inputPath}` };
|
|
783
750
|
try {
|
|
784
|
-
const bytes =
|
|
785
|
-
const content = bytes[0] === 0x1f && bytes[1] === 0x8b
|
|
751
|
+
const bytes = readImportFile(inputPath);
|
|
752
|
+
const content = bytes[0] === 0x1f && bytes[1] === 0x8b
|
|
753
|
+
? gunzipSync(bytes, { maxOutputLength: MAX_IMPORT_BYTES }) : bytes;
|
|
786
754
|
parsed = JSON.parse(content.toString('utf-8'));
|
|
787
755
|
}
|
|
788
756
|
catch (e) {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Tool definitions for swarm coordination with file-based state persistence.
|
|
5
5
|
* Replaces previous stub implementations with real state tracking.
|
|
6
6
|
*/
|
|
7
|
-
import { closeSync, existsSync, mkdirSync, openSync,
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { nullPrototypeRecords, readPreservedJsonStore } 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';
|
|
@@ -84,14 +85,18 @@ function reconcileOrphanSwarms(store) {
|
|
|
84
85
|
// #2085 — exported so `agent-tools.ts agent_spawn` can push into
|
|
85
86
|
// `swarm.agents` (the field `swarm_status` reads).
|
|
86
87
|
export function loadSwarmStore() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
88
|
+
// #4737: a parse error used to fall through to an empty store, and the next
|
|
89
|
+
// save (or orphan reconcile) overwrote swarm-state.json. Only a missing
|
|
90
|
+
// file is empty; corrupt bytes are copied aside and the error is reported.
|
|
91
|
+
const store = readPreservedJsonStore(getSwarmStatePath(), 'swarm', () => ({ swarms: {}, version: '3.0.0' }), (candidate) => {
|
|
92
|
+
const swarms = candidate.swarms;
|
|
93
|
+
return swarms === undefined || (!!swarms && typeof swarms === 'object' && !Array.isArray(swarms));
|
|
94
|
+
});
|
|
95
|
+
if (!store.swarms)
|
|
96
|
+
store.swarms = {};
|
|
97
|
+
// Record ids come from MCP input. A null prototype stops `__proto__` and
|
|
98
|
+
// `constructor` from resolving onto Object.prototype.
|
|
99
|
+
store.swarms = nullPrototypeRecords(store.swarms);
|
|
95
100
|
// #1799 — reconcile orphans on every load and persist if anything changed.
|
|
96
101
|
// Cheap (process.kill(pid, 0) is sub-millisecond) and means
|
|
97
102
|
// `swarm_status`/`swarm_health` never see ghost "running" entries.
|
|
@@ -365,14 +370,18 @@ export const swarmTools = [
|
|
|
365
370
|
}
|
|
366
371
|
const store = loadSwarmStore();
|
|
367
372
|
const swarmId = input.swarmId;
|
|
368
|
-
if (swarmId && store.swarms[swarmId]) {
|
|
373
|
+
if (swarmId && !store.swarms[swarmId]) {
|
|
374
|
+
return { success: false, error: `Swarm ${swarmId} not found` };
|
|
375
|
+
}
|
|
376
|
+
const { getLiveAgentIds } = await import('./agent-tools.js');
|
|
377
|
+
if (swarmId) {
|
|
369
378
|
const swarm = store.swarms[swarmId];
|
|
370
379
|
return {
|
|
371
380
|
swarmId: swarm.swarmId,
|
|
372
381
|
status: swarm.status,
|
|
373
382
|
topology: swarm.topology,
|
|
374
383
|
maxAgents: swarm.maxAgents,
|
|
375
|
-
agentCount: swarm.agents.length,
|
|
384
|
+
agentCount: getLiveAgentIds(swarm.agents).length,
|
|
376
385
|
taskCount: swarm.tasks.length,
|
|
377
386
|
config: swarm.config,
|
|
378
387
|
apsc: apscStateOf(swarm),
|
|
@@ -397,7 +406,7 @@ export const swarmTools = [
|
|
|
397
406
|
status: latest.status,
|
|
398
407
|
topology: latest.topology,
|
|
399
408
|
maxAgents: latest.maxAgents,
|
|
400
|
-
agentCount: latest.agents.length,
|
|
409
|
+
agentCount: getLiveAgentIds(latest.agents).length,
|
|
401
410
|
taskCount: latest.tasks.length,
|
|
402
411
|
config: latest.config,
|
|
403
412
|
apsc: apscStateOf(latest),
|
|
@@ -429,7 +438,7 @@ export const swarmTools = [
|
|
|
429
438
|
const swarmId = input.swarmId;
|
|
430
439
|
// Find the swarm
|
|
431
440
|
let target;
|
|
432
|
-
if (swarmId
|
|
441
|
+
if (swarmId) {
|
|
433
442
|
target = store.swarms[swarmId];
|
|
434
443
|
}
|
|
435
444
|
else {
|
|
@@ -452,6 +461,19 @@ export const swarmTools = [
|
|
|
452
461
|
error: 'Swarm already terminated',
|
|
453
462
|
};
|
|
454
463
|
}
|
|
464
|
+
const { agentTools, getLiveAgentIds } = await import('./agent-tools.js');
|
|
465
|
+
const terminateAgent = agentTools.find(tool => tool.name === 'agent_terminate');
|
|
466
|
+
let agentsTerminated = 0;
|
|
467
|
+
for (const agentId of getLiveAgentIds(target.agents)) {
|
|
468
|
+
const result = await terminateAgent.handler({
|
|
469
|
+
agentId,
|
|
470
|
+
force: input.graceful === false,
|
|
471
|
+
});
|
|
472
|
+
if (!result.success) {
|
|
473
|
+
return { success: false, swarmId: target.swarmId, agentsTerminated, error: result.error };
|
|
474
|
+
}
|
|
475
|
+
agentsTerminated++;
|
|
476
|
+
}
|
|
455
477
|
target.status = 'terminated';
|
|
456
478
|
target.updatedAt = new Date().toISOString();
|
|
457
479
|
saveSwarmStore(store);
|
|
@@ -460,7 +482,7 @@ export const swarmTools = [
|
|
|
460
482
|
swarmId: target.swarmId,
|
|
461
483
|
terminated: true,
|
|
462
484
|
graceful: input.graceful ?? true,
|
|
463
|
-
agentsTerminated
|
|
485
|
+
agentsTerminated,
|
|
464
486
|
terminatedAt: target.updatedAt,
|
|
465
487
|
};
|
|
466
488
|
},
|
|
@@ -93,6 +93,37 @@ function releaseAgents(agentIds, taskId) {
|
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Stores `task_assign` may bind an agent from.
|
|
98
|
+
*
|
|
99
|
+
* The canonical file is `.claude-flow/agents/store.json`. `hive-mind_spawn`
|
|
100
|
+
* writes its workers to `.claude-flow/agents.json` instead (#1916), so an id
|
|
101
|
+
* that exists in either store is a real agent. A corrupt canonical store
|
|
102
|
+
* throws, matching the read this handler already performed, so a bad file is
|
|
103
|
+
* never replaced. The hive file stays best-effort: a corrupt copy must not
|
|
104
|
+
* hide agents that are present in the canonical store.
|
|
105
|
+
*/
|
|
106
|
+
function assignableAgentStores() {
|
|
107
|
+
const canonical = readRecordStore(join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json'), 'agents', () => ({ agents: {} }));
|
|
108
|
+
const stores = [canonical.agents];
|
|
109
|
+
const hivePath = join(getProjectCwd(), STORAGE_DIR, 'agents.json');
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(hivePath))
|
|
112
|
+
return stores;
|
|
113
|
+
const parsed = JSON.parse(readFileSync(hivePath, 'utf-8'));
|
|
114
|
+
const agents = parsed?.agents;
|
|
115
|
+
if (agents && typeof agents === 'object' && !Array.isArray(agents)) {
|
|
116
|
+
stores.push(agents);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Best-effort, same as updateAgents.
|
|
121
|
+
}
|
|
122
|
+
return stores;
|
|
123
|
+
}
|
|
124
|
+
function agentIsAssignable(id, stores) {
|
|
125
|
+
return stores.some((agents) => Object.prototype.hasOwnProperty.call(agents, id) && Boolean(agents[id]));
|
|
126
|
+
}
|
|
96
127
|
export const taskTools = [
|
|
97
128
|
{
|
|
98
129
|
name: 'task_create',
|
|
@@ -450,10 +481,20 @@ export const taskTools = [
|
|
|
450
481
|
if (!task) {
|
|
451
482
|
return { taskId, error: 'Task not found' };
|
|
452
483
|
}
|
|
484
|
+
// Completed and cancelled tasks are terminal (#4725). Assigning a
|
|
485
|
+
// completed task marks the agent busy, and task_complete then returns
|
|
486
|
+
// without releasing it; task_cancel refuses a finished task too, so
|
|
487
|
+
// nothing ever sets that agent back to idle.
|
|
488
|
+
if (task.status === 'completed' || task.status === 'cancelled') {
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
taskId,
|
|
492
|
+
status: task.status,
|
|
493
|
+
error: `Cannot assign a ${task.status} task`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
453
496
|
const previouslyAssigned = [...task.assignedTo];
|
|
454
|
-
|
|
455
|
-
const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json');
|
|
456
|
-
readRecordStore(agentStorePath, 'agents', () => ({ agents: {} }));
|
|
497
|
+
const agentStores = assignableAgentStores();
|
|
457
498
|
if (input.unassign) {
|
|
458
499
|
// Revert previously assigned agents to idle
|
|
459
500
|
releaseAgents(previouslyAssigned, taskId);
|
|
@@ -461,6 +502,20 @@ export const taskTools = [
|
|
|
461
502
|
}
|
|
462
503
|
else {
|
|
463
504
|
const agentIds = input.agentIds || [];
|
|
505
|
+
// An id in neither store is a ghost owner: the task would go
|
|
506
|
+
// in_progress with nobody to do the work (#4725).
|
|
507
|
+
if (Array.isArray(agentIds)) {
|
|
508
|
+
const missing = agentIds.filter((id) => typeof id === 'string' && !agentIsAssignable(id, agentStores));
|
|
509
|
+
if (missing.length > 0) {
|
|
510
|
+
const listed = missing.join(', ');
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
taskId,
|
|
514
|
+
status: task.status,
|
|
515
|
+
error: missing.length === 1 ? `Agent not found: ${listed}` : `Agents not found: ${listed}`,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
464
519
|
// Revert old agents to idle
|
|
465
520
|
releaseAgents(previouslyAssigned.filter((id) => !agentIds.includes(id)), taskId);
|
|
466
521
|
// Set new agents to active
|
|
@@ -569,21 +624,29 @@ export const taskTools = [
|
|
|
569
624
|
const original = store.tasks[taskId];
|
|
570
625
|
if (!original)
|
|
571
626
|
return { success: false, taskId, error: 'Task not found' };
|
|
627
|
+
const retainState = input.resetState === false;
|
|
572
628
|
const newTaskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
573
|
-
|
|
629
|
+
const retry = {
|
|
574
630
|
taskId: newTaskId,
|
|
575
631
|
type: original.type,
|
|
576
632
|
description: original.description,
|
|
577
633
|
priority: original.priority,
|
|
578
634
|
status: 'pending',
|
|
579
|
-
progress: 0,
|
|
635
|
+
progress: retainState ? original.progress ?? 0 : 0,
|
|
580
636
|
assignedTo: [...original.assignedTo],
|
|
581
637
|
tags: [...original.tags, 'retry-of:' + taskId],
|
|
582
638
|
createdAt: new Date().toISOString(),
|
|
583
639
|
startedAt: null,
|
|
584
640
|
completedAt: null,
|
|
585
641
|
};
|
|
642
|
+
if (retainState && original.result !== undefined)
|
|
643
|
+
retry.result = original.result;
|
|
644
|
+
store.tasks[newTaskId] = retry;
|
|
586
645
|
saveTaskStore(store);
|
|
646
|
+
updateAgents(retry.assignedTo, (agent) => {
|
|
647
|
+
agent.status = 'busy';
|
|
648
|
+
agent.currentTask = newTaskId;
|
|
649
|
+
});
|
|
587
650
|
return {
|
|
588
651
|
taskId,
|
|
589
652
|
newTaskId,
|
|
@@ -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();
|
|
@@ -139,6 +144,15 @@ export const terminalTools = [
|
|
|
139
144
|
}
|
|
140
145
|
// Find or create default session
|
|
141
146
|
let session = sessionId ? store.sessions[sessionId] : Object.values(store.sessions).find(s => s.status === 'active');
|
|
147
|
+
// #4730: terminal_close only flips status to "closed". Executing anyway
|
|
148
|
+
// ran the command and then wrote status back to "active", so a closed
|
|
149
|
+
// session kept accepting commands. Reject anything that is not active
|
|
150
|
+
// before the process is spawned and before that status write. When no
|
|
151
|
+
// sessionId was given, the lookup above already selected an active
|
|
152
|
+
// session (or we create one below).
|
|
153
|
+
if (session && session.status !== 'active') {
|
|
154
|
+
return { success: false, error: `Session is not active (${session.status})` };
|
|
155
|
+
}
|
|
142
156
|
if (!session) {
|
|
143
157
|
// Create default session
|
|
144
158
|
const id = `term-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
@@ -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
|
|
@@ -8,6 +8,7 @@ import { join } from 'node:path';
|
|
|
8
8
|
import { getProjectCwd } from './types.js';
|
|
9
9
|
import { validateIdentifier, validatePath, validateText } from './validate-input.js';
|
|
10
10
|
import { executeAgentTask } from './agent-execute-core.js';
|
|
11
|
+
import { readPreservedJsonStore } from './record-store.js';
|
|
11
12
|
// Storage paths
|
|
12
13
|
const STORAGE_DIR = '.claude-flow';
|
|
13
14
|
const WORKFLOW_DIR = 'workflows';
|
|
@@ -73,19 +74,19 @@ function withRecordMaps(store, keys) {
|
|
|
73
74
|
return store;
|
|
74
75
|
}
|
|
75
76
|
const WORKFLOW_MAPS = ['workflows', 'templates'];
|
|
77
|
+
function isRecordMap(value) {
|
|
78
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
}
|
|
76
80
|
function loadWorkflowStore() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Return default store on error
|
|
87
|
-
}
|
|
88
|
-
return withRecordMaps({ workflows: {}, templates: {}, version: '3.0.0' }, WORKFLOW_MAPS);
|
|
81
|
+
// #4737: a parse error used to return an empty store, and the next save
|
|
82
|
+
// overwrote the file. Only a missing file is empty; corrupt bytes are
|
|
83
|
+
// copied aside and the error is reported.
|
|
84
|
+
const path = getWorkflowPath();
|
|
85
|
+
const data = readPreservedJsonStore(path, 'workflow', () => ({ workflows: {}, templates: {}, version: '3.0.0' }), (store) => WORKFLOW_MAPS.every((key) => {
|
|
86
|
+
const map = store[key];
|
|
87
|
+
return map === undefined || isRecordMap(map);
|
|
88
|
+
}));
|
|
89
|
+
return withRecordMaps(data, WORKFLOW_MAPS);
|
|
89
90
|
}
|
|
90
91
|
function saveWorkflowStore(store) {
|
|
91
92
|
ensureWorkflowDir();
|