claude-flow 3.47.1 → 3.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  3. package/.claude/helpers/hook-handler.cjs +7 -4
  4. package/.claude/helpers/learning-service.mjs +4 -2
  5. package/.claude/helpers/memory.cjs +1 -1
  6. package/.claude/helpers/metrics-db.mjs +4 -2
  7. package/.claude/helpers/router.cjs +1 -1
  8. package/.claude/helpers/session.cjs +1 -1
  9. package/.claude/proven-config.json +42 -0
  10. package/.claude-plugin/marketplace.json +16 -1
  11. package/README.md +1 -53
  12. package/README.zh-CN.md +1 -53
  13. package/node_modules/@claude-flow/codex/package.json +1 -1
  14. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  16. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  17. package/node_modules/@claude-flow/mcp/package.json +6 -3
  18. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  19. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  20. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  22. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  23. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  24. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  25. package/node_modules/@claude-flow/security/package.json +2 -3
  26. package/package.json +7 -7
  27. package/v3/@claude-flow/cli/README.md +1 -53
  28. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  29. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  30. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  32. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  33. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
  34. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
  35. package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
  36. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  37. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  38. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  39. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  40. package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
  41. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  42. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  43. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  44. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  45. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  46. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  47. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  48. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  49. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  50. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  51. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
  52. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
  62. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  63. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  64. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  68. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  69. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
  70. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  71. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  72. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  73. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  74. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  75. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  76. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  77. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  78. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  79. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
  80. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  81. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  82. package/v3/@claude-flow/cli/package.json +9 -9
  83. package/v3/@claude-flow/guidance/package.json +5 -6
  84. package/v3/@claude-flow/shared/package.json +6 -3
@@ -205,7 +205,9 @@ export interface MemoryInitResult {
205
205
  * Ensure memory_entries table has all required columns
206
206
  * Adds missing columns for older databases (e.g., 'content' column)
207
207
  */
208
- export declare function ensureSchemaColumns(dbPath: string): Promise<{
208
+ export declare function ensureSchemaColumns(dbPath: string, options?: {
209
+ encryptWrites?: boolean;
210
+ }): Promise<{
209
211
  success: boolean;
210
212
  columnsAdded: string[];
211
213
  error?: string;
@@ -496,6 +498,8 @@ export declare function listEntries(options: {
496
498
  limit?: number;
497
499
  offset?: number;
498
500
  dbPath?: string;
501
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
502
+ encryptWrites?: boolean;
499
503
  /** #2073: When true, include the entry's full `content` string in each result. */
500
504
  includeContent?: boolean;
501
505
  /** ADR-323: restrict rows to these provenance types. */
@@ -549,6 +553,8 @@ export declare function deleteEntry(options: {
549
553
  key: string;
550
554
  namespace?: string;
551
555
  dbPath?: string;
556
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
557
+ encryptWrites?: boolean;
552
558
  }): Promise<{
553
559
  success: boolean;
554
560
  deleted: boolean;
@@ -573,6 +579,8 @@ export declare function withMemoryDbLock<T>(dbPath: string, fn: () => Promise<T>
573
579
  export declare function purgeNamespace(options: {
574
580
  namespace: string;
575
581
  dbPath?: string;
582
+ /** Internal native-mirror writes must remain plaintext, including schema migration. */
583
+ encryptWrites?: boolean;
576
584
  }): Promise<{
577
585
  success: boolean;
578
586
  deletedCount: number;
@@ -8,12 +8,14 @@
8
8
  *
9
9
  * @module v3/cli/memory-initializer
10
10
  */
11
+ import { liveMemoryRowSql } from './live-memory-row.js';
11
12
  import * as fs from 'fs';
12
13
  import * as path from 'path';
13
14
  import { AsyncLocalStorage } from 'node:async_hooks';
14
15
  import { createRequire } from 'node:module';
15
16
  import { readFileMaybeEncrypted, writeFileAtomic, writeFileRestricted } from '../fs-secure.js';
16
17
  import { restoreMemoryDbFromBackup } from '../services/memory-backup.js';
18
+ import { validateIdentifier } from '../mcp-tools/validate-input.js';
17
19
  /**
18
20
  * ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
19
21
  * entry (a user's stated claim vs an agent's own output vs a tool result vs
@@ -208,14 +210,14 @@ async function getBridge() {
208
210
  * missing better-sqlite3. Appending the recorded reason turns an unactionable
209
211
  * message into a diagnosis.
210
212
  */
211
- async function walRefusalError(operation) {
213
+ async function walRefusalError(operation, bridgeDbPath) {
212
214
  const base = 'memory database has an active native WAL connection '
213
215
  + '(found -wal/-shm sidecar files) — refusing an unsafe sql.js '
214
216
  + `whole-image ${operation}. Retry once the native writer completes, or `
215
217
  + 'restore the native better-sqlite3 bridge.';
216
218
  try {
217
219
  const bridge = await getBridge();
218
- const reason = bridge?.getBridgeFailureReason?.();
220
+ const reason = bridge?.getBridgeFailureReason?.(bridgeDbPath);
219
221
  if (reason)
220
222
  return `${base} Bridge unavailable: ${reason}`;
221
223
  }
@@ -1141,7 +1143,7 @@ INSERT OR IGNORE INTO vector_indexes (id, name, dimensions) VALUES
1141
1143
  * Ensure memory_entries table has all required columns
1142
1144
  * Adds missing columns for older databases (e.g., 'content' column)
1143
1145
  */
1144
- export async function ensureSchemaColumns(dbPath) {
1146
+ export async function ensureSchemaColumns(dbPath, options = {}) {
1145
1147
  const columnsAdded = [];
1146
1148
  try {
1147
1149
  if (!fs.existsSync(dbPath)) {
@@ -1213,7 +1215,7 @@ export async function ensureSchemaColumns(dbPath) {
1213
1215
  if (modified) {
1214
1216
  // Save updated database
1215
1217
  const data = db.export();
1216
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
1218
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
1217
1219
  }
1218
1220
  db.close();
1219
1221
  return { success: true, columnsAdded };
@@ -1312,13 +1314,12 @@ async function activateControllerRegistry(verbose) {
1312
1314
  if (!bridge) {
1313
1315
  return { activated, failed, initTimeMs: performance.now() - startTime };
1314
1316
  }
1315
- const registry = await bridge.getControllerRegistry();
1316
- if (!registry) {
1317
+ const controllers = await bridge.bridgeListControllers();
1318
+ if (!controllers) {
1317
1319
  return { activated, failed, initTimeMs: performance.now() - startTime };
1318
1320
  }
1319
1321
  // Collect controller status from the registry
1320
- if (typeof registry.listControllers === 'function') {
1321
- const controllers = registry.listControllers();
1322
+ if (controllers) {
1322
1323
  for (const ctrl of controllers) {
1323
1324
  if (ctrl.enabled) {
1324
1325
  activated.push(ctrl.name);
@@ -1879,7 +1880,7 @@ export async function checkMemoryInitialization(dbPath) {
1879
1880
  // Try to load with sql.js
1880
1881
  const initSqlJs = (await import('sql.js')).default;
1881
1882
  const SQL = await initSqlJs();
1882
- const fileBuffer = fs.readFileSync(path_);
1883
+ const fileBuffer = readFileMaybeEncrypted(path_, null);
1883
1884
  db = new SQL.Database(fileBuffer);
1884
1885
  // Check for metadata table
1885
1886
  const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
@@ -2579,7 +2580,7 @@ export async function storeEntry(options) {
2579
2580
  return {
2580
2581
  success: false,
2581
2582
  id: '',
2582
- error: await walRefusalError('write'),
2583
+ error: await walRefusalError('write', options.dbPath),
2583
2584
  };
2584
2585
  }
2585
2586
  const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
@@ -2751,8 +2752,8 @@ export async function searchEntries(options) {
2751
2752
  // query (no extra round-trip) so a provenance-filtered search
2752
2753
  // still gets RaBitQ's speedup instead of falling back to brute
2753
2754
  // force.
2754
- const stmt = db.prepare('SELECT content, embedding, provenance_type FROM memory_entries WHERE id = ? AND status = ?');
2755
- stmt.bind([candidate.id, 'active']);
2755
+ const stmt = db.prepare(`SELECT content, embedding, provenance_type FROM memory_entries WHERE id = ? AND ${liveMemoryRowSql()}`);
2756
+ stmt.bind([candidate.id]);
2756
2757
  if (stmt.step()) {
2757
2758
  const [content, embeddingJson, provenanceTypeVal] = stmt.get();
2758
2759
  if (provenanceFilter?.length && !provenanceFilter.includes(provenanceTypeVal || 'unknown')) {
@@ -2812,29 +2813,28 @@ export async function searchEntries(options) {
2812
2813
  const db = new SQL.Database(fileBuffer);
2813
2814
  const provenanceByKey = new Map();
2814
2815
  for (const r of filtered) {
2815
- const stmt = db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1');
2816
+ const stmt = db.prepare(`SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? AND ${liveMemoryRowSql()} LIMIT 1`);
2816
2817
  stmt.bind([r.namespace, r.key]);
2817
2818
  if (stmt.step()) {
2818
- provenanceByKey.set(`${r.namespace}::${r.key}`, stmt.get()[0] || 'unknown');
2819
+ provenanceByKey.set(JSON.stringify([r.namespace, r.key]), stmt.get()[0] || 'unknown');
2819
2820
  }
2820
2821
  stmt.free();
2821
2822
  }
2822
2823
  db.close();
2823
2824
  filtered = filtered
2824
- .map(r => ({ ...r, provenanceType: provenanceByKey.get(`${r.namespace}::${r.key}`) || 'unknown' }));
2825
+ .filter(r => provenanceByKey.has(JSON.stringify([r.namespace, r.key])))
2826
+ .map(r => ({ ...r, provenanceType: provenanceByKey.get(JSON.stringify([r.namespace, r.key])) || 'unknown' }));
2825
2827
  if (provenanceFilter?.length) {
2826
2828
  filtered = filtered.filter(r => provenanceFilter.includes(r.provenanceType));
2827
2829
  }
2828
2830
  }
2829
2831
  catch {
2830
- // A requested trust filter fails closed. Unfiltered callers retain
2831
- // backward-compatible results with an explicit unknown label.
2832
- filtered = provenanceFilter?.length
2833
- ? []
2834
- : filtered.map(r => ({ ...r, provenanceType: 'unknown' }));
2832
+ // An ANN hit alone cannot prove the row is still live. Fall back
2833
+ // to the authoritative SQL scan if liveness cannot be checked.
2834
+ filtered = [];
2835
2835
  }
2836
2836
  }
2837
- if (!provenanceFilter?.length || filtered.length >= limit) {
2837
+ if (filtered.length >= limit) {
2838
2838
  return {
2839
2839
  success: true,
2840
2840
  results: filtered.slice(0, limit),
@@ -2853,7 +2853,7 @@ export async function searchEntries(options) {
2853
2853
  // Get entries with embeddings
2854
2854
  // ADR-323: build the WHERE clause incrementally so namespace and
2855
2855
  // provenance filters compose (both, either, or neither).
2856
- const whereClauses = [`status = 'active'`];
2856
+ const whereClauses = [liveMemoryRowSql()];
2857
2857
  const whereParams = [];
2858
2858
  if (effectiveNamespace !== 'all') {
2859
2859
  whereClauses.push('namespace = ?');
@@ -2980,8 +2980,14 @@ export async function listEntries(options) {
2980
2980
  if (!fs.existsSync(dbPath)) {
2981
2981
  return { success: false, entries: [], total: 0, error: 'Database not found' };
2982
2982
  }
2983
+ // Listing can migrate/backfill the schema, so it is also a whole-image
2984
+ // writer. The newly selected native mirror may still have a live WAL.
2985
+ await releaseOwnNativeHandle(dbPath);
2986
+ if (hasNativeWalSidecars(dbPath)) {
2987
+ return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
2988
+ }
2983
2989
  // Ensure schema has all required columns (migration for older DBs)
2984
- await ensureSchemaColumns(dbPath);
2990
+ await ensureSchemaColumns(dbPath, options);
2985
2991
  const initSqlJs = (await import('sql.js')).default;
2986
2992
  const SQL = await initSqlJs();
2987
2993
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -2990,7 +2996,7 @@ export async function listEntries(options) {
2990
2996
  // that predate the status column may have NULL after migration.
2991
2997
  // See memory-bridge.ts:bridgeListEntries for full context.
2992
2998
  // Get total count
2993
- const whereClauses = [ACTIVE_MEMORY_ROW_SQL];
2999
+ const whereClauses = [liveMemoryRowSql()];
2994
3000
  const whereParams = [];
2995
3001
  if (namespace) {
2996
3002
  whereClauses.push('namespace = ?');
@@ -3088,7 +3094,7 @@ export async function getEntry(options) {
3088
3094
  return {
3089
3095
  success: false,
3090
3096
  found: false,
3091
- error: await walRefusalError('read/write'),
3097
+ error: await walRefusalError('read/write', options.dbPath),
3092
3098
  };
3093
3099
  }
3094
3100
  // #2878: this is a mutator, not a reader — the access_count bump below
@@ -3107,7 +3113,7 @@ export async function getEntry(options) {
3107
3113
  const getStmt = db.prepare(`
3108
3114
  SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
3109
3115
  FROM memory_entries
3110
- WHERE ${ACTIVE_MEMORY_ROW_SQL}
3116
+ WHERE ${liveMemoryRowSql()}
3111
3117
  AND key = ?
3112
3118
  AND namespace = ?
3113
3119
  LIMIT 1
@@ -3211,14 +3217,14 @@ export async function deleteEntry(options) {
3211
3217
  key,
3212
3218
  namespace,
3213
3219
  remainingEntries: 0,
3214
- error: await walRefusalError('write'),
3220
+ error: await walRefusalError('write', options.dbPath),
3215
3221
  };
3216
3222
  }
3217
3223
  // #2878: whole-image read-modify-write — without the lock a concurrent
3218
3224
  // writer's flush resurrects the row this call just tombstoned.
3219
3225
  return await withMemoryDbLock(dbPath, async () => {
3220
3226
  // Ensure schema has all required columns (migration for older DBs)
3221
- await ensureSchemaColumns(dbPath);
3227
+ await ensureSchemaColumns(dbPath, options);
3222
3228
  const initSqlJs = (await import('sql.js')).default;
3223
3229
  const SQL = await initSqlJs();
3224
3230
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -3268,7 +3274,7 @@ export async function deleteEntry(options) {
3268
3274
  const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
3269
3275
  // Save updated database
3270
3276
  const data = db.export();
3271
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
3277
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
3272
3278
  db.close();
3273
3279
  // Clean up in-memory HNSW index so ghost vectors don't appear in searches.
3274
3280
  // Remove the entry from the HNSW entries map and invalidate the index.
@@ -3387,11 +3393,13 @@ export async function withMemoryDbLock(dbPath, fn) {
3387
3393
  }
3388
3394
  }
3389
3395
  }
3390
- const NAMESPACE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
3391
3396
  export async function purgeNamespace(options) {
3392
3397
  const { namespace, dbPath: customPath } = options;
3393
- if (!NAMESPACE_PATTERN.test(namespace)) {
3394
- return { success: false, deletedCount: 0, remainingEntries: 0, error: `Invalid namespace: ${namespace}` };
3398
+ // #3570: the same validator store, import and export use, so any namespace
3399
+ // that can be written can also be purged (`team:alice` included).
3400
+ const vNs = validateIdentifier(namespace, 'namespace');
3401
+ if (!vNs.valid) {
3402
+ return { success: false, deletedCount: 0, remainingEntries: 0, error: `Invalid namespace: ${vNs.error}` };
3395
3403
  }
3396
3404
  const swarmDir = getMemoryRoot();
3397
3405
  const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
@@ -3420,7 +3428,12 @@ export async function purgeNamespace(options) {
3420
3428
  if (!fs.existsSync(dbPath)) {
3421
3429
  return { success: false, deletedCount: 0, remainingEntries: 0, error: 'Database not found' };
3422
3430
  }
3423
- await ensureSchemaColumns(dbPath);
3431
+ // Recheck at mutation time even when the CLI already read a preview.
3432
+ await releaseOwnNativeHandle(dbPath);
3433
+ if (hasNativeWalSidecars(dbPath)) {
3434
+ return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
3435
+ }
3436
+ await ensureSchemaColumns(dbPath, options);
3424
3437
  const initSqlJs = (await import('sql.js')).default;
3425
3438
  const SQL = await initSqlJs();
3426
3439
  const fileBuffer = readFileMaybeEncrypted(dbPath, null);
@@ -3431,7 +3444,7 @@ export async function purgeNamespace(options) {
3431
3444
  const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
3432
3445
  const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
3433
3446
  const data = db.export();
3434
- writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
3447
+ writeFileRestricted(dbPath, Buffer.from(data), { encrypt: options.encryptWrites ?? true });
3435
3448
  db.close();
3436
3449
  if (deletedCount > 0 && hnswIndex?.entries) {
3437
3450
  for (const [id, entry] of hnswIndex.entries) {
@@ -3,6 +3,7 @@
3
3
  * Handles actual plugin installation, persistence, and lifecycle
4
4
  * Bridges discovery service with file system persistence
5
5
  */
6
+ import { type TrustDecision } from './trust-policy.js';
6
7
  export interface InstalledPlugin {
7
8
  name: string;
8
9
  version: string;
@@ -13,6 +14,40 @@ export interface InstalledPlugin {
13
14
  commands?: string[];
14
15
  hooks?: string[];
15
16
  config?: Record<string, unknown>;
17
+ /** Declared (or registry-assigned) trust level, recorded at install time. */
18
+ trustLevel?: string;
19
+ /** Declared permissions, recorded at install time so they can be enforced. */
20
+ permissions?: string[];
21
+ /** How the install was verified. */
22
+ verification?: 'checksum' | 'npm-integrity' | 'policy' | 'skipped';
23
+ /** Whether npm lifecycle scripts ran during install (false = `--ignore-scripts`). */
24
+ scriptsRun?: boolean;
25
+ /** Hooks/commands the plugin declared but that were not registered, and why. */
26
+ withheld?: {
27
+ hooks: string[];
28
+ commands: string[];
29
+ reasons: string[];
30
+ };
31
+ }
32
+ /** Options for {@link PluginManager.installFromLocal} / {@link PluginManager.installFromNpm}. */
33
+ export interface PluginInstallOptions {
34
+ /** `--verify` (default true). */
35
+ verify?: boolean;
36
+ /** `--trust` (default false). */
37
+ trust?: boolean;
38
+ /** Registry checksum (`sha256:<hex>`) the downloaded tarball must match. */
39
+ expectedChecksum?: string;
40
+ /** Trust level from the registry entry, when the plugin was found there. */
41
+ registryTrustLevel?: string;
42
+ /** Permissions from the registry entry, merged with the package's own declaration. */
43
+ registryPermissions?: string[];
44
+ }
45
+ export interface PluginInstallResult {
46
+ success: boolean;
47
+ error?: string;
48
+ plugin?: InstalledPlugin;
49
+ decision?: TrustDecision;
50
+ warnings?: string[];
16
51
  }
17
52
  export interface InstalledPluginsManifest {
18
53
  version: '1.0.0';
@@ -46,19 +81,11 @@ export declare class PluginManager {
46
81
  /**
47
82
  * Install a plugin from npm
48
83
  */
49
- installFromNpm(packageName: string, version?: string): Promise<{
50
- success: boolean;
51
- error?: string;
52
- plugin?: InstalledPlugin;
53
- }>;
84
+ installFromNpm(packageName: string, version?: string, opts?: PluginInstallOptions): Promise<PluginInstallResult>;
54
85
  /**
55
86
  * Install a plugin from a local path
56
87
  */
57
- installFromLocal(sourcePath: string): Promise<{
58
- success: boolean;
59
- error?: string;
60
- plugin?: InstalledPlugin;
61
- }>;
88
+ installFromLocal(sourcePath: string, opts?: PluginInstallOptions): Promise<PluginInstallResult>;
62
89
  /**
63
90
  * Uninstall a plugin
64
91
  */
@@ -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