@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.
- package/.claude/helpers/auto-memory-hook.mjs +13 -8
- package/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +19 -14
- package/.claude/helpers/learning-service.mjs +5 -2
- package/.claude/helpers/session.cjs +48 -12
- package/.claude/helpers/statusline.cjs +14 -4
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.js +7 -13
- package/dist/src/commands/hooks.js +3 -6
- package/dist/src/commands/init.js +3 -10
- package/dist/src/commands/metaharness.js +9 -14
- package/dist/src/commands/plugins.js +81 -8
- package/dist/src/commands/security-code-patterns.d.ts +40 -0
- package/dist/src/commands/security-code-patterns.js +192 -0
- package/dist/src/commands/security.js +8 -11
- package/dist/src/commands/status.js +8 -10
- package/dist/src/init/executor.d.ts +1 -1
- package/dist/src/init/executor.js +25 -16
- package/dist/src/init/helper-commands.d.ts +18 -0
- package/dist/src/init/helper-commands.js +142 -0
- package/dist/src/init/helpers-generator.js +128 -29
- package/dist/src/init/settings-generator.js +36 -103
- package/dist/src/mcp-tools/metaharness-tools.js +7 -10
- package/dist/src/mcp-tools/task-tools.js +24 -3
- package/dist/src/mcp-tools/testgen-tools.js +8 -12
- package/dist/src/mcp-tools/transfer-tools.js +9 -8
- package/dist/src/mcp-tools/workflow-tools.js +10 -4
- package/dist/src/memory/memory-initializer.js +2 -19
- package/dist/src/memory/shared-sqlite.d.ts +6 -0
- package/dist/src/memory/shared-sqlite.js +35 -7
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/plugins/manager.d.ts +3 -1
- package/dist/src/plugins/manager.js +31 -10
- package/dist/src/plugins/own-install.d.ts +15 -0
- package/dist/src/plugins/own-install.js +51 -0
- package/dist/src/plugins/store/discovery.d.ts +26 -2
- package/dist/src/plugins/store/discovery.js +108 -81
- package/dist/src/plugins/store/index.d.ts +1 -1
- package/dist/src/plugins/store/index.js +1 -1
- package/dist/src/plugins/store/types.d.ts +5 -0
- package/dist/src/plugins/trust-policy.d.ts +13 -0
- package/dist/src/plugins/trust-policy.js +29 -0
- package/dist/src/ruvector/graph-analyzer.js +18 -6
- package/dist/src/transfer/ipfs/client.js +3 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js +10 -4
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.js +13 -6
- package/node_modules/@claude-flow/mcp/dist/sampling.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js +10 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +17 -11
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/index.d.ts +1 -1
- package/node_modules/@claude-flow/security/dist/index.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/index.js +1 -1
- package/node_modules/@claude-flow/security/dist/index.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts +9 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js +15 -2
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- 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
|
-
//
|
|
431
|
-
//
|
|
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 (
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
152
|
-
if (
|
|
153
|
-
|
|
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
|
-
|
|
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-
|
|
215
|
-
trending: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-
|
|
216
|
-
newest: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-
|
|
217
|
-
official: ['@claude-flow/plugin-iot-cognitum', '@claude-flow/plugin-agent-federation', '@claude-flow/plugin-
|
|
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
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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
|
}
|