@claude-flow/cli 3.56.3 → 3.56.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +13 -8
  2. package/.claude/helpers/helpers.manifest.json +5 -5
  3. package/.claude/helpers/hook-handler.cjs +19 -14
  4. package/.claude/helpers/learning-service.mjs +5 -2
  5. package/.claude/helpers/session.cjs +48 -12
  6. package/.claude/helpers/statusline.cjs +14 -4
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/commands/doctor.js +7 -13
  9. package/dist/src/commands/hooks.js +3 -6
  10. package/dist/src/commands/init.js +3 -10
  11. package/dist/src/commands/metaharness.js +9 -14
  12. package/dist/src/commands/plugins.js +81 -8
  13. package/dist/src/commands/security-code-patterns.d.ts +40 -0
  14. package/dist/src/commands/security-code-patterns.js +192 -0
  15. package/dist/src/commands/security.js +8 -11
  16. package/dist/src/commands/status.js +8 -10
  17. package/dist/src/init/executor.d.ts +1 -1
  18. package/dist/src/init/executor.js +25 -16
  19. package/dist/src/init/helper-commands.d.ts +18 -0
  20. package/dist/src/init/helper-commands.js +142 -0
  21. package/dist/src/init/helpers-generator.js +128 -29
  22. package/dist/src/init/settings-generator.js +36 -103
  23. package/dist/src/mcp-tools/metaharness-tools.js +7 -10
  24. package/dist/src/mcp-tools/task-tools.js +24 -3
  25. package/dist/src/mcp-tools/testgen-tools.js +8 -12
  26. package/dist/src/mcp-tools/transfer-tools.js +9 -8
  27. package/dist/src/mcp-tools/workflow-tools.js +10 -4
  28. package/dist/src/memory/memory-initializer.js +2 -19
  29. package/dist/src/memory/shared-sqlite.d.ts +6 -0
  30. package/dist/src/memory/shared-sqlite.js +35 -7
  31. package/dist/src/missions/schemas.d.ts +125 -125
  32. package/dist/src/mods/command-registry/schema.d.ts +199 -199
  33. package/dist/src/plugins/manager.d.ts +3 -1
  34. package/dist/src/plugins/manager.js +31 -10
  35. package/dist/src/plugins/own-install.d.ts +15 -0
  36. package/dist/src/plugins/own-install.js +51 -0
  37. package/dist/src/plugins/store/discovery.d.ts +26 -2
  38. package/dist/src/plugins/store/discovery.js +108 -81
  39. package/dist/src/plugins/store/index.d.ts +1 -1
  40. package/dist/src/plugins/store/index.js +1 -1
  41. package/dist/src/plugins/store/types.d.ts +5 -0
  42. package/dist/src/plugins/trust-policy.d.ts +13 -0
  43. package/dist/src/plugins/trust-policy.js +29 -0
  44. package/dist/src/ruvector/graph-analyzer.js +18 -6
  45. package/dist/src/transfer/ipfs/client.js +3 -1
  46. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  47. package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts +1 -0
  48. package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts.map +1 -1
  49. package/node_modules/@claude-flow/mcp/dist/resource-registry.js +10 -4
  50. package/node_modules/@claude-flow/mcp/dist/resource-registry.js.map +1 -1
  51. package/node_modules/@claude-flow/mcp/dist/sampling.d.ts.map +1 -1
  52. package/node_modules/@claude-flow/mcp/dist/sampling.js +13 -6
  53. package/node_modules/@claude-flow/mcp/dist/sampling.js.map +1 -1
  54. package/node_modules/@claude-flow/mcp/dist/task-manager.d.ts.map +1 -1
  55. package/node_modules/@claude-flow/mcp/dist/task-manager.js +10 -1
  56. package/node_modules/@claude-flow/mcp/dist/task-manager.js.map +1 -1
  57. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts +1 -0
  58. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  59. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +17 -11
  60. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  61. package/node_modules/@claude-flow/mcp/package.json +1 -1
  62. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  63. package/node_modules/@claude-flow/security/dist/index.d.ts +1 -1
  64. package/node_modules/@claude-flow/security/dist/index.d.ts.map +1 -1
  65. package/node_modules/@claude-flow/security/dist/index.js +1 -1
  66. package/node_modules/@claude-flow/security/dist/index.js.map +1 -1
  67. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts +9 -1
  68. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts.map +1 -1
  69. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js +15 -2
  70. package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js.map +1 -1
  71. package/node_modules/@claude-flow/security/package.json +1 -1
  72. package/package.json +5 -5
@@ -134,10 +134,12 @@ export declare class PluginManager {
134
134
  /**
135
135
  * Upgrade a plugin to a new version
136
136
  */
137
- upgrade(packageName: string, version?: string): Promise<{
137
+ upgrade(packageName: string, version?: string, opts?: Pick<PluginInstallOptions, 'verify' | 'trust'>): Promise<{
138
138
  success: boolean;
139
139
  error?: string;
140
140
  plugin?: InstalledPlugin;
141
+ decision?: TrustDecision;
142
+ warnings?: string[];
141
143
  }>;
142
144
  /**
143
145
  * Update plugin config
@@ -411,7 +411,7 @@ export class PluginManager {
411
411
  /**
412
412
  * Upgrade a plugin to a new version
413
413
  */
414
- async upgrade(packageName, version) {
414
+ async upgrade(packageName, version, opts = {}) {
415
415
  if (!this.manifest) {
416
416
  await this.initialize();
417
417
  }
@@ -427,24 +427,45 @@ export class PluginManager {
427
427
  // Validate package name to prevent injection (S-3)
428
428
  validatePackageName(versionSpec);
429
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.
430
+ // #3557: the new version's lifecycle scripts run only with an explicit
431
+ // --trust for this upgrade. A recorded scriptsRun:true from an earlier
432
+ // install is not enough: it may come from registry vouching that was
433
+ // never verified, and the new version's scripts were never reviewed.
434
+ const warnings = [];
435
+ const scriptsRun = opts.trust === true;
432
436
  const upgradeArgs = ['install', '--prefix', this.config.pluginsDir, versionSpec];
433
- if (existing.scriptsRun === false)
437
+ if (!scriptsRun) {
434
438
  upgradeArgs.push('--ignore-scripts');
439
+ warnings.push(`Install scripts were skipped for ${packageName} (--ignore-scripts). Upgrade with --trust to run them.`);
440
+ }
435
441
  await runNpm(upgradeArgs, 120000);
436
442
  // Update manifest
437
443
  const installDir = path.join(this.config.pluginsDir, 'node_modules');
438
444
  const packageJsonPath = path.join(installDir, packageName, 'package.json');
439
- if (fs.existsSync(packageJsonPath)) {
440
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
441
- existing.version = pkg.version;
442
- existing.commands = pkg['claude-flow']?.commands || existing.commands;
443
- existing.hooks = pkg['claude-flow']?.hooks || existing.hooks;
445
+ if (!fs.existsSync(packageJsonPath)) {
446
+ return { success: false, error: `Upgraded package.json not found for ${packageName}` };
447
+ }
448
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
449
+ // #3557: re-run the trust policy on the NEW version's declaration, so
450
+ // hooks and commands withheld at install (or newly declared, or newly
451
+ // needing elevated permissions) are not registered without --trust.
452
+ const trusted = applyTrustPolicy(pkg, { verify: opts.verify, trust: opts.trust });
453
+ existing.version = String(pkg.version ?? existing.version);
454
+ existing.commands = trusted.commands;
455
+ existing.hooks = trusted.hooks;
456
+ existing.trustLevel = trusted.trustLevel;
457
+ existing.permissions = trusted.permissions;
458
+ existing.verification = opts.verify === false ? 'skipped' : 'npm-integrity';
459
+ existing.scriptsRun = scriptsRun;
460
+ if (trusted.withheld) {
461
+ existing.withheld = trusted.withheld;
462
+ }
463
+ else {
464
+ delete existing.withheld;
444
465
  }
445
466
  await this.saveManifest();
446
467
  console.log(`[PluginManager] Upgraded ${packageName} to ${existing.version}`);
447
- return { success: true, plugin: existing };
468
+ return { success: true, plugin: existing, decision: trusted.decision, warnings };
448
469
  }
449
470
  catch (error) {
450
471
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Directories from `startDir` upward that still belong to the package's own
3
+ * install: `startDir` and its ancestors, up to but excluding the first
4
+ * `node_modules` directory. In a source checkout (no `node_modules` between the
5
+ * module and the repo root) this reaches the repo root, as monorepo dev needs.
6
+ */
7
+ export declare function ownInstallAncestors(startDir: string, maxDepth?: number): string[];
8
+ /**
9
+ * Candidate `<ancestor>/plugins/<plugin>/<...sub>` directories inside the
10
+ * package's own install (see {@link ownInstallAncestors}). The `<ancestor>/../plugins`
11
+ * form (the sibling of a package root) is only produced when that sibling is itself
12
+ * still inside the own-install boundary.
13
+ */
14
+ export declare function ownPluginDirs(startDir: string, plugin: string, ...sub: string[]): string[];
15
+ //# sourceMappingURL=own-install.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Where the CLI may load executable plugin code from: its OWN install only.
3
+ *
4
+ * Several callers walk up from their own module location to find a bundled
5
+ * plugin directory (`plugins/ruflo-*`) whose scripts they then import or spawn.
6
+ * A project-local install lives at `<project>/node_modules/@claude-flow/cli/...`,
7
+ * so an unbounded walk-up leaves the package, crosses `node_modules`, and lands in
8
+ * `<project>` itself, where `<project>/plugins/ruflo-*` is whatever the opened
9
+ * repository ships. The walk must therefore stop at the `node_modules` boundary:
10
+ * everything above it belongs to whoever installed the package (a project, a
11
+ * global prefix, an npx cache), not to the package.
12
+ */
13
+ import { basename, dirname, join, resolve } from 'node:path';
14
+ /**
15
+ * Directories from `startDir` upward that still belong to the package's own
16
+ * install: `startDir` and its ancestors, up to but excluding the first
17
+ * `node_modules` directory. In a source checkout (no `node_modules` between the
18
+ * module and the repo root) this reaches the repo root, as monorepo dev needs.
19
+ */
20
+ export function ownInstallAncestors(startDir, maxDepth = 8) {
21
+ const dirs = [];
22
+ let p = resolve(startDir);
23
+ for (let i = 0; i < maxDepth; i++) {
24
+ if (basename(p) === 'node_modules')
25
+ break;
26
+ dirs.push(p);
27
+ const parent = dirname(p);
28
+ if (parent === p)
29
+ break;
30
+ p = parent;
31
+ }
32
+ return dirs;
33
+ }
34
+ /**
35
+ * Candidate `<ancestor>/plugins/<plugin>/<...sub>` directories inside the
36
+ * package's own install (see {@link ownInstallAncestors}). The `<ancestor>/../plugins`
37
+ * form (the sibling of a package root) is only produced when that sibling is itself
38
+ * still inside the own-install boundary.
39
+ */
40
+ export function ownPluginDirs(startDir, plugin, ...sub) {
41
+ const ancestors = ownInstallAncestors(startDir);
42
+ const inside = new Set(ancestors);
43
+ const out = [];
44
+ for (const p of ancestors) {
45
+ out.push(join(p, 'plugins', plugin, ...sub));
46
+ if (inside.has(dirname(p)))
47
+ out.push(join(p, '..', 'plugins', plugin, ...sub));
48
+ }
49
+ return out;
50
+ }
51
+ //# sourceMappingURL=own-install.js.map
@@ -38,7 +38,21 @@ export interface PluginDiscoveryResult {
38
38
  * is NOT the verified registry, and callers must say so rather than report a discovery.
39
39
  */
40
40
  demo?: boolean;
41
+ /**
42
+ * True only when the registry's signature verified against the pinned key. Entries of an
43
+ * unverified registry carry no registry trust: their trustLevel is reported as
44
+ * 'unverified', so nothing downstream can treat them as registry-vouched.
45
+ */
46
+ verified?: boolean;
47
+ /** Why the registry is not verified (set whenever `verified` is not true). */
48
+ unverifiedReason?: string;
41
49
  }
50
+ /** Registry-level verification status, for callers that report registry data. */
51
+ export declare function registryStatus(result: PluginDiscoveryResult): {
52
+ verified: boolean;
53
+ unverifiedReason?: string;
54
+ source?: string;
55
+ };
42
56
  /**
43
57
  * Plugin Discovery Service
44
58
  */
@@ -46,10 +60,20 @@ export declare class PluginDiscoveryService {
46
60
  private config;
47
61
  private cache;
48
62
  constructor(config?: Partial<PluginStoreConfig>);
63
+ /**
64
+ * Whether an unverified registry may be replaced by the built-in list. Off under
65
+ * `CLAUDE_FLOW_STRICT_PLUGINS=true` (ADR-145) unless the config says otherwise.
66
+ */
67
+ private allowsUnverifiedFallback;
49
68
  /**
50
69
  * Discover plugin registry via IPNS
51
70
  */
52
71
  discoverRegistry(registryName?: string): Promise<PluginDiscoveryResult>;
72
+ /**
73
+ * The signed registry is unavailable or unverified. Outside strict mode, use the built-in
74
+ * list, labelled as such and stripped of registry trust; in strict mode, fail.
75
+ */
76
+ private fallbackToBuiltIn;
53
77
  /**
54
78
  * Create demo plugin registry with real npm stats
55
79
  */
@@ -63,7 +87,7 @@ export declare class PluginDiscoveryService {
63
87
  */
64
88
  private getDemoPluginsWithStats;
65
89
  /**
66
- * Verify registry Ed25519 signature.
90
+ * Verify the registry Ed25519 signature; returns why it failed, or null when it verified.
67
91
  *
68
92
  * Mirrors the signing scheme in scripts/publish-registry.ts: the signer
69
93
  * removes registrySignature + registryPublicKey from the registry object
@@ -73,7 +97,7 @@ export declare class PluginDiscoveryService {
73
97
  * whoever served the registry and can be swapped by a compromised
74
98
  * gateway / on-path attacker.
75
99
  */
76
- private verifyRegistrySignature;
100
+ private registrySignatureFailure;
77
101
  /**
78
102
  * List available registries
79
103
  */
@@ -4,6 +4,7 @@
4
4
  * Parallel implementation to pattern store for plugins
5
5
  */
6
6
  import { resolveIPNS, fetchFromIPFS, verifyEd25519Signature } from '../../transfer/ipfs/client.js';
7
+ import { isPlaceholderSigningKey, isStrictPluginMode } from '../trust-policy.js';
7
8
  /**
8
9
  * Fetch real npm download stats for a package
9
10
  */
@@ -84,6 +85,32 @@ export const DEFAULT_PLUGIN_STORE_CONFIG = {
84
85
  allowedPermissions: ['network', 'filesystem', 'memory', 'hooks'],
85
86
  requirePermissionPrompt: true,
86
87
  };
88
+ /**
89
+ * Strip registry-asserted trust from a registry that was not verified. Its trust labels, its
90
+ * `official` list and its author verification are just claims made by whoever served (or
91
+ * hardcoded) the list.
92
+ */
93
+ function withoutRegistryTrust(registry) {
94
+ return {
95
+ ...registry,
96
+ plugins: registry.plugins.map((p) => ({
97
+ ...p,
98
+ trustLevel: 'unverified',
99
+ verified: false,
100
+ ...(p.author ? { author: { ...p.author, verified: false } } : {}),
101
+ })),
102
+ official: [],
103
+ authors: (registry.authors ?? []).map((a) => ({ ...a, verified: false })),
104
+ };
105
+ }
106
+ /** Registry-level verification status, for callers that report registry data. */
107
+ export function registryStatus(result) {
108
+ return {
109
+ verified: result.verified === true,
110
+ ...(result.verified === true ? {} : { unverifiedReason: result.unverifiedReason ?? 'registry not verified' }),
111
+ ...(result.source ? { source: result.source } : {}),
112
+ };
113
+ }
87
114
  /**
88
115
  * Plugin Discovery Service
89
116
  */
@@ -93,6 +120,13 @@ export class PluginDiscoveryService {
93
120
  constructor(config = {}) {
94
121
  this.config = { ...DEFAULT_PLUGIN_STORE_CONFIG, ...config };
95
122
  }
123
+ /**
124
+ * Whether an unverified registry may be replaced by the built-in list. Off under
125
+ * `CLAUDE_FLOW_STRICT_PLUGINS=true` (ADR-145) unless the config says otherwise.
126
+ */
127
+ allowsUnverifiedFallback() {
128
+ return this.config.allowUnverifiedFallback ?? !isStrictPluginMode();
129
+ }
96
130
  /**
97
131
  * Discover plugin registry via IPNS
98
132
  */
@@ -112,11 +146,13 @@ export class PluginDiscoveryService {
112
146
  console.log(`[PluginDiscovery] Cache hit for ${registry.name}`);
113
147
  return {
114
148
  success: true,
115
- registry: cached.registry,
149
+ registry: cached.verified ? cached.registry : withoutRegistryTrust(cached.registry),
116
150
  fromCache: true,
117
151
  // A cached fallback is still the fallback: never relabel it as the real registry.
118
152
  source: cached.demo ? `${registry.name} (demo)` : registry.name,
119
153
  ...(cached.demo ? { demo: true } : {}),
154
+ verified: cached.verified === true,
155
+ ...(cached.verified ? {} : { unverifiedReason: cached.unverifiedReason ?? 'registry not verified' }),
120
156
  };
121
157
  }
122
158
  try {
@@ -132,15 +168,14 @@ export class PluginDiscoveryService {
132
168
  // Resolve IPNS to get current CID
133
169
  cid = await resolveIPNS(registry.ipnsName, registry.gateway);
134
170
  if (!cid) {
135
- // Fallback to demo registry
136
- return this.createDemoRegistryAsync(registry);
171
+ return this.fallbackToBuiltIn(registry, `could not resolve IPNS name ${registry.ipnsName}`);
137
172
  }
138
173
  console.log(`[PluginDiscovery] Resolved IPNS to CID: ${cid}`);
139
174
  }
140
175
  // Fetch registry from IPFS
141
176
  const registryData = await fetchFromIPFS(cid, registry.gateway);
142
177
  if (!registryData) {
143
- return this.createDemoRegistryAsync(registry);
178
+ return this.fallbackToBuiltIn(registry, `could not fetch registry CID ${cid}`);
144
179
  }
145
180
  // Verify registry signature when required.
146
181
  // Fail closed on missing/invalid signature — silently warning and using
@@ -148,17 +183,35 @@ export class PluginDiscoveryService {
148
183
  // on-path attacker) swap in attacker-mapped plugin entries that the
149
184
  // installer would then load unsandboxed.
150
185
  if (this.config.requireVerification) {
151
- const verified = await this.verifyRegistrySignature(registryData, registry.publicKey);
152
- if (!verified) {
153
- console.warn(`[PluginDiscovery] Registry signature verification failed for ` +
154
- `${registry.name} (CID ${cid}); falling back to demo registry.`);
155
- return this.createDemoRegistryAsync(registry);
186
+ const failure = await this.registrySignatureFailure(registryData, registry.publicKey);
187
+ if (failure) {
188
+ return this.fallbackToBuiltIn(registry, `${failure}, CID ${cid}`);
156
189
  }
157
190
  }
191
+ else {
192
+ // Verification disabled by config: usable, but it vouches for nothing.
193
+ const unverifiedReason = 'signature verification disabled (requireVerification: false)';
194
+ this.cache.set(registry.ipnsName, {
195
+ registry: registryData,
196
+ timestamp: Date.now(),
197
+ verified: false,
198
+ unverifiedReason,
199
+ });
200
+ return {
201
+ success: true,
202
+ registry: withoutRegistryTrust(registryData),
203
+ cid,
204
+ source: registry.name,
205
+ fromCache: false,
206
+ verified: false,
207
+ unverifiedReason,
208
+ };
209
+ }
158
210
  // Cache the result
159
211
  this.cache.set(registry.ipnsName, {
160
212
  registry: registryData,
161
213
  timestamp: Date.now(),
214
+ verified: true,
162
215
  });
163
216
  return {
164
217
  success: true,
@@ -166,14 +219,43 @@ export class PluginDiscoveryService {
166
219
  cid,
167
220
  source: registry.name,
168
221
  fromCache: false,
222
+ verified: true,
169
223
  };
170
224
  }
171
225
  catch (error) {
172
226
  console.error(`[PluginDiscovery] Failed to discover registry:`, error);
173
- // Return demo registry on error
174
- return this.createDemoRegistryAsync(registry);
227
+ return this.fallbackToBuiltIn(registry, `discovery error: ${error instanceof Error ? error.message : String(error)}`);
175
228
  }
176
229
  }
230
+ /**
231
+ * The signed registry is unavailable or unverified. Outside strict mode, use the built-in
232
+ * list, labelled as such and stripped of registry trust; in strict mode, fail.
233
+ */
234
+ async fallbackToBuiltIn(registry, reason) {
235
+ if (!this.allowsUnverifiedFallback()) {
236
+ return {
237
+ success: false,
238
+ verified: false,
239
+ unverifiedReason: reason,
240
+ error: `Plugin registry ${registry.name} is not verified (${reason}). ` +
241
+ 'Not falling back to the built-in plugin list because CLAUDE_FLOW_STRICT_PLUGINS=true.',
242
+ };
243
+ }
244
+ console.warn(`[PluginDiscovery] Registry ${registry.name} is not verified (${reason}); ` +
245
+ 'using the built-in plugin list, which is unverified and confers no registry trust.');
246
+ const result = await this.createDemoRegistryAsync(registry);
247
+ const cached = this.cache.get(registry.ipnsName);
248
+ if (cached) {
249
+ cached.verified = false;
250
+ cached.unverifiedReason = reason;
251
+ }
252
+ return {
253
+ ...result,
254
+ ...(result.registry ? { registry: withoutRegistryTrust(result.registry) } : {}),
255
+ verified: false,
256
+ unverifiedReason: reason,
257
+ };
258
+ }
177
259
  /**
178
260
  * Create demo plugin registry with real npm stats
179
261
  */
@@ -211,10 +293,10 @@ export class PluginDiscoveryService {
211
293
  totalPlugins: plugins.length,
212
294
  totalDownloads: plugins.reduce((sum, p) => sum + p.downloads, 0),
213
295
  totalAuthors: 1,
214
- featured: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-agentic-qe', '@claude-flow/plugin-prime-radiant', '@claude-flow/security', '@claude-flow/claims', '@claude-flow/teammate-plugin'],
215
- trending: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-agentic-qe', '@claude-flow/plugin-prime-radiant'],
216
- newest: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-agentic-qe', '@claude-flow/plugin-prime-radiant'],
217
- official: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-agentic-qe', '@claude-flow/plugin-prime-radiant', '@claude-flow/security', '@claude-flow/claims'],
296
+ featured: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-prime-radiant', '@claude-flow/security', '@claude-flow/claims', '@claude-flow/teammate-plugin'],
297
+ trending: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-prime-radiant'],
298
+ newest: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-prime-radiant'],
299
+ official: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-prime-radiant', '@claude-flow/security', '@claude-flow/claims'],
218
300
  compatibilityMatrix: [
219
301
  { pluginId: '@claude-flow/neural', pluginVersion: '3.0.0', claudeFlowVersions: ['3.x'], tested: true },
220
302
  { pluginId: '@claude-flow/security', pluginVersion: '3.0.0', claudeFlowVersions: ['3.x'], tested: true },
@@ -548,66 +630,6 @@ export class PluginDiscoveryService {
548
630
  verified: true,
549
631
  trustLevel: 'official',
550
632
  },
551
- // Agentic QE - AI-powered quality engineering
552
- {
553
- id: '@claude-flow/plugin-agentic-qe',
554
- name: '@claude-flow/plugin-agentic-qe',
555
- displayName: 'Agentic Quality Engineering',
556
- description: 'AI-powered quality engineering with 58 agents that write tests, find bugs, predict defects, scan security, and perform chaos engineering safely.',
557
- version: '3.0.0-alpha.5',
558
- cid: 'bafybeiagenticqeplugin2024',
559
- size: 285000,
560
- checksum: 'sha256:agenticqe2024xyz',
561
- author: officialAuthor,
562
- license: 'MIT',
563
- categories: ['ai-ml', 'devops', 'security'],
564
- tags: ['testing', 'qe', 'tdd', 'security', 'chaos-engineering', 'coverage', 'defect-prediction', 'agents'],
565
- keywords: ['quality', 'testing', 'agents', 'tdd', 'security'],
566
- downloads: 1200,
567
- rating: 4.8,
568
- ratingCount: 24,
569
- lastUpdated: baseTime,
570
- createdAt: '2026-01-20T00:00:00Z',
571
- minClaudeFlowVersion: '3.0.0',
572
- dependencies: [
573
- { name: '@claude-flow/core', version: '^3.0.0' },
574
- ],
575
- type: 'integration',
576
- hooks: [
577
- 'aqe:generate-tests',
578
- 'aqe:analyze-coverage',
579
- 'aqe:security-scan',
580
- 'aqe:predict-defects',
581
- 'aqe:chaos-inject',
582
- ],
583
- commands: [
584
- 'aqe generate-tests',
585
- 'aqe tdd-cycle',
586
- 'aqe security-scan',
587
- 'aqe predict-defects',
588
- 'aqe chaos-inject',
589
- 'aqe quality-gate',
590
- 'aqe visual-regression',
591
- ],
592
- permissions: ['filesystem', 'network', 'memory'],
593
- exports: [
594
- 'TestGenerator',
595
- 'CoverageAnalyzer',
596
- 'SecurityScanner',
597
- 'DefectPredictor',
598
- 'ChaosInjector',
599
- 'QualityGate',
600
- ],
601
- verified: true,
602
- trustLevel: 'official',
603
- securityAudit: {
604
- auditor: 'claude-flow-security-team',
605
- auditDate: '2026-01-20T00:00:00Z',
606
- auditVersion: '3.0.0-alpha.3',
607
- passed: true,
608
- issues: [],
609
- },
610
- },
611
633
  // Prime Radiant - Mathematical coherence and consensus verification
612
634
  {
613
635
  id: '@claude-flow/plugin-prime-radiant',
@@ -1093,7 +1115,6 @@ export class PluginDiscoveryService {
1093
1115
  const basePlugins = this.getDemoPlugins();
1094
1116
  // Only fetch stats for real npm packages
1095
1117
  const realNpmPackages = [
1096
- '@claude-flow/plugin-agentic-qe',
1097
1118
  '@claude-flow/plugin-prime-radiant',
1098
1119
  '@claude-flow/claims',
1099
1120
  '@claude-flow/security',
@@ -1158,7 +1179,7 @@ export class PluginDiscoveryService {
1158
1179
  });
1159
1180
  }
1160
1181
  /**
1161
- * Verify registry Ed25519 signature.
1182
+ * Verify the registry Ed25519 signature; returns why it failed, or null when it verified.
1162
1183
  *
1163
1184
  * Mirrors the signing scheme in scripts/publish-registry.ts: the signer
1164
1185
  * removes registrySignature + registryPublicKey from the registry object
@@ -1168,9 +1189,14 @@ export class PluginDiscoveryService {
1168
1189
  * whoever served the registry and can be swapped by a compromised
1169
1190
  * gateway / on-path attacker.
1170
1191
  */
1171
- async verifyRegistrySignature(registry, expectedPublicKey) {
1172
- if (!registry.registrySignature || !expectedPublicKey) {
1173
- return false;
1192
+ async registrySignatureFailure(registry, expectedPublicKey) {
1193
+ // A placeholder pin (e.g. the all-zero key) can't vouch for anything, and the all-zero
1194
+ // key accepts a forged signature under ZIP-215 verification. Never verify against it.
1195
+ if (isPlaceholderSigningKey(expectedPublicKey)) {
1196
+ return 'pinned registry signing key is missing or a placeholder';
1197
+ }
1198
+ if (!registry.registrySignature) {
1199
+ return 'registry is unsigned';
1174
1200
  }
1175
1201
  // Object spread preserves insertion order; delete drops a key without
1176
1202
  // re-ordering the rest, matching the signer's view of the registry.
@@ -1178,7 +1204,8 @@ export class PluginDiscoveryService {
1178
1204
  delete registryToVerify.registrySignature;
1179
1205
  delete registryToVerify.registryPublicKey;
1180
1206
  const message = JSON.stringify(registryToVerify);
1181
- return verifyEd25519Signature(message, registry.registrySignature, expectedPublicKey);
1207
+ const ok = await verifyEd25519Signature(message, registry.registrySignature, expectedPublicKey);
1208
+ return ok ? null : 'registry signature does not verify against the pinned key';
1182
1209
  }
1183
1210
  /**
1184
1211
  * List available registries
@@ -9,7 +9,7 @@
9
9
  * - Publish with signing
10
10
  */
11
11
  export * from './types.js';
12
- export { PluginDiscoveryService, createPluginDiscoveryService, DEFAULT_PLUGIN_STORE_CONFIG, type PluginDiscoveryResult, } from './discovery.js';
12
+ export { PluginDiscoveryService, createPluginDiscoveryService, DEFAULT_PLUGIN_STORE_CONFIG, registryStatus, type PluginDiscoveryResult, } from './discovery.js';
13
13
  export { searchPlugins, getPluginSearchSuggestions, getPluginTagCloud, getPluginCategoryStats, findSimilarPlugins, getFeaturedPlugins, getTrendingPlugins, getNewestPlugins, getOfficialPlugins, getPluginsByPermission, } from './search.js';
14
14
  import type { PluginRegistry, PluginEntry, PluginSearchOptions, PluginSearchResult, PluginStoreConfig } from './types.js';
15
15
  import { PluginDiscoveryService } from './discovery.js';
@@ -11,7 +11,7 @@
11
11
  // Re-export types
12
12
  export * from './types.js';
13
13
  // Re-export discovery
14
- export { PluginDiscoveryService, createPluginDiscoveryService, DEFAULT_PLUGIN_STORE_CONFIG, } from './discovery.js';
14
+ export { PluginDiscoveryService, createPluginDiscoveryService, DEFAULT_PLUGIN_STORE_CONFIG, registryStatus, } from './discovery.js';
15
15
  // Re-export search
16
16
  export { searchPlugins, getPluginSearchSuggestions, getPluginTagCloud, getPluginCategoryStats, findSimilarPlugins, getFeaturedPlugins, getTrendingPlugins, getNewestPlugins, getOfficialPlugins, getPluginsByPermission, } from './search.js';
17
17
  import { PluginDiscoveryService } from './discovery.js';
@@ -249,6 +249,11 @@ export interface PluginStoreConfig {
249
249
  cacheDir: string;
250
250
  cacheExpiry: number;
251
251
  requireVerification: boolean;
252
+ /**
253
+ * Fall back to the built-in (unverified) plugin list when the signed registry can't be
254
+ * verified. Defaults to true, or false under `CLAUDE_FLOW_STRICT_PLUGINS=true`.
255
+ */
256
+ allowUnverifiedFallback?: boolean;
252
257
  requireSecurityAudit: boolean;
253
258
  minTrustLevel: TrustLevel;
254
259
  trustedAuthors: string[];
@@ -58,4 +58,17 @@ export declare function shouldRunInstallScripts(opts: {
58
58
  /** A registry checksum we can actually verify: `sha256:` + 64 hex chars. */
59
59
  export declare function parseSha256Checksum(checksum: unknown): string | null;
60
60
  export declare function sha256Hex(data: Buffer): string;
61
+ /**
62
+ * Whether `CLAUDE_FLOW_STRICT_PLUGINS=true` (ADR-145): an unverified plugin
63
+ * registry is then an error, not a warning.
64
+ */
65
+ export declare function isStrictPluginMode(env?: NodeJS.ProcessEnv): boolean;
66
+ /**
67
+ * A signing key that cannot vouch for anything: missing, not 32 bytes of hex,
68
+ * not a curve point, or a small-order point — which includes the all-zero
69
+ * placeholder in `trust/trust-anchors.json`. Signatures can be forged against
70
+ * small-order keys under ZIP-215 verification, so such a key must never be
71
+ * handed to a verifier.
72
+ */
73
+ export declare function isPlaceholderSigningKey(key: unknown): boolean;
61
74
  //# sourceMappingURL=trust-policy.d.ts.map
@@ -6,6 +6,7 @@
6
6
  * These pure helpers decide what an install may register.
7
7
  */
8
8
  import { createHash } from 'crypto';
9
+ import { ExtendedPoint } from '@noble/ed25519';
9
10
  /**
10
11
  * Permissions a plugin may declare and still have its hooks and commands
11
12
  * registered without `--trust`. Anything else (filesystem, network, shell,
@@ -81,4 +82,32 @@ export function parseSha256Checksum(checksum) {
81
82
  export function sha256Hex(data) {
82
83
  return createHash('sha256').update(data).digest('hex');
83
84
  }
85
+ /**
86
+ * Whether `CLAUDE_FLOW_STRICT_PLUGINS=true` (ADR-145): an unverified plugin
87
+ * registry is then an error, not a warning.
88
+ */
89
+ export function isStrictPluginMode(env = process.env) {
90
+ return (env.CLAUDE_FLOW_STRICT_PLUGINS ?? '').trim().toLowerCase() === 'true';
91
+ }
92
+ /**
93
+ * A signing key that cannot vouch for anything: missing, not 32 bytes of hex,
94
+ * not a curve point, or a small-order point — which includes the all-zero
95
+ * placeholder in `trust/trust-anchors.json`. Signatures can be forged against
96
+ * small-order keys under ZIP-215 verification, so such a key must never be
97
+ * handed to a verifier.
98
+ */
99
+ export function isPlaceholderSigningKey(key) {
100
+ if (typeof key !== 'string')
101
+ return true;
102
+ const hex = key.trim().replace(/^ed25519:/i, '');
103
+ if (!/^[0-9a-f]{64}$/i.test(hex))
104
+ return true;
105
+ try {
106
+ // zip215=true decodes every encoding a lenient verifier would accept.
107
+ return ExtendedPoint.fromHex(hex, true).isSmallOrder();
108
+ }
109
+ catch {
110
+ return true;
111
+ }
112
+ }
84
113
  //# sourceMappingURL=trust-policy.js.map
@@ -494,6 +494,17 @@ function fallbackLouvain(nodes, edges) {
494
494
  let improved = true;
495
495
  const maxIterations = 10;
496
496
  let iteration = 0;
497
+ // Community total weighted degree. Maintained incrementally (±nodeDegree on
498
+ // each accepted move) rather than rebuilt from a full scan of `community`
499
+ // inside the per-node loop below — the rebuild was O(n) per node (O(n^2) per
500
+ // pass), matching neither the reference Louvain implementations (networkx's
501
+ // `Stot`, python-louvain's `status.degrees`, both updated in O(1) per move)
502
+ // nor the algorithm's own linear-on-sparse-data design intent.
503
+ const communityTotal = new Map();
504
+ for (const node of nodes) {
505
+ const c = community.get(node);
506
+ communityTotal.set(c, (communityTotal.get(c) || 0) + (degree.get(node) || 0));
507
+ }
497
508
  while (improved && iteration < maxIterations) {
498
509
  improved = false;
499
510
  iteration++;
@@ -507,17 +518,16 @@ function fallbackLouvain(nodes, edges) {
507
518
  const neighborCommunity = community.get(neighbor);
508
519
  communityWeights.set(neighborCommunity, (communityWeights.get(neighborCommunity) || 0) + weight);
509
520
  }
510
- // Calculate community totals
511
- const communityTotal = new Map();
512
- for (const [n, c] of Array.from(community.entries())) {
513
- communityTotal.set(c, (communityTotal.get(c) || 0) + (degree.get(n) || 0));
514
- }
515
521
  let bestCommunity = currentCommunity;
516
522
  let bestGain = 0;
517
523
  for (const [targetCommunity, edgeWeight] of Array.from(communityWeights.entries())) {
518
524
  if (targetCommunity === currentCommunity)
519
525
  continue;
520
- // Calculate modularity gain
526
+ // Calculate modularity gain. `currentTotal` still includes this
527
+ // node's own degree (not yet removed) — the `+ nodeDegree` term
528
+ // below backs it out, so `communityTotal` must stay un-patched for
529
+ // `node` until after the move decision, matching the pre-existing
530
+ // formula this complexity fix leaves unchanged.
521
531
  const currentTotal = communityTotal.get(currentCommunity) || 0;
522
532
  const targetTotal = communityTotal.get(targetCommunity) || 0;
523
533
  const currentEdges = communityWeights.get(currentCommunity) || 0;
@@ -530,6 +540,8 @@ function fallbackLouvain(nodes, edges) {
530
540
  }
531
541
  if (bestCommunity !== currentCommunity) {
532
542
  community.set(node, bestCommunity);
543
+ communityTotal.set(currentCommunity, (communityTotal.get(currentCommunity) || 0) - nodeDegree);
544
+ communityTotal.set(bestCommunity, (communityTotal.get(bestCommunity) || 0) + nodeDegree);
533
545
  improved = true;
534
546
  }
535
547
  }