claude-flow 3.48.0 → 3.50.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.
Files changed (63) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/hook-handler.cjs +20 -4
  3. package/.claude/helpers/memory.cjs +1 -1
  4. package/.claude/helpers/router.cjs +1 -1
  5. package/.claude/helpers/session.cjs +1 -1
  6. package/.claude/proven-config.json +42 -0
  7. package/.claude-plugin/marketplace.json +26 -1
  8. package/README.md +1 -53
  9. package/README.zh-CN.md +1 -53
  10. package/node_modules/@claude-flow/codex/package.json +1 -1
  11. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  12. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  13. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  14. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  15. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  16. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  17. package/node_modules/@claude-flow/security/package.json +1 -1
  18. package/package.json +2 -2
  19. package/v3/@claude-flow/cli/README.md +3 -53
  20. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  21. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +88 -9
  23. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +7 -4
  24. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/memory.js +30 -7
  27. package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
  29. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  30. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/session.js +128 -21
  32. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  33. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  34. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  35. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  36. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  37. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  38. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  39. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  40. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  41. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +22 -10
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +87 -14
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +43 -6
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +236 -50
  48. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +6 -3
  49. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
  50. package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
  51. package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
  52. package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
  53. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
  54. package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
  55. package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
  56. package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
  57. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  58. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  59. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  60. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  61. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  62. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +42 -2
  63. package/v3/@claude-flow/cli/package.json +2 -2
@@ -4,9 +4,11 @@
4
4
  * Bridges discovery service with file system persistence
5
5
  */
6
6
  import * as fs from 'fs';
7
+ import * as os from 'os';
7
8
  import * as path from 'path';
8
9
  import { execFile } from 'child_process';
9
10
  import { promisify } from 'util';
11
+ import { evaluatePluginTrust, parseSha256Checksum, readDeclaredTrust, sha256Hex, shouldRunInstallScripts, } from './trust-policy.js';
10
12
  const execFileAsync = promisify(execFile);
11
13
  // On Windows, `npm` is a shell script (no `.exe`) and `npm.cmd` is a batch
12
14
  // wrapper. Since Node 18.20.2 / 20.12.2 (CVE-2024-27980) the runtime refuses
@@ -30,6 +32,30 @@ function validatePackageName(spec) {
30
32
  throw new Error(`Invalid package name: ${spec}`);
31
33
  }
32
34
  }
35
+ /**
36
+ * Apply the #3557 trust policy to a plugin's package.json: record its declared
37
+ * trust and permissions, and withhold hooks/commands the policy doesn't allow.
38
+ */
39
+ function applyTrustPolicy(pkg, opts) {
40
+ const block = (pkg['claude-flow'] ?? {});
41
+ const commands = Array.isArray(block.commands) ? block.commands : [];
42
+ const hooks = Array.isArray(block.hooks) ? block.hooks : [];
43
+ const declared = readDeclaredTrust(pkg);
44
+ const permissions = [...new Set([...(opts.registryPermissions ?? []), ...declared.permissions])];
45
+ const trustLevel = opts.registryTrustLevel ?? declared.trustLevel;
46
+ const decision = evaluatePluginTrust({ trustLevel: declared.trustLevel, permissions }, { verify: opts.verify !== false, trust: opts.trust === true, registryTrustLevel: opts.registryTrustLevel });
47
+ if (decision.allowed) {
48
+ return { commands, hooks, trustLevel, permissions, decision };
49
+ }
50
+ return {
51
+ commands: [],
52
+ hooks: [],
53
+ trustLevel,
54
+ permissions,
55
+ withheld: { hooks, commands, reasons: decision.reasons },
56
+ decision,
57
+ };
58
+ }
33
59
  // ============================================================================
34
60
  // Plugin Manager
35
61
  // ============================================================================
@@ -98,11 +124,14 @@ export class PluginManager {
98
124
  /**
99
125
  * Install a plugin from npm
100
126
  */
101
- async installFromNpm(packageName, version) {
127
+ async installFromNpm(packageName, version, opts = {}) {
102
128
  if (!this.manifest) {
103
129
  await this.initialize();
104
130
  }
105
131
  const versionSpec = version ? `${packageName}@${version}` : packageName;
132
+ const verify = opts.verify !== false;
133
+ const warnings = [];
134
+ let tmpDir;
106
135
  try {
107
136
  // Check if already installed
108
137
  if (this.manifest.plugins[packageName]) {
@@ -116,23 +145,56 @@ export class PluginManager {
116
145
  await this.ensureDirectory(installDir);
117
146
  // Validate package name to prevent injection (S-3)
118
147
  validatePackageName(versionSpec);
148
+ // #3557: with --verify, a registry checksum must match the tarball we
149
+ // install. Pack first, hash that exact file, then install from it, so the
150
+ // bytes that were checked are the bytes that get installed.
151
+ let installTarget = versionSpec;
152
+ let verification = verify ? 'npm-integrity' : 'skipped';
153
+ const expected = verify ? parseSha256Checksum(opts.expectedChecksum) : null;
154
+ if (expected) {
155
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruflo-plugin-verify-'));
156
+ const packed = await runNpm(['pack', versionSpec, '--pack-destination', tmpDir, '--json', '--ignore-scripts'], 120000);
157
+ const info = JSON.parse(packed.stdout);
158
+ const filename = info[0]?.filename;
159
+ if (!filename)
160
+ throw new Error(`npm pack returned no tarball for ${versionSpec}`);
161
+ const tarball = path.join(tmpDir, path.basename(filename));
162
+ const actual = sha256Hex(fs.readFileSync(tarball));
163
+ if (actual !== expected) {
164
+ return {
165
+ success: false,
166
+ error: `Checksum mismatch for ${versionSpec}: registry expects sha256:${expected}, ` +
167
+ `downloaded tarball is sha256:${actual}. Refusing to install (--verify).`,
168
+ };
169
+ }
170
+ installTarget = tarball;
171
+ verification = 'checksum';
172
+ }
173
+ else if (verify && opts.expectedChecksum) {
174
+ warnings.push(`Registry checksum "${opts.expectedChecksum}" is not a verifiable sha256 digest; ` +
175
+ `relying on npm's own registry integrity check.`);
176
+ }
119
177
  // Use npm to install (array form prevents shell injection)
120
178
  console.log(`[PluginManager] Installing ${versionSpec}...`);
121
- await runNpm(['install', '--prefix', this.config.pluginsDir, versionSpec], 120000);
179
+ // #3557 follow-up: lifecycle scripts run before the package's own trust
180
+ // declaration can be read, so untrusted installs skip them entirely.
181
+ const scriptsRun = shouldRunInstallScripts(opts);
182
+ const installArgs = ['install', '--prefix', this.config.pluginsDir, installTarget];
183
+ if (!scriptsRun) {
184
+ installArgs.push('--ignore-scripts');
185
+ warnings.push(`Install scripts were skipped for ${packageName} (--ignore-scripts): it is not registry-vouched and --trust was not given. ` +
186
+ `Reinstall with --trust to run them.`);
187
+ }
188
+ await runNpm(installArgs, 120000);
122
189
  // Get installed version
123
190
  const packageJsonPath = path.join(installDir, packageName, 'package.json');
124
191
  let installedVersion = version || 'latest';
125
- let commands = [];
126
- let hooks = [];
192
+ let pkg = {};
127
193
  if (fs.existsSync(packageJsonPath)) {
128
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
129
- installedVersion = pkg.version;
130
- // Check for claude-flow plugin metadata
131
- if (pkg['claude-flow']) {
132
- commands = pkg['claude-flow'].commands || [];
133
- hooks = pkg['claude-flow'].hooks || [];
134
- }
194
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
195
+ installedVersion = String(pkg.version ?? installedVersion);
135
196
  }
197
+ const trusted = applyTrustPolicy(pkg, opts);
136
198
  // Create plugin entry
137
199
  const plugin = {
138
200
  name: packageName,
@@ -141,25 +203,34 @@ export class PluginManager {
141
203
  enabled: true,
142
204
  source: 'npm',
143
205
  path: path.join(installDir, packageName),
144
- commands,
145
- hooks,
206
+ commands: trusted.commands,
207
+ hooks: trusted.hooks,
208
+ trustLevel: trusted.trustLevel,
209
+ permissions: trusted.permissions,
210
+ verification,
211
+ scriptsRun,
212
+ ...(trusted.withheld ? { withheld: trusted.withheld } : {}),
146
213
  };
147
214
  // Save to manifest
148
215
  this.manifest.plugins[packageName] = plugin;
149
216
  await this.saveManifest();
150
217
  console.log(`[PluginManager] Installed ${packageName}@${installedVersion}`);
151
- return { success: true, plugin };
218
+ return { success: true, plugin, decision: trusted.decision, warnings };
152
219
  }
153
220
  catch (error) {
154
221
  const errorMsg = error instanceof Error ? error.message : String(error);
155
222
  console.error(`[PluginManager] Failed to install ${packageName}:`, errorMsg);
156
223
  return { success: false, error: errorMsg };
157
224
  }
225
+ finally {
226
+ if (tmpDir)
227
+ fs.rmSync(tmpDir, { recursive: true, force: true });
228
+ }
158
229
  }
159
230
  /**
160
231
  * Install a plugin from a local path
161
232
  */
162
- async installFromLocal(sourcePath) {
233
+ async installFromLocal(sourcePath, opts = {}) {
163
234
  if (!this.manifest) {
164
235
  await this.initialize();
165
236
  }
@@ -182,6 +253,10 @@ export class PluginManager {
182
253
  error: `Plugin ${packageName} is already installed`,
183
254
  };
184
255
  }
256
+ // #3557: record declared trust/permissions; withhold hooks and commands
257
+ // the policy doesn't allow without --trust. A local path has no registry
258
+ // entry to vouch for it, so the plugin's own declaration decides.
259
+ const trusted = applyTrustPolicy(pkg, { verify: opts.verify, trust: opts.trust });
185
260
  // Create plugin entry (link to local path, don't copy)
186
261
  const plugin = {
187
262
  name: packageName,
@@ -190,14 +265,20 @@ export class PluginManager {
190
265
  enabled: true,
191
266
  source: 'local',
192
267
  path: absolutePath,
193
- commands: pkg['claude-flow']?.commands || [],
194
- hooks: pkg['claude-flow']?.hooks || [],
268
+ commands: trusted.commands,
269
+ hooks: trusted.hooks,
270
+ trustLevel: trusted.trustLevel,
271
+ permissions: trusted.permissions,
272
+ verification: opts.verify === false ? 'skipped' : 'policy',
273
+ // A local install links the path; it never runs npm or package scripts.
274
+ scriptsRun: false,
275
+ ...(trusted.withheld ? { withheld: trusted.withheld } : {}),
195
276
  };
196
277
  // Save to manifest
197
278
  this.manifest.plugins[packageName] = plugin;
198
279
  await this.saveManifest();
199
280
  console.log(`[PluginManager] Installed local plugin ${packageName}@${pkg.version}`);
200
- return { success: true, plugin };
281
+ return { success: true, plugin, decision: trusted.decision };
201
282
  }
202
283
  catch (error) {
203
284
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -345,8 +426,13 @@ export class PluginManager {
345
426
  const versionSpec = version ? `${packageName}@${version}` : `${packageName}@latest`;
346
427
  // Validate package name to prevent injection (S-3)
347
428
  validatePackageName(versionSpec);
348
- // Reinstall with new version (array form prevents shell injection)
349
- await runNpm(['install', '--prefix', this.config.pluginsDir, versionSpec], 120000);
429
+ // Reinstall with new version (array form prevents shell injection).
430
+ // An install recorded with scriptsRun:false stays script-free on upgrade;
431
+ // legacy entries (no field) keep their pre-#3557 behaviour.
432
+ const upgradeArgs = ['install', '--prefix', this.config.pluginsDir, versionSpec];
433
+ if (existing.scriptsRun === false)
434
+ upgradeArgs.push('--ignore-scripts');
435
+ await runNpm(upgradeArgs, 120000);
350
436
  // Update manifest
351
437
  const installDir = path.join(this.config.pluginsDir, 'node_modules');
352
438
  const packageJsonPath = path.join(installDir, packageName, 'package.json');
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Plugin install trust policy (#3557).
3
+ *
4
+ * `plugins install --verify` used to be parsed and never read, and a plugin's
5
+ * declared `trustLevel`/`permissions` were neither recorded nor enforced.
6
+ * These pure helpers decide what an install may register.
7
+ */
8
+ /**
9
+ * Permissions a plugin may declare and still have its hooks and commands
10
+ * registered without `--trust`. Anything else (filesystem, network, shell,
11
+ * secrets, config, privileged, …) needs an explicit `--trust`.
12
+ */
13
+ export declare const DEFAULT_PLUGIN_PERMISSIONS: readonly string[];
14
+ /** Trust levels that are withheld unless the user passes `--trust`. */
15
+ export declare const UNTRUSTED_TRUST_LEVELS: readonly string[];
16
+ /** Registry trust levels that the registry itself vouches for. */
17
+ export declare const REGISTRY_VOUCHED_TRUST_LEVELS: readonly string[];
18
+ export interface DeclaredTrust {
19
+ trustLevel?: string;
20
+ permissions: string[];
21
+ }
22
+ export interface TrustDecision {
23
+ /** Hooks and commands may be registered. */
24
+ allowed: boolean;
25
+ /** Verification was skipped with `--no-verify`. */
26
+ verificationSkipped: boolean;
27
+ /** Why registration was withheld (empty when allowed). */
28
+ reasons: string[];
29
+ /** Declared permissions outside {@link DEFAULT_PLUGIN_PERMISSIONS}. */
30
+ excessPermissions: string[];
31
+ }
32
+ export interface TrustOptions {
33
+ /** `--verify` (default true). */
34
+ verify: boolean;
35
+ /** `--trust` (default false). */
36
+ trust: boolean;
37
+ /** Trust level from a registry entry, when the plugin was found there. */
38
+ registryTrustLevel?: string;
39
+ }
40
+ /**
41
+ * Read the trust declaration from a plugin's package.json. The `claude-flow`
42
+ * block wins; top-level fields are accepted as a fallback.
43
+ */
44
+ export declare function readDeclaredTrust(pkg: Record<string, unknown>): DeclaredTrust;
45
+ /** Decide whether an install may register the plugin's hooks and commands. */
46
+ export declare function evaluatePluginTrust(declared: DeclaredTrust, opts: TrustOptions): TrustDecision;
47
+ /**
48
+ * Whether npm may run the package's lifecycle scripts (preinstall/install/
49
+ * postinstall, and those of its dependencies). Scripts execute before the
50
+ * package's own trust declaration can be read, so only an explicit `--trust`
51
+ * or a registry-vouched entry allows them; everything else installs with
52
+ * `--ignore-scripts`.
53
+ */
54
+ export declare function shouldRunInstallScripts(opts: {
55
+ trust?: boolean;
56
+ registryTrustLevel?: string;
57
+ }): boolean;
58
+ /** A registry checksum we can actually verify: `sha256:` + 64 hex chars. */
59
+ export declare function parseSha256Checksum(checksum: unknown): string | null;
60
+ export declare function sha256Hex(data: Buffer): string;
61
+ //# sourceMappingURL=trust-policy.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Plugin install trust policy (#3557).
3
+ *
4
+ * `plugins install --verify` used to be parsed and never read, and a plugin's
5
+ * declared `trustLevel`/`permissions` were neither recorded nor enforced.
6
+ * These pure helpers decide what an install may register.
7
+ */
8
+ import { createHash } from 'crypto';
9
+ /**
10
+ * Permissions a plugin may declare and still have its hooks and commands
11
+ * registered without `--trust`. Anything else (filesystem, network, shell,
12
+ * secrets, config, privileged, …) needs an explicit `--trust`.
13
+ */
14
+ export const DEFAULT_PLUGIN_PERMISSIONS = Object.freeze(['memory:read']);
15
+ /** Trust levels that are withheld unless the user passes `--trust`. */
16
+ export const UNTRUSTED_TRUST_LEVELS = Object.freeze(['untrusted', 'unverified']);
17
+ /** Registry trust levels that the registry itself vouches for. */
18
+ export const REGISTRY_VOUCHED_TRUST_LEVELS = Object.freeze(['official', 'verified']);
19
+ function asStringList(value) {
20
+ if (!Array.isArray(value))
21
+ return [];
22
+ return value.filter((v) => typeof v === 'string' && v.trim() !== '').map((v) => v.trim());
23
+ }
24
+ /**
25
+ * Read the trust declaration from a plugin's package.json. The `claude-flow`
26
+ * block wins; top-level fields are accepted as a fallback.
27
+ */
28
+ export function readDeclaredTrust(pkg) {
29
+ const block = (pkg['claude-flow'] && typeof pkg['claude-flow'] === 'object'
30
+ ? pkg['claude-flow']
31
+ : {});
32
+ const trustLevel = typeof block.trustLevel === 'string'
33
+ ? block.trustLevel
34
+ : typeof pkg.trustLevel === 'string' ? pkg.trustLevel : undefined;
35
+ const permissions = asStringList(block.permissions).length > 0
36
+ ? asStringList(block.permissions)
37
+ : asStringList(pkg.permissions);
38
+ return { trustLevel, permissions };
39
+ }
40
+ /** Decide whether an install may register the plugin's hooks and commands. */
41
+ export function evaluatePluginTrust(declared, opts) {
42
+ const excessPermissions = declared.permissions.filter((p) => !DEFAULT_PLUGIN_PERMISSIONS.includes(p));
43
+ if (!opts.verify) {
44
+ return { allowed: true, verificationSkipped: true, reasons: [], excessPermissions };
45
+ }
46
+ const reasons = [];
47
+ const registryVouched = opts.registryTrustLevel !== undefined
48
+ && REGISTRY_VOUCHED_TRUST_LEVELS.includes(opts.registryTrustLevel);
49
+ if (!registryVouched) {
50
+ if (declared.trustLevel && UNTRUSTED_TRUST_LEVELS.includes(declared.trustLevel)) {
51
+ reasons.push(`declares trustLevel "${declared.trustLevel}"`);
52
+ }
53
+ if (excessPermissions.length > 0) {
54
+ reasons.push(`declares permissions beyond the default set (${DEFAULT_PLUGIN_PERMISSIONS.join(', ')}): ${excessPermissions.join(', ')}`);
55
+ }
56
+ }
57
+ if (reasons.length > 0 && !opts.trust) {
58
+ return { allowed: false, verificationSkipped: false, reasons, excessPermissions };
59
+ }
60
+ return { allowed: true, verificationSkipped: false, reasons: [], excessPermissions };
61
+ }
62
+ /**
63
+ * Whether npm may run the package's lifecycle scripts (preinstall/install/
64
+ * postinstall, and those of its dependencies). Scripts execute before the
65
+ * package's own trust declaration can be read, so only an explicit `--trust`
66
+ * or a registry-vouched entry allows them; everything else installs with
67
+ * `--ignore-scripts`.
68
+ */
69
+ export function shouldRunInstallScripts(opts) {
70
+ if (opts.trust === true)
71
+ return true;
72
+ return opts.registryTrustLevel !== undefined && REGISTRY_VOUCHED_TRUST_LEVELS.includes(opts.registryTrustLevel);
73
+ }
74
+ /** A registry checksum we can actually verify: `sha256:` + 64 hex chars. */
75
+ export function parseSha256Checksum(checksum) {
76
+ if (typeof checksum !== 'string')
77
+ return null;
78
+ const m = /^sha256:([0-9a-f]{64})$/i.exec(checksum.trim());
79
+ return m ? m[1].toLowerCase() : null;
80
+ }
81
+ export function sha256Hex(data) {
82
+ return createHash('sha256').update(data).digest('hex');
83
+ }
84
+ //# sourceMappingURL=trust-policy.js.map
@@ -17,6 +17,12 @@ export declare function issuePolicyApproval(approval: Omit<PolicyApproval, 'uses
17
17
  issuedAt?: number;
18
18
  }, projectRoot?: string, approvalIssuerVerifier?: (issuer: string) => boolean): Promise<PolicyApproval>;
19
19
  export declare function revokePolicyApproval(id: string, projectRoot?: string): Promise<boolean>;
20
+ /**
21
+ * Read-only verification (#3568). It must not run inside
22
+ * `withPolicyTransaction`, whose own post-operation check throws a generic
23
+ * `policy-ledger-verification-failed` and hides which check failed. The only
24
+ * write is persisting an anchor established for a pre-anchor ledger.
25
+ */
20
26
  export declare function verifyPolicyLedger(projectRoot?: string): Promise<ReturnType<AgenticPolicyEngine['verifyLedger']>>;
21
27
  export declare function authorizeMcpTool(toolName: string, input: Record<string, unknown>, context?: Record<string, unknown>, attributes?: Readonly<{
22
28
  actionType?: string;
@@ -4,6 +4,7 @@ import { execFileSync } from 'node:child_process';
4
4
  import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { hostname, userInfo } from 'node:os';
7
+ import { syncPolicyProjection } from '../mods/policy-projection.js';
7
8
  const POLICY_DIR = join('.claude-flow', 'policy');
8
9
  const POLICY_FILE = 'state.json';
9
10
  const LOCK_FILE = 'state.lock';
@@ -227,6 +228,20 @@ function verifyStateAnchor(projectRoot, state) {
227
228
  }
228
229
  }
229
230
  async function writePolicyState(projectRoot, statePath, state) {
231
+ await writePolicyStateFiles(projectRoot, statePath, state);
232
+ // ADR-404: the ruflo mod reads Claude Code tool rules from a small
233
+ // projection, never from state.json. Written only after the state and its
234
+ // anchor are safely down; a failure here never fails the state write. A
235
+ // projection left stale by such a failure can only tighten, never loosen,
236
+ // a Claude Code verdict (the mod merges with `stricter`).
237
+ try {
238
+ syncPolicyProjection(projectRoot, state);
239
+ }
240
+ catch (error) {
241
+ process.stderr.write(`[policy] claude-code projection not written: ${error.message}\n`);
242
+ }
243
+ }
244
+ async function writePolicyStateFiles(projectRoot, statePath, state) {
230
245
  const anchorPath = trustPaths(projectRoot).anchor;
231
246
  if (state.mode === 'enforce' || existsSync(anchorPath)) {
232
247
  const key = trustKey(projectRoot, true);
@@ -338,9 +353,12 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
338
353
  approvalIssuerVerifier: options.approvalIssuerVerifier,
339
354
  });
340
355
  const result = await operation(engine);
341
- const nextState = engine.exportState();
356
+ // Verify before exporting: verification establishes the ledger anchor
357
+ // (#3568) on state written before the anchor existed, and that anchor
358
+ // must be part of what is persisted.
342
359
  if (!engine.verifyLedger().valid)
343
360
  throw new Error('policy-ledger-verification-failed');
361
+ const nextState = engine.exportState();
344
362
  await writePolicyState(projectRoot, target.state, nextState);
345
363
  return result;
346
364
  }
@@ -393,8 +411,30 @@ export async function issuePolicyApproval(approval, projectRoot = process.cwd(),
393
411
  export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
394
412
  return withPolicyTransaction(projectRoot, (engine) => engine.revokeApproval(id));
395
413
  }
414
+ /**
415
+ * Read-only verification (#3568). It must not run inside
416
+ * `withPolicyTransaction`, whose own post-operation check throws a generic
417
+ * `policy-ledger-verification-failed` and hides which check failed. The only
418
+ * write is persisting an anchor established for a pre-anchor ledger.
419
+ */
396
420
  export async function verifyPolicyLedger(projectRoot = process.cwd()) {
397
- return withPolicyTransaction(projectRoot, (engine) => engine.verifyLedger());
421
+ const target = paths(projectRoot);
422
+ mkdirSync(target.dir, { recursive: true, mode: 0o700 });
423
+ const release = await acquireLock(target.lock);
424
+ try {
425
+ const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), {
426
+ signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
427
+ keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
428
+ });
429
+ const result = engine.verifyLedger();
430
+ if (result.anchor === 'established-now') {
431
+ await writePolicyState(projectRoot, target.state, engine.exportState());
432
+ }
433
+ return result;
434
+ }
435
+ finally {
436
+ release();
437
+ }
398
438
  }
399
439
  /**
400
440
  * Resolve the caller identity for `authorizeMcpTool`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.48.0",
3
+ "version": "3.50.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -103,7 +103,7 @@
103
103
  "@claude-flow/mcp": "3.0.0",
104
104
  "@claude-flow/neural": "3.0.0",
105
105
  "@claude-flow/plugin-agent-federation": "1.0.0",
106
- "@claude-flow/security": "3.0.0",
106
+ "@claude-flow/security": "3.0.1",
107
107
  "@claude-flow/shared": "3.0.0",
108
108
  "@iarna/toml": "^2.2.5",
109
109
  "@noble/ed25519": "2.3.0",