claude-flow 3.47.1 → 3.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/auto-memory-hook.mjs +6 -2
- package/.claude/helpers/hook-handler.cjs +7 -4
- package/.claude/helpers/learning-service.mjs +4 -2
- package/.claude/helpers/memory.cjs +1 -1
- package/.claude/helpers/metrics-db.mjs +4 -2
- package/.claude/helpers/router.cjs +1 -1
- package/.claude/helpers/session.cjs +1 -1
- package/.claude/proven-config.json +42 -0
- package/.claude-plugin/marketplace.json +16 -1
- package/README.md +1 -53
- package/README.zh-CN.md +1 -53
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +6 -3
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +2 -3
- package/package.json +7 -7
- package/v3/@claude-flow/cli/README.md +1 -53
- package/v3/@claude-flow/cli/bin/cli.js +4 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
- package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
- package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
- package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
- package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
- package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
- package/v3/@claude-flow/cli/dist/src/index.js +10 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
- package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
- package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
- package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
- package/v3/@claude-flow/cli/package.json +9 -9
- package/v3/@claude-flow/guidance/package.json +5 -6
- package/v3/@claude-flow/shared/package.json +6 -3
|
@@ -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
|
|
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
|
|
1316
|
-
if (!
|
|
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 (
|
|
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 =
|
|
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(
|
|
2755
|
-
stmt.bind([candidate.id
|
|
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(
|
|
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(
|
|
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
|
-
.
|
|
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
|
-
//
|
|
2831
|
-
//
|
|
2832
|
-
filtered =
|
|
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 (
|
|
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 = [
|
|
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 = [
|
|
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 ${
|
|
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
|
-
|
|
3394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
let hooks = [];
|
|
192
|
+
let pkg = {};
|
|
127
193
|
if (fs.existsSync(packageJsonPath)) {
|
|
128
|
-
|
|
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:
|
|
194
|
-
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
|
-
|
|
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
|