@claude-flow/cli 3.71.4 → 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/helpers.manifest.json +4 -4
- package/.claude/helpers/hook-handler.cjs +23 -10
- 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/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/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 +59 -8
- package/dist/src/mcp-tools/browser-argv.d.ts +9 -0
- package/dist/src/mcp-tools/browser-argv.js +33 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.js +12 -2
- 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 +21 -42
- package/dist/src/mcp-tools/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +8 -0
- package/dist/src/mcp-tools/record-store.js +98 -1
- 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 +5 -3
- package/dist/src/mcp-tools/swarm-tools.js +35 -17
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +9 -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
|
@@ -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
|
}
|
|
@@ -9,6 +9,7 @@ import { gunzipSync } from 'node:zlib';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { getProjectCwd } from './types.js';
|
|
11
11
|
import { containedWritePath } from './path-containment.js';
|
|
12
|
+
import { MAX_IMPORT_BYTES, readImportFile } from './import-limits.js';
|
|
12
13
|
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
13
14
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
14
15
|
function isRecord(value) {
|
|
@@ -729,7 +730,7 @@ export const sessionTools = [
|
|
|
729
730
|
inputSchema: {
|
|
730
731
|
type: 'object',
|
|
731
732
|
properties: {
|
|
732
|
-
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' },
|
|
733
734
|
data: { type: 'object', description: 'The session record itself, as written by session_export (or pass inputPath)' },
|
|
734
735
|
name: { type: 'string', description: 'Override the imported session name' },
|
|
735
736
|
activate: { type: 'boolean', description: 'Restore the imported session into the active stores' },
|
|
@@ -747,8 +748,9 @@ export const sessionTools = [
|
|
|
747
748
|
if (!existsSync(inputPath))
|
|
748
749
|
return { error: `File not found: ${inputPath}` };
|
|
749
750
|
try {
|
|
750
|
-
const bytes =
|
|
751
|
-
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;
|
|
752
754
|
parsed = JSON.parse(content.toString('utf-8'));
|
|
753
755
|
}
|
|
754
756
|
catch (e) {
|
|
@@ -4,9 +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 } from './record-store.js';
|
|
9
|
+
import { nullPrototypeRecords, readPreservedJsonStore } from './record-store.js';
|
|
10
10
|
import { getProjectCwd } from './types.js';
|
|
11
11
|
import { validateIdentifier } from './validate-input.js';
|
|
12
12
|
import { createApscState, isApscAgentEligible, normalizeApscConfig, recordApscSignal, } from '../services/pheromone-adaptive.js';
|
|
@@ -85,16 +85,17 @@ function reconcileOrphanSwarms(store) {
|
|
|
85
85
|
// #2085 — exported so `agent-tools.ts agent_spawn` can push into
|
|
86
86
|
// `swarm.agents` (the field `swarm_status` reads).
|
|
87
87
|
export function loadSwarmStore() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
98
99
|
store.swarms = nullPrototypeRecords(store.swarms);
|
|
99
100
|
// #1799 — reconcile orphans on every load and persist if anything changed.
|
|
100
101
|
// Cheap (process.kill(pid, 0) is sub-millisecond) and means
|
|
@@ -369,14 +370,18 @@ export const swarmTools = [
|
|
|
369
370
|
}
|
|
370
371
|
const store = loadSwarmStore();
|
|
371
372
|
const swarmId = input.swarmId;
|
|
372
|
-
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) {
|
|
373
378
|
const swarm = store.swarms[swarmId];
|
|
374
379
|
return {
|
|
375
380
|
swarmId: swarm.swarmId,
|
|
376
381
|
status: swarm.status,
|
|
377
382
|
topology: swarm.topology,
|
|
378
383
|
maxAgents: swarm.maxAgents,
|
|
379
|
-
agentCount: swarm.agents.length,
|
|
384
|
+
agentCount: getLiveAgentIds(swarm.agents).length,
|
|
380
385
|
taskCount: swarm.tasks.length,
|
|
381
386
|
config: swarm.config,
|
|
382
387
|
apsc: apscStateOf(swarm),
|
|
@@ -401,7 +406,7 @@ export const swarmTools = [
|
|
|
401
406
|
status: latest.status,
|
|
402
407
|
topology: latest.topology,
|
|
403
408
|
maxAgents: latest.maxAgents,
|
|
404
|
-
agentCount: latest.agents.length,
|
|
409
|
+
agentCount: getLiveAgentIds(latest.agents).length,
|
|
405
410
|
taskCount: latest.tasks.length,
|
|
406
411
|
config: latest.config,
|
|
407
412
|
apsc: apscStateOf(latest),
|
|
@@ -433,7 +438,7 @@ export const swarmTools = [
|
|
|
433
438
|
const swarmId = input.swarmId;
|
|
434
439
|
// Find the swarm
|
|
435
440
|
let target;
|
|
436
|
-
if (swarmId
|
|
441
|
+
if (swarmId) {
|
|
437
442
|
target = store.swarms[swarmId];
|
|
438
443
|
}
|
|
439
444
|
else {
|
|
@@ -456,6 +461,19 @@ export const swarmTools = [
|
|
|
456
461
|
error: 'Swarm already terminated',
|
|
457
462
|
};
|
|
458
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
|
+
}
|
|
459
477
|
target.status = 'terminated';
|
|
460
478
|
target.updatedAt = new Date().toISOString();
|
|
461
479
|
saveSwarmStore(store);
|
|
@@ -464,7 +482,7 @@ export const swarmTools = [
|
|
|
464
482
|
swarmId: target.swarmId,
|
|
465
483
|
terminated: true,
|
|
466
484
|
graceful: input.graceful ?? true,
|
|
467
|
-
agentsTerminated
|
|
485
|
+
agentsTerminated,
|
|
468
486
|
terminatedAt: target.updatedAt,
|
|
469
487
|
};
|
|
470
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,
|
|
@@ -144,6 +144,15 @@ export const terminalTools = [
|
|
|
144
144
|
}
|
|
145
145
|
// Find or create default session
|
|
146
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
|
+
}
|
|
147
156
|
if (!session) {
|
|
148
157
|
// Create default session
|
|
149
158
|
const id = `term-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
@@ -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();
|
|
@@ -1,14 +1,53 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join, dirname } from 'node:path';
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
import { join, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
5
|
+
// Host configuration precedes defaults. Caller key overrides stay under ~/.ruflo;
|
|
6
|
+
// a caller relayWs must match the configured relay. relayHttp keeps ADR-125 precedence.
|
|
7
7
|
const HTTP_BASE = (o) => (o || process.env.RUFLO_X_RELAY_HTTP || 'https://relay.ruv.io').replace(/\/$/, '');
|
|
8
8
|
const RELAY_WS = (o) => o || process.env.RUFLO_X_RELAY_WS || 'wss://relay.ruv.io';
|
|
9
9
|
const KEY_FILE = () => process.env.RUFLO_NOSTR_KEY_FILE || join(homedir(), '.ruflo', 'nostr.key');
|
|
10
10
|
const hex = (b) => Buffer.from(b).toString('hex');
|
|
11
11
|
const unhex = (h) => Uint8Array.from(Buffer.from(h, 'hex'));
|
|
12
|
+
function callerKeyFile(value) {
|
|
13
|
+
// Host configuration is trusted; an MCP argument cannot select a different host file.
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return KEY_FILE();
|
|
16
|
+
if (typeof value !== 'string' || !value)
|
|
17
|
+
throw new Error('keyFile must name a file under ~/.ruflo');
|
|
18
|
+
const root = resolve(homedir(), '.ruflo');
|
|
19
|
+
const target = resolve(value);
|
|
20
|
+
const rel = relative(root, target);
|
|
21
|
+
if (!rel || rel === '..' || rel.startsWith('../') || rel.startsWith('..\\') || isAbsolute(rel)) {
|
|
22
|
+
throw new Error('keyFile must name a file under ~/.ruflo');
|
|
23
|
+
}
|
|
24
|
+
for (let current = target;; current = dirname(current)) {
|
|
25
|
+
try {
|
|
26
|
+
if (lstatSync(current).isSymbolicLink())
|
|
27
|
+
throw new Error('keyFile must not follow symlinks');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'ENOENT')
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (relative(root, current) === '')
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
function configuredRelay(value) {
|
|
39
|
+
const configured = new URL(RELAY_WS());
|
|
40
|
+
if (value !== undefined && typeof value !== 'string')
|
|
41
|
+
throw new Error('relayWs must be the configured relay URL');
|
|
42
|
+
const requested = new URL(value === undefined ? configured.href : value);
|
|
43
|
+
const localTestRelay = Boolean(process.env.RUFLO_X_RELAY_WS)
|
|
44
|
+
&& configured.protocol === 'ws:' && ['localhost', '127.0.0.1', '[::1]'].includes(configured.hostname);
|
|
45
|
+
if ((!localTestRelay && configured.protocol !== 'wss:') || requested.href !== configured.href
|
|
46
|
+
|| requested.username || requested.password || requested.hash) {
|
|
47
|
+
throw new Error('relayWs must use wss and match the configured relay (explicit loopback ws configuration is allowed)');
|
|
48
|
+
}
|
|
49
|
+
return requested.href;
|
|
50
|
+
}
|
|
12
51
|
async function loadNostrTools() {
|
|
13
52
|
try {
|
|
14
53
|
return (await import('nostr-tools/pure'));
|
|
@@ -24,7 +63,7 @@ export function loadOrCreateKey(nt, file = KEY_FILE()) {
|
|
|
24
63
|
}
|
|
25
64
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
26
65
|
const sk = nt.generateSecretKey();
|
|
27
|
-
writeFileSync(file, hex(sk), { mode: 0o600 });
|
|
66
|
+
writeFileSync(file, hex(sk), { mode: 0o600, flag: 'wx' });
|
|
28
67
|
return { sk, pubkey: nt.getPublicKey(sk), created: true };
|
|
29
68
|
}
|
|
30
69
|
export function nip98Header(nt, sk, url, method, body) {
|
|
@@ -80,24 +119,26 @@ export const xFederationJoinTools = [{
|
|
|
80
119
|
code: { type: 'string', description: 'Optional private invite code (v2.…), for invite-only relays.' },
|
|
81
120
|
gatewayUrl: { type: 'string', description: 'Public registration gateway origin; defaults to https://x.ruv.io.' },
|
|
82
121
|
relayHttp: { type: 'string', description: 'Relay HTTPS base for the claim; takes precedence over RUFLO_X_RELAY_HTTP.' },
|
|
83
|
-
relayWs: { type: 'string', description: 'Relay wss URL for NIP-42;
|
|
84
|
-
keyFile: { type: 'string', description: '
|
|
122
|
+
relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; must match RUFLO_X_RELAY_WS or the default relay.' },
|
|
123
|
+
keyFile: { type: 'string', description: 'Caller-selected file under ~/.ruflo (no symlinks). Omit to use host RUFLO_NOSTR_KEY_FILE or ~/.ruflo/nostr.key.' }
|
|
85
124
|
}, required: [] },
|
|
86
125
|
handler: async (input) => {
|
|
87
126
|
const i = input;
|
|
88
|
-
const nt = await loadNostrTools();
|
|
89
127
|
// Validate input before the optional-dependency check so a bad code fails fast and identically
|
|
90
128
|
// whether or not nostr-tools is present.
|
|
91
129
|
if (i.code !== undefined && !/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
|
|
92
130
|
throw new Error('invite code must look like v2.<token>');
|
|
131
|
+
const registrationBase = i.code ? undefined : secureBase(i.gatewayUrl || process.env.RUFLO_X_GATEWAY_URL || 'https://x.ruv.io');
|
|
132
|
+
const keyFile = callerKeyFile(i.keyFile);
|
|
133
|
+
const relayWs = configuredRelay(i.relayWs);
|
|
134
|
+
const nt = await loadNostrTools();
|
|
93
135
|
if (!nt)
|
|
94
136
|
return { degraded: true, reason: 'nostr-tools not installed', hint: 'npm i -g nostr-tools (secp256k1 signing is not in node:crypto)' };
|
|
95
|
-
const
|
|
96
|
-
const { sk, pubkey, created } = loadOrCreateKey(nt, i.keyFile);
|
|
137
|
+
const { sk, pubkey, created } = loadOrCreateKey(nt, keyFile);
|
|
97
138
|
// Existing members need no new grant. In particular never downgrade/re-admit
|
|
98
139
|
// their key just because they run join again.
|
|
99
140
|
if (!i.code) {
|
|
100
|
-
const existing = await verifyMembership(nt, sk,
|
|
141
|
+
const existing = await verifyMembership(nt, sk, relayWs);
|
|
101
142
|
if (existing.ok)
|
|
102
143
|
return { ok: true, pubkey, keyCreated: created, membershipVerified: true, alreadyMember: true };
|
|
103
144
|
}
|
|
@@ -108,7 +149,7 @@ export const xFederationJoinTools = [{
|
|
|
108
149
|
r = await fetch(url, { method: 'POST', redirect: 'error', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });
|
|
109
150
|
}
|
|
110
151
|
catch {
|
|
111
|
-
const check = await verifyMembership(nt, sk,
|
|
152
|
+
const check = await verifyMembership(nt, sk, relayWs);
|
|
112
153
|
if (check.ok)
|
|
113
154
|
return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
|
|
114
155
|
throw new Error('Admission response unavailable and relay membership unverified. Keep your local key and retry; do not create another identity.');
|
|
@@ -117,12 +158,12 @@ export const xFederationJoinTools = [{
|
|
|
117
158
|
if (!r.ok) {
|
|
118
159
|
// A lost acknowledgment can follow a successful grant. Check with the
|
|
119
160
|
// relay before declaring failure; never silently generate a second key.
|
|
120
|
-
const check = await verifyMembership(nt, sk,
|
|
161
|
+
const check = await verifyMembership(nt, sk, relayWs);
|
|
121
162
|
if (check.ok)
|
|
122
163
|
return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
|
|
123
164
|
throw new Error(`join rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);
|
|
124
165
|
}
|
|
125
|
-
const auth = await verifyMembership(nt, sk,
|
|
166
|
+
const auth = await verifyMembership(nt, sk, relayWs);
|
|
126
167
|
return { ok: auth.ok, pubkey, keyCreated: created, role: claim.role ?? 'member', membershipVerified: auth.ok, ...(auth.ok ? {} : { reason: auth.reason }),
|
|
127
168
|
next: 'Publish kind-1 events tagged ["t","ruflo-swarm"] — or run `ruflo federation sync` to read the swarm.' };
|
|
128
169
|
},
|
|
@@ -152,6 +152,12 @@ declare class LocalReasoningBank {
|
|
|
152
152
|
private persistenceEnabled;
|
|
153
153
|
private dirty;
|
|
154
154
|
private saveTimeout;
|
|
155
|
+
/** IDs changed locally since the last successful flush. */
|
|
156
|
+
private dirtyIds;
|
|
157
|
+
/** IDs deleted locally; a disk copy must not be resurrected. */
|
|
158
|
+
private deletedIds;
|
|
159
|
+
/** A local clear() must remove disk entries too. */
|
|
160
|
+
private clearRequested;
|
|
155
161
|
constructor(options: {
|
|
156
162
|
maxSize: number;
|
|
157
163
|
persistence?: boolean;
|
|
@@ -168,6 +174,11 @@ declare class LocalReasoningBank {
|
|
|
168
174
|
private saveToDisk;
|
|
169
175
|
/**
|
|
170
176
|
* Immediately flush patterns to disk
|
|
177
|
+
*
|
|
178
|
+
* Re-reads the file under the cooperating-writer lock and merges only the
|
|
179
|
+
* patterns this process changed, so a long-lived daemon cannot erase patterns
|
|
180
|
+
* written by another hook/process (#4735). Deletions and clear() are carried
|
|
181
|
+
* as tombstones; the write itself is temp-file + rename.
|
|
171
182
|
*/
|
|
172
183
|
flushToDisk(): void;
|
|
173
184
|
/**
|