claude-flow 3.47.1 → 3.49.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/.proven-config-version +1 -0
- package/.claude/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/hook-handler.cjs +7 -4
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/memory.cjs +1 -1
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/.claude/helpers/router.cjs +1 -1
- package/.claude/helpers/session.cjs +1 -1
- package/.claude/proven-config.json +42 -0
- package/.claude-plugin/marketplace.json +16 -1
- package/README.md +1 -53
- package/README.zh-CN.md +1 -53
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +6 -3
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +1 -53
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
- package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/index.js +10 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
- package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
- package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
- package/v3/@claude-flow/cli/package.json +9 -9
- package/v3/@claude-flow/guidance/package.json +5 -6
- package/v3/@claude-flow/shared/package.json +6 -3
|
@@ -18,6 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
|
|
|
18
18
|
import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
|
|
19
19
|
import { generateClaudeMd } from './claudemd-generator.js';
|
|
20
20
|
import { recordMemoryPackagePath } from './memory-package-resolver.js';
|
|
21
|
+
import { ensureCommonJsCompanions } from './helper-companions.js';
|
|
21
22
|
import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
|
|
22
23
|
/**
|
|
23
24
|
* Skills to copy based on configuration
|
|
@@ -499,7 +500,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
499
500
|
const sourceHelpersForUpgrade = findSourceHelpersDir();
|
|
500
501
|
if (sourceHelpersForUpgrade) {
|
|
501
502
|
// Keep in sync with helper-refresh.ts:CRITICAL_HELPERS.
|
|
502
|
-
const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.
|
|
503
|
+
const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.cjs'];
|
|
503
504
|
for (const helperName of criticalHelpers) {
|
|
504
505
|
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
|
|
505
506
|
const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
|
|
@@ -524,7 +525,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
524
525
|
'hook-handler.cjs': generateHookHandler(),
|
|
525
526
|
'intelligence.cjs': generateIntelligenceStub(),
|
|
526
527
|
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
527
|
-
'router.
|
|
528
|
+
'router.cjs': generateAgentRouter(), // ADR-389 / #3555
|
|
528
529
|
};
|
|
529
530
|
for (const [helperName, content] of Object.entries(generatedCritical)) {
|
|
530
531
|
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
|
|
@@ -541,6 +542,11 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
541
542
|
catch { }
|
|
542
543
|
}
|
|
543
544
|
}
|
|
545
|
+
// #3555: the refreshed hook-handler requires the .cjs companions; older
|
|
546
|
+
// installs only have session.js / memory.js, which can't load in ESM.
|
|
547
|
+
for (const name of await ensureCommonJsCompanions(path.join(targetDir, '.claude', 'helpers'))) {
|
|
548
|
+
result.created.push(`.claude/helpers/${name}`);
|
|
549
|
+
}
|
|
544
550
|
// Stamp the installed version so the startup auto-refresh treats these as
|
|
545
551
|
// current (no redundant re-copy on the next command).
|
|
546
552
|
try {
|
|
@@ -1295,9 +1301,9 @@ async function writeHelpers(targetDir, options, result) {
|
|
|
1295
1301
|
const helpers = {
|
|
1296
1302
|
'pre-commit': generatePreCommitHook(),
|
|
1297
1303
|
'post-commit': generatePostCommitHook(),
|
|
1298
|
-
'session.
|
|
1299
|
-
'router.
|
|
1300
|
-
'memory.
|
|
1304
|
+
'session.cjs': generateSessionManager(),
|
|
1305
|
+
'router.cjs': generateAgentRouter(),
|
|
1306
|
+
'memory.cjs': generateMemoryHelper(),
|
|
1301
1307
|
'hook-handler.cjs': generateHookHandler(),
|
|
1302
1308
|
'intelligence.cjs': generateIntelligenceStub(),
|
|
1303
1309
|
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonJS companion helpers that `hook-handler.cjs` requires but that are not
|
|
3
|
+
* in the signed critical set (#3555).
|
|
4
|
+
*
|
|
5
|
+
* They are CommonJS, so they ship as `.cjs`: a `.js` copy cannot load in a
|
|
6
|
+
* `"type":"module"` project. Installs made before the rename have only the
|
|
7
|
+
* `.js` copies, which the refreshed hook-handler no longer requires, so the
|
|
8
|
+
* refresh writes any missing `.cjs` companion from the CLI's own compiled
|
|
9
|
+
* generators (the trust root; no external file to verify). Stale `.js` copies
|
|
10
|
+
* are left in place and ignored. Existing `.cjs` files are never overwritten.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
export const COMMONJS_COMPANION_HELPERS = ['session.cjs', 'memory.cjs'];
|
|
15
|
+
export async function ensureCommonJsCompanions(helpersDir) {
|
|
16
|
+
const missing = COMMONJS_COMPANION_HELPERS.filter((name) => !fs.existsSync(path.join(helpersDir, name)));
|
|
17
|
+
if (missing.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const gen = await import('./helpers-generator.js');
|
|
20
|
+
const content = {
|
|
21
|
+
'session.cjs': gen.generateSessionManager,
|
|
22
|
+
'memory.cjs': gen.generateMemoryHelper,
|
|
23
|
+
};
|
|
24
|
+
const written = [];
|
|
25
|
+
for (const name of missing) {
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(path.join(helpersDir, name), content[name](), { encoding: 'utf-8', mode: 0o755 });
|
|
28
|
+
written.push(name);
|
|
29
|
+
}
|
|
30
|
+
catch { /* best-effort: hook-handler degrades gracefully without it */ }
|
|
31
|
+
}
|
|
32
|
+
return written;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=helper-companions.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface IntegrityResult {
|
|
2
|
+
/** Critical helper names whose on-disk hash doesn't match the signed manifest
|
|
3
|
+
* (or that are missing from disk despite being part of the signed set). */
|
|
4
|
+
tampered: string[];
|
|
5
|
+
/** Set only when the package's OWN signed manifest can't be verified at all —
|
|
6
|
+
* there's no ground truth to check the installed files against, distinct
|
|
7
|
+
* from a clean `tampered: []` result. */
|
|
8
|
+
blocked?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
|
|
12
|
+
* `sourceDir`'s signed manifest. Read-only — safe to call without holding any
|
|
13
|
+
* refresh lock; callers only need to acquire one if `tampered` comes back
|
|
14
|
+
* non-empty and they intend to repair it.
|
|
15
|
+
*
|
|
16
|
+
* `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
|
|
17
|
+
* — fails open rather than flagging every install as tampered when there is
|
|
18
|
+
* no ground truth at all to compare against.
|
|
19
|
+
*/
|
|
20
|
+
export declare function verifyInstalledCriticalHelpers(helpersDir: string, sourceDir: string | null, criticalHelpers: readonly string[], pubkeyPemOverride?: string): IntegrityResult;
|
|
21
|
+
//# sourceMappingURL=helper-integrity.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-install integrity re-verification for signed critical helpers (#3565).
|
|
3
|
+
*
|
|
4
|
+
* `helper-refresh.ts` verifies a helper's signature/hash once, at copy time,
|
|
5
|
+
* then stamps the target directory with the installed CLI version. Every
|
|
6
|
+
* later command short-circuited on "stamp already matches" with NO further
|
|
7
|
+
* verification — so a critical helper modified on disk after that point (a
|
|
8
|
+
* sibling package's postinstall, a stray older CLI process racing a write,
|
|
9
|
+
* direct tampering) stayed silently modified through any number of
|
|
10
|
+
* subsequent commands, forever. This module re-hashes the already-INSTALLED
|
|
11
|
+
* helpers against the signed manifest independent of the stamp, so callers
|
|
12
|
+
* can re-run it on every invocation rather than trusting a single past check.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately has no opinion on WHAT to do about tampering (re-copy, warn,
|
|
15
|
+
* refuse) — that policy lives in helper-refresh.ts, which already owns the
|
|
16
|
+
* fail-closed verify-then-copy path this result feeds into to heal.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
|
|
21
|
+
/**
|
|
22
|
+
* Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
|
|
23
|
+
* `sourceDir`'s signed manifest. Read-only — safe to call without holding any
|
|
24
|
+
* refresh lock; callers only need to acquire one if `tampered` comes back
|
|
25
|
+
* non-empty and they intend to repair it.
|
|
26
|
+
*
|
|
27
|
+
* `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
|
|
28
|
+
* — fails open rather than flagging every install as tampered when there is
|
|
29
|
+
* no ground truth at all to compare against.
|
|
30
|
+
*/
|
|
31
|
+
export function verifyInstalledCriticalHelpers(helpersDir, sourceDir, criticalHelpers, pubkeyPemOverride) {
|
|
32
|
+
if (!sourceDir)
|
|
33
|
+
return { tampered: [] };
|
|
34
|
+
let trusted = null;
|
|
35
|
+
try {
|
|
36
|
+
trusted = verifyHelpersManifest(fs.readFileSync(path.join(sourceDir, HELPERS_MANIFEST_FILE), 'utf-8'), pubkeyPemOverride);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
trusted = null;
|
|
40
|
+
}
|
|
41
|
+
if (!trusted)
|
|
42
|
+
return { tampered: [], blocked: 'signed helpers manifest missing or signature invalid' };
|
|
43
|
+
const tampered = [];
|
|
44
|
+
for (const name of criticalHelpers) {
|
|
45
|
+
const expected = trusted.files[name];
|
|
46
|
+
// Not part of THIS version's signed set (or source doesn't ship it) —
|
|
47
|
+
// nothing to compare the installed copy against.
|
|
48
|
+
if (!expected || !fs.existsSync(path.join(sourceDir, name)))
|
|
49
|
+
continue;
|
|
50
|
+
let actual = null;
|
|
51
|
+
try {
|
|
52
|
+
actual = sha256Hex(fs.readFileSync(path.join(helpersDir, name)));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
actual = null;
|
|
56
|
+
}
|
|
57
|
+
if (actual !== expected)
|
|
58
|
+
tampered.push(name);
|
|
59
|
+
}
|
|
60
|
+
return { tampered };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=helper-integrity.js.map
|
|
@@ -10,6 +10,20 @@ interface RefreshOptions {
|
|
|
10
10
|
lockRetryMsOverride?: number;
|
|
11
11
|
malformedLockStaleMsOverride?: number;
|
|
12
12
|
}
|
|
13
|
+
interface RefreshResult {
|
|
14
|
+
refreshed: boolean;
|
|
15
|
+
from?: string;
|
|
16
|
+
to?: string;
|
|
17
|
+
blocked?: string;
|
|
18
|
+
/** Set when `refreshed` was caused by #3565 healing, not a version bump —
|
|
19
|
+
* the stamp already matched but one or more critical helpers failed
|
|
20
|
+
* on-disk integrity verification and were re-copied from the verified
|
|
21
|
+
* source. */
|
|
22
|
+
healed?: boolean;
|
|
23
|
+
/** Critical helper names that failed integrity verification, present only
|
|
24
|
+
* alongside `healed: true`. */
|
|
25
|
+
tampered?: string[];
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* ruflo-owned helpers that carry hook logic (or the render surface for the
|
|
15
29
|
* funnel disclosure row) and must track the package version. Adding to this
|
|
@@ -19,6 +33,8 @@ interface RefreshOptions {
|
|
|
19
33
|
export declare const CRITICAL_HELPERS: string[];
|
|
20
34
|
/** Installed @claude-flow/cli version — the value the helpers are stamped with. */
|
|
21
35
|
export declare function getInstalledCliVersion(): string;
|
|
36
|
+
/** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
|
|
37
|
+
export declare function findPackageHelpersDir(): string | null;
|
|
22
38
|
/**
|
|
23
39
|
* On CLI startup, refresh critical helpers if their stamp is older than the
|
|
24
40
|
* installed CLI version. Two passes:
|
|
@@ -70,17 +86,8 @@ export declare function getInstalledCliVersion(): string;
|
|
|
70
86
|
* compat with pre-3.31.3 callers). If the global pass ran, its own result is
|
|
71
87
|
* carried in the optional `global` field.
|
|
72
88
|
*/
|
|
73
|
-
export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<{
|
|
74
|
-
|
|
75
|
-
from?: string;
|
|
76
|
-
to?: string;
|
|
77
|
-
blocked?: string;
|
|
78
|
-
global?: {
|
|
79
|
-
refreshed: boolean;
|
|
80
|
-
from?: string;
|
|
81
|
-
to?: string;
|
|
82
|
-
blocked?: string;
|
|
83
|
-
};
|
|
89
|
+
export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<RefreshResult & {
|
|
90
|
+
global?: RefreshResult;
|
|
84
91
|
}>;
|
|
85
92
|
export {};
|
|
86
93
|
//# sourceMappingURL=helper-refresh.d.ts.map
|
|
@@ -20,6 +20,8 @@ import { fileURLToPath } from 'url';
|
|
|
20
20
|
import { createRequire } from 'module';
|
|
21
21
|
import * as semver from 'semver';
|
|
22
22
|
import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
|
|
23
|
+
import { ensureCommonJsCompanions } from './helper-companions.js';
|
|
24
|
+
import { verifyInstalledCriticalHelpers } from './helper-integrity.js';
|
|
23
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
/**
|
|
25
27
|
* Walk up from `startDir` to the nearest ancestor whose `package.json` names
|
|
@@ -71,10 +73,11 @@ export const CRITICAL_HELPERS = [
|
|
|
71
73
|
// statusline.cjs is here so the funnel disclosure row (ADR-301) reaches
|
|
72
74
|
// existing installs on the next `ruflo` command, not only fresh `ruflo init`.
|
|
73
75
|
'statusline.cjs',
|
|
74
|
-
// router.
|
|
76
|
+
// router.cjs is loaded by hook-handler.cjs to label each prompt with an agent.
|
|
75
77
|
// Without it here, installs kept the pre-#2257 substring router forever
|
|
76
|
-
// ("latest" -> tester). ADR-389 / #3401.
|
|
77
|
-
|
|
78
|
+
// ("latest" -> tester). ADR-389 / #3401. It is CommonJS, so it ships as
|
|
79
|
+
// `.cjs`: a `.js` copy cannot load in a `"type":"module"` project (#3555).
|
|
80
|
+
'router.cjs',
|
|
78
81
|
];
|
|
79
82
|
function errorCode(error) {
|
|
80
83
|
return typeof error === 'object' && error !== null && 'code' in error
|
|
@@ -230,7 +233,7 @@ export function getInstalledCliVersion() {
|
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
/** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
|
|
233
|
-
function findPackageHelpersDir() {
|
|
236
|
+
export function findPackageHelpersDir() {
|
|
234
237
|
const candidates = [];
|
|
235
238
|
try {
|
|
236
239
|
const esmRequire = createRequire(import.meta.url);
|
|
@@ -313,7 +316,7 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
313
316
|
'hook-handler.cjs': gen.generateHookHandler(),
|
|
314
317
|
'intelligence.cjs': gen.generateIntelligenceStub(),
|
|
315
318
|
'auto-memory-hook.mjs': gen.generateAutoMemoryHook(),
|
|
316
|
-
'router.
|
|
319
|
+
'router.cjs': gen.generateAgentRouter(), // ADR-389 / #3555
|
|
317
320
|
// Fallback needs the same generator inputs `ruflo init` uses. We match the
|
|
318
321
|
// hardcoded default (maxAgents 15) because the fallback fires when the
|
|
319
322
|
// installed package is unresolvable — no way to read the user's project
|
|
@@ -339,9 +342,12 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
339
342
|
}
|
|
340
343
|
/**
|
|
341
344
|
* On CLI startup: if an initialized project's critical helpers are stamped older
|
|
342
|
-
* than the installed CLI version, silently re-copy them. Fast path is a
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
+
* than the installed CLI version, silently re-copy them. Fast path is a stamp
|
|
346
|
+
* read + string compare, PLUS a re-hash of each installed critical helper
|
|
347
|
+
* against the signed manifest (#3565) — still sub-ms for the handful of small
|
|
348
|
+
* files involved, and lock-free unless a mismatch is found. The copy runs at
|
|
349
|
+
* most once per version bump; the heal-on-tamper path can run on any call.
|
|
350
|
+
* Best-effort, never throws. No-op outside a ruflo project (requires an
|
|
345
351
|
* existing hook-handler.cjs — never creates files in an unrelated directory).
|
|
346
352
|
*
|
|
347
353
|
* FORWARD-ONLY (never downgrades): refreshing on any mere INEQUALITY, rather
|
|
@@ -365,6 +371,9 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
365
371
|
* `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
|
|
366
372
|
* signed fixture and get real, deterministic coverage of the verify → hash →
|
|
367
373
|
* copy logic without depending on that.
|
|
374
|
+
*
|
|
375
|
+
* #3565: also re-verifies on every stamp-match call (see helper-integrity.ts)
|
|
376
|
+
* and heals a mismatch via this same verify-then-copy path.
|
|
368
377
|
*/
|
|
369
378
|
async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
370
379
|
if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
|
|
@@ -377,7 +386,9 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
377
386
|
// on this repo (CLAUDE.md "Concurrent-session helper corruption"). The
|
|
378
387
|
// existing semver.gte guard below still fires for normal installs — this
|
|
379
388
|
// is the escape hatch for the small set of users editing helpers directly.
|
|
380
|
-
// Applies to whichever dir this call is refreshing (project or global)
|
|
389
|
+
// Applies to whichever dir this call is refreshing (project or global),
|
|
390
|
+
// and also exempts it from the #3565 integrity re-check below — a
|
|
391
|
+
// deliberate local edit is not "tampering".
|
|
381
392
|
if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) {
|
|
382
393
|
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
383
394
|
}
|
|
@@ -386,8 +397,28 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
386
397
|
stamped = fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim();
|
|
387
398
|
}
|
|
388
399
|
catch { /* pre-feature: unstamped */ }
|
|
389
|
-
if (stamped === version)
|
|
390
|
-
|
|
400
|
+
if (stamped === version) {
|
|
401
|
+
const source = opts.sourceDirOverride ?? findPackageHelpersDir();
|
|
402
|
+
const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
|
|
403
|
+
if (integrity.blocked)
|
|
404
|
+
return { refreshed: false, blocked: integrity.blocked };
|
|
405
|
+
if (integrity.tampered.length === 0)
|
|
406
|
+
return { refreshed: false }; // up to date AND verified intact
|
|
407
|
+
// Tampering detected post-install — heal via the same fail-closed
|
|
408
|
+
// verify-then-copy path a version-bump refresh already uses.
|
|
409
|
+
await opts.beforeWriteOverride?.();
|
|
410
|
+
const healRes = await writeCriticalHelpers(helpersDir, version, {
|
|
411
|
+
sourceDirOverride: opts.sourceDirOverride,
|
|
412
|
+
pubkeyPemOverride: opts.pubkeyPemOverride,
|
|
413
|
+
});
|
|
414
|
+
if (healRes.blocked)
|
|
415
|
+
return { refreshed: false, blocked: healRes.blocked };
|
|
416
|
+
if (healRes.wrote)
|
|
417
|
+
await ensureCommonJsCompanions(helpersDir); // #3555
|
|
418
|
+
return healRes.wrote
|
|
419
|
+
? { refreshed: true, healed: true, tampered: integrity.tampered, from: stamped, to: version }
|
|
420
|
+
: { refreshed: false };
|
|
421
|
+
}
|
|
391
422
|
if (stamped && semver.valid(stamped) && semver.valid(version) && semver.gte(stamped, version)) {
|
|
392
423
|
// Stamped version is already >= what this binary reports — refreshing
|
|
393
424
|
// would silently DOWNGRADE the helpers. Skip, untouched.
|
|
@@ -400,6 +431,8 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
400
431
|
});
|
|
401
432
|
if (res.blocked)
|
|
402
433
|
return { refreshed: false, blocked: res.blocked };
|
|
434
|
+
if (res.wrote)
|
|
435
|
+
await ensureCommonJsCompanions(helpersDir); // #3555
|
|
403
436
|
return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
|
|
404
437
|
}
|
|
405
438
|
async function refreshOneHelpersDir(helpersDir, version, opts) {
|
|
@@ -412,8 +445,18 @@ async function refreshOneHelpersDir(helpersDir, version, opts) {
|
|
|
412
445
|
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
413
446
|
}
|
|
414
447
|
try {
|
|
415
|
-
if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version)
|
|
416
|
-
|
|
448
|
+
if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version) {
|
|
449
|
+
// #3565: re-hash before trusting a stamp match (helper-integrity.ts).
|
|
450
|
+
// Read-only — no lock needed unless something's actually wrong; the
|
|
451
|
+
// (rare) heal path below re-verifies under lock.
|
|
452
|
+
const source = opts.sourceDirOverride ?? findPackageHelpersDir();
|
|
453
|
+
const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
|
|
454
|
+
if (integrity.blocked)
|
|
455
|
+
return { refreshed: false, blocked: integrity.blocked };
|
|
456
|
+
if (integrity.tampered.length === 0)
|
|
457
|
+
return { refreshed: false };
|
|
458
|
+
// else: tampering found — fall through to acquire the lock and heal.
|
|
459
|
+
}
|
|
417
460
|
}
|
|
418
461
|
catch { /* unstamped: continue to the locked path */ }
|
|
419
462
|
const releaseLock = await acquireRefreshLock(helpersDir, opts);
|
|
@@ -242,7 +242,7 @@ const [,, command, ...args] = process.argv;
|
|
|
242
242
|
if (command && commands[command]) {
|
|
243
243
|
commands[command](...args);
|
|
244
244
|
} else {
|
|
245
|
-
console.log('Usage: session.
|
|
245
|
+
console.log('Usage: session.cjs <start|restore|end|status|update|metric> [args]');
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
module.exports = commands;
|
|
@@ -336,7 +336,7 @@ if (require.main === module) {
|
|
|
336
336
|
const result = routeTask(task);
|
|
337
337
|
console.log(JSON.stringify(result, null, 2));
|
|
338
338
|
} else {
|
|
339
|
-
console.log('Usage: router.
|
|
339
|
+
console.log('Usage: router.cjs <task description>');
|
|
340
340
|
console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
|
|
341
341
|
}
|
|
342
342
|
}
|
|
@@ -427,7 +427,7 @@ const value = valueParts.join(' ');
|
|
|
427
427
|
if (command && commands[command]) {
|
|
428
428
|
commands[command](key, value);
|
|
429
429
|
} else {
|
|
430
|
-
console.log('Usage: memory.
|
|
430
|
+
console.log('Usage: memory.cjs <get|set|delete|clear|keys> [key] [value]');
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
module.exports = commands;
|
|
@@ -504,9 +504,9 @@ export function generateHookHandler() {
|
|
|
504
504
|
' return null;',
|
|
505
505
|
'}',
|
|
506
506
|
'',
|
|
507
|
-
"const router = safeRequire(path.join(helpersDir, 'router.
|
|
508
|
-
"const session = safeRequire(path.join(helpersDir, 'session.
|
|
509
|
-
"const memory = safeRequire(path.join(helpersDir, 'memory.
|
|
507
|
+
"const router = safeRequire(path.join(helpersDir, 'router.cjs'));",
|
|
508
|
+
"const session = safeRequire(path.join(helpersDir, 'session.cjs'));",
|
|
509
|
+
"const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));",
|
|
510
510
|
"const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));",
|
|
511
511
|
'',
|
|
512
512
|
'const [,, command, ...args] = process.argv;',
|
|
@@ -1005,12 +1005,14 @@ export function generateAutoMemoryHook() {
|
|
|
1005
1005
|
*/
|
|
1006
1006
|
|
|
1007
1007
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1008
|
-
import { join, dirname } from 'path';
|
|
1008
|
+
import { join, dirname, resolve } from 'path';
|
|
1009
1009
|
import { fileURLToPath } from 'url';
|
|
1010
1010
|
|
|
1011
1011
|
const __filename = fileURLToPath(import.meta.url);
|
|
1012
1012
|
const __dirname = dirname(__filename);
|
|
1013
|
-
const PROJECT_ROOT =
|
|
1013
|
+
const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
|
|
1014
|
+
? resolve(process.env.CLAUDE_PROJECT_DIR)
|
|
1015
|
+
: join(__dirname, '../..');
|
|
1014
1016
|
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
|
1015
1017
|
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
|
1016
1018
|
|
|
@@ -1356,7 +1358,7 @@ const [,, command, ...args] = process.argv;
|
|
|
1356
1358
|
if (command && commands[command]) {
|
|
1357
1359
|
commands[command](...args);
|
|
1358
1360
|
} else {
|
|
1359
|
-
console.log('Usage: session.
|
|
1361
|
+
console.log('Usage: session.cjs <start|restore|end|status>');
|
|
1360
1362
|
console.log(\`Platform: \${platform}\`);
|
|
1361
1363
|
console.log(\`Data dir: \${SESSION_DIR}\`);
|
|
1362
1364
|
}
|
|
@@ -1374,9 +1376,9 @@ export function generateHelpers(options) {
|
|
|
1374
1376
|
helpers['pre-commit'] = generatePreCommitHook();
|
|
1375
1377
|
helpers['post-commit'] = generatePostCommitHook();
|
|
1376
1378
|
// Cross-platform Node.js scripts
|
|
1377
|
-
helpers['session.
|
|
1378
|
-
helpers['router.
|
|
1379
|
-
helpers['memory.
|
|
1379
|
+
helpers['session.cjs'] = generateCrossPlatformSessionManager();
|
|
1380
|
+
helpers['router.cjs'] = generateAgentRouter();
|
|
1381
|
+
helpers['memory.cjs'] = generateMemoryHelper();
|
|
1380
1382
|
// Windows-specific scripts
|
|
1381
1383
|
helpers['daemon-manager.ps1'] = generateWindowsDaemonManager();
|
|
1382
1384
|
helpers['daemon-manager.cmd'] = generateWindowsBatchWrapper();
|
|
@@ -27,6 +27,12 @@ import { fileURLToPath } from 'url';
|
|
|
27
27
|
import { dirname } from 'path';
|
|
28
28
|
import { trackRequest } from './mcp-tools/request-tracker.js';
|
|
29
29
|
import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
|
|
30
|
+
/** A failed task can be valid data; only an explicit error denotes tool failure. */
|
|
31
|
+
function hasToolError(result) {
|
|
32
|
+
return result !== null && typeof result === 'object'
|
|
33
|
+
&& typeof result.error === 'string'
|
|
34
|
+
&& result.error.trim().length > 0;
|
|
35
|
+
}
|
|
30
36
|
// ESM-compatible __dirname
|
|
31
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
32
38
|
const __dirname = dirname(__filename);
|
|
@@ -666,11 +672,12 @@ export class MCPServerManager extends EventEmitter {
|
|
|
666
672
|
}
|
|
667
673
|
try {
|
|
668
674
|
const result = await callMCPTool(toolName, toolParams, { sessionId });
|
|
669
|
-
|
|
675
|
+
const isError = hasToolError(result);
|
|
676
|
+
trackRequest(toolName, !isError);
|
|
670
677
|
return {
|
|
671
678
|
jsonrpc: '2.0',
|
|
672
679
|
id: message.id,
|
|
673
|
-
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] },
|
|
680
|
+
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError },
|
|
674
681
|
};
|
|
675
682
|
}
|
|
676
683
|
catch (error) {
|
|
@@ -741,7 +748,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
741
748
|
handler: async (input, context) => {
|
|
742
749
|
try {
|
|
743
750
|
const result = await callMCPTool(tool.name, input || {}, { sessionId: context?.sessionId || fallbackSessionId });
|
|
744
|
-
trackRequest(tool.name,
|
|
751
|
+
trackRequest(tool.name, !hasToolError(result));
|
|
745
752
|
return result;
|
|
746
753
|
}
|
|
747
754
|
catch (error) {
|