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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module v3/cli/mcp-tools/memory-tools
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join, resolve } from 'path';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
@@ -42,7 +42,15 @@ const MAX_QUERY_LENGTH = 4096;
|
|
|
42
42
|
// validateMemoryInput. Imported by sanitizeMemoryKey so write-side sanitization
|
|
43
43
|
// and read-side rejection can never drift apart (the symmetry bug behind #1884).
|
|
44
44
|
const DANGEROUS_KEY_CHARS = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/g;
|
|
45
|
-
const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
45
|
+
export const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
46
|
+
/**
|
|
47
|
+
* #3570: the one key rule for every memory write path (MCP store, CLI store,
|
|
48
|
+
* import). Plain `/` stays legal (`probe/x`); traversal and shell metacharacters
|
|
49
|
+
* do not. Returns the error message, or null when the key is acceptable.
|
|
50
|
+
*/
|
|
51
|
+
export function memoryKeyError(key) {
|
|
52
|
+
return DANGEROUS_KEY_PATTERN.test(key) ? 'Key contains disallowed characters' : null;
|
|
53
|
+
}
|
|
46
54
|
function validateMemoryInput(key, value, query, namespace) {
|
|
47
55
|
if (key && key.length > MAX_KEY_LENGTH) {
|
|
48
56
|
throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);
|
|
@@ -54,9 +62,9 @@ function validateMemoryInput(key, value, query, namespace) {
|
|
|
54
62
|
throw new Error(`Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`);
|
|
55
63
|
}
|
|
56
64
|
// Reject path traversal and shell metacharacters in keys/namespaces (#1425)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const keyError = key ? memoryKeyError(key) : null;
|
|
66
|
+
if (keyError)
|
|
67
|
+
throw new Error(keyError);
|
|
60
68
|
if (namespace && DANGEROUS_KEY_PATTERN.test(namespace)) {
|
|
61
69
|
throw new Error('Namespace contains disallowed characters');
|
|
62
70
|
}
|
|
@@ -273,11 +281,11 @@ const MEMORY_STATS_MAX_PAGES = 200;
|
|
|
273
281
|
* breakdown and embedding coverage computed from a prefix, and a listing
|
|
274
282
|
* that reported `success: false` was read as an empty one.
|
|
275
283
|
*/
|
|
276
|
-
async function collectAllEntries(listEntries) {
|
|
284
|
+
async function collectAllEntries(listEntries, dbPath) {
|
|
277
285
|
const entries = [];
|
|
278
286
|
let total = 0;
|
|
279
287
|
for (let page = 0; page < MEMORY_STATS_MAX_PAGES; page++) {
|
|
280
|
-
const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length });
|
|
288
|
+
const result = await listEntries({ limit: MEMORY_STATS_PAGE, offset: entries.length, dbPath });
|
|
281
289
|
if (!result.success) {
|
|
282
290
|
return { success: false, entries, total: result.total ?? total, error: result.error };
|
|
283
291
|
}
|
|
@@ -300,10 +308,10 @@ function memoryStatsUnavailable(error) {
|
|
|
300
308
|
* WAL frames, so it is allowed to fail without that meaning the store is
|
|
301
309
|
* missing — which is exactly the conflation #3311 reports.
|
|
302
310
|
*/
|
|
303
|
-
async function readMemoryStatusLabels() {
|
|
311
|
+
async function readMemoryStatusLabels(dbPath) {
|
|
304
312
|
try {
|
|
305
313
|
const { checkMemoryInitialization } = await getMemoryFunctions();
|
|
306
|
-
const status = await checkMemoryInitialization();
|
|
314
|
+
const status = await checkMemoryInitialization(dbPath);
|
|
307
315
|
return { version: status.version, features: status.features };
|
|
308
316
|
}
|
|
309
317
|
catch {
|
|
@@ -315,37 +323,43 @@ async function readMemoryStatusLabels() {
|
|
|
315
323
|
* #1606: Wrapped in try/catch to prevent process-level crashes that kill
|
|
316
324
|
* the stdio MCP transport on Windows/Codex.
|
|
317
325
|
*/
|
|
318
|
-
async function ensureInitialized() {
|
|
326
|
+
async function ensureInitialized(dbPath) {
|
|
319
327
|
try {
|
|
320
328
|
const { initializeMemoryDatabase, checkMemoryInitialization, storeEntry } = await getMemoryFunctions();
|
|
321
329
|
// Check if already initialized
|
|
322
|
-
const status = await checkMemoryInitialization();
|
|
330
|
+
const status = await checkMemoryInitialization(dbPath);
|
|
323
331
|
if (!status.initialized) {
|
|
324
|
-
await initializeMemoryDatabase({ force: false, verbose: false });
|
|
332
|
+
await initializeMemoryDatabase({ force: false, verbose: false, dbPath });
|
|
325
333
|
}
|
|
326
334
|
// Migrate legacy JSON data if exists (from old .claude-flow/memory/ location)
|
|
335
|
+
// A custom import target must not consume the one-time migration marker
|
|
336
|
+
// for the project's normal memory store.
|
|
327
337
|
if (hasLegacyStore()) {
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
338
|
+
const { resolveDbPath } = await import('../memory/memory-initializer.js');
|
|
339
|
+
if (!dbPath || dbPath === resolveDbPath()) {
|
|
340
|
+
const legacyStore = loadLegacyStore();
|
|
341
|
+
if (legacyStore && Object.keys(legacyStore.entries).length > 0) {
|
|
342
|
+
console.error('[MCP Memory] Migrating legacy JSON store to sql.js...');
|
|
343
|
+
let migrated = 0;
|
|
344
|
+
for (const [key, entry] of Object.entries(legacyStore.entries)) {
|
|
345
|
+
try {
|
|
346
|
+
const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value);
|
|
347
|
+
await storeEntry({
|
|
348
|
+
key,
|
|
349
|
+
value,
|
|
350
|
+
namespace: 'default',
|
|
351
|
+
generateEmbeddingFlag: true,
|
|
352
|
+
dbPath,
|
|
353
|
+
});
|
|
354
|
+
migrated++;
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
console.error(`[MCP Memory] Failed to migrate key "${key}":`, e);
|
|
358
|
+
}
|
|
345
359
|
}
|
|
360
|
+
console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
|
|
361
|
+
markMigrationComplete();
|
|
346
362
|
}
|
|
347
|
-
console.error(`[MCP Memory] Migrated ${migrated}/${Object.keys(legacyStore.entries).length} entries`);
|
|
348
|
-
markMigrationComplete();
|
|
349
363
|
}
|
|
350
364
|
}
|
|
351
365
|
}
|
|
@@ -407,6 +421,11 @@ export const memoryTools = [
|
|
|
407
421
|
};
|
|
408
422
|
}
|
|
409
423
|
validateMemoryInput(key, value, undefined, namespace);
|
|
424
|
+
// #3570: a namespace written here must be exportable and purgeable, so it
|
|
425
|
+
// passes the same validator export and purge use.
|
|
426
|
+
const vNs = validateIdentifier(namespace, 'namespace');
|
|
427
|
+
if (!vNs.valid)
|
|
428
|
+
throw new Error(vNs.error);
|
|
410
429
|
const startTime = performance.now();
|
|
411
430
|
try {
|
|
412
431
|
const result = await storeEntry({
|
|
@@ -792,10 +811,18 @@ export const memoryTools = [
|
|
|
792
811
|
category: 'memory',
|
|
793
812
|
inputSchema: {
|
|
794
813
|
type: 'object',
|
|
795
|
-
properties: {
|
|
814
|
+
properties: {
|
|
815
|
+
dbPath: { type: 'string', description: 'Database file to inspect; omitted uses the MCP store default' },
|
|
816
|
+
},
|
|
796
817
|
},
|
|
797
|
-
handler: async () => {
|
|
798
|
-
|
|
818
|
+
handler: async (input) => {
|
|
819
|
+
if (input.dbPath !== undefined && (typeof input.dbPath !== 'string' || !input.dbPath.trim())) {
|
|
820
|
+
return memoryStatsUnavailable('dbPath must be a non-empty string');
|
|
821
|
+
}
|
|
822
|
+
const dbPath = typeof input.dbPath === 'string' ? resolve(input.dbPath) : undefined;
|
|
823
|
+
// An explicit read must not initialize or migrate the unrelated default store.
|
|
824
|
+
if (!dbPath)
|
|
825
|
+
await ensureInitialized();
|
|
799
826
|
const { listEntries } = await getMemoryFunctions();
|
|
800
827
|
// #3311: the store's own listing decides whether memory is there.
|
|
801
828
|
// `checkMemoryInitialization` opens a whole-image sql.js snapshot of
|
|
@@ -806,7 +833,7 @@ export const memoryTools = [
|
|
|
806
833
|
// but it no longer gets to overrule a working store.
|
|
807
834
|
let listing;
|
|
808
835
|
try {
|
|
809
|
-
listing = await collectAllEntries(listEntries);
|
|
836
|
+
listing = await collectAllEntries(listEntries, dbPath);
|
|
810
837
|
}
|
|
811
838
|
catch (error) {
|
|
812
839
|
return memoryStatsUnavailable(error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -822,16 +849,49 @@ export const memoryTools = [
|
|
|
822
849
|
// from the breakdown while still being counted in the total.
|
|
823
850
|
const namespaces = Object.create(null);
|
|
824
851
|
let withEmbeddings = 0;
|
|
852
|
+
let oldest = Infinity;
|
|
853
|
+
let newest = -Infinity;
|
|
825
854
|
for (const entry of listing.entries) {
|
|
826
855
|
namespaces[entry.namespace] = (namespaces[entry.namespace] || 0) + 1;
|
|
827
856
|
if (entry.hasEmbedding)
|
|
828
857
|
withEmbeddings++;
|
|
858
|
+
// AgentDB returns INTEGER epoch milliseconds; legacy sql.js rows
|
|
859
|
+
// may expose ISO strings or numeric strings from SQLite TEXT affinity.
|
|
860
|
+
const rawCreated = entry.createdAt;
|
|
861
|
+
const millis = typeof rawCreated === 'number'
|
|
862
|
+
? rawCreated
|
|
863
|
+
: typeof rawCreated === 'string' && /^-?\d+$/.test(rawCreated)
|
|
864
|
+
? Number(rawCreated)
|
|
865
|
+
: Date.parse(rawCreated ?? '');
|
|
866
|
+
const created = new Date(millis).getTime();
|
|
867
|
+
if (Number.isFinite(created)) {
|
|
868
|
+
oldest = Math.min(oldest, created);
|
|
869
|
+
newest = Math.max(newest, created);
|
|
870
|
+
}
|
|
829
871
|
}
|
|
830
872
|
const counted = listing.entries.length;
|
|
831
|
-
const status = await readMemoryStatusLabels();
|
|
873
|
+
const status = await readMemoryStatusLabels(dbPath);
|
|
874
|
+
let totalSize = null;
|
|
875
|
+
if (dbPath) {
|
|
876
|
+
try {
|
|
877
|
+
let bytes = statSync(dbPath).size;
|
|
878
|
+
try {
|
|
879
|
+
bytes += statSync(dbPath + '-wal').size;
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (error.code !== 'ENOENT')
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
totalSize = `${bytes} B`;
|
|
886
|
+
}
|
|
887
|
+
catch { /* unavailable file metadata is unknown, not zero */ }
|
|
888
|
+
}
|
|
832
889
|
return {
|
|
833
890
|
initialized: true,
|
|
834
891
|
totalEntries: listing.total,
|
|
892
|
+
...(dbPath ? { location: dbPath, totalSize } : {}),
|
|
893
|
+
oldestEntry: counted >= listing.total && Number.isFinite(oldest) ? new Date(oldest).toISOString() : null,
|
|
894
|
+
newestEntry: counted >= listing.total && Number.isFinite(newest) ? new Date(newest).toISOString() : null,
|
|
835
895
|
entriesCounted: counted,
|
|
836
896
|
// The breakdown below covers `entriesCounted` rows, which is every
|
|
837
897
|
// row unless the listing was truncated; say so rather than letting
|
|
@@ -1484,13 +1544,20 @@ export const memoryTools = [
|
|
|
1484
1544
|
inputPath: { type: 'string', description: 'Path to the JSON export file' },
|
|
1485
1545
|
merge: { type: 'boolean', description: 'Merge into existing entries (upsert) vs. fail on conflict (default true)' },
|
|
1486
1546
|
namespace: { type: 'string', description: 'Override the namespace for all imported entries' },
|
|
1547
|
+
dbPath: { type: 'string', description: 'Database file to import into (defaults to the MCP memory store)' },
|
|
1487
1548
|
},
|
|
1488
1549
|
required: ['inputPath'],
|
|
1489
1550
|
},
|
|
1490
1551
|
handler: async (input) => {
|
|
1491
|
-
|
|
1552
|
+
const dbPath = typeof input.dbPath === 'string' && input.dbPath.trim()
|
|
1553
|
+
? resolve(input.dbPath)
|
|
1554
|
+
: undefined;
|
|
1555
|
+
await ensureInitialized(dbPath);
|
|
1492
1556
|
const { storeEntry } = await getMemoryFunctions();
|
|
1493
1557
|
const t0 = Date.now();
|
|
1558
|
+
// Values are re-embedded on import; count the vectors actually written
|
|
1559
|
+
// rather than reporting a constant 0 next to entries that show a vector.
|
|
1560
|
+
let vectors = 0;
|
|
1494
1561
|
const inputPath = String(input.inputPath ?? '');
|
|
1495
1562
|
if (!inputPath || !existsSync(inputPath))
|
|
1496
1563
|
return { error: `File not found: ${inputPath || '(empty)'}` };
|
|
@@ -1508,6 +1575,24 @@ export const memoryTools = [
|
|
|
1508
1575
|
if (!v.valid)
|
|
1509
1576
|
throw new Error(v.error);
|
|
1510
1577
|
}
|
|
1578
|
+
// #3570: validate every entry's namespace up front so a bad file writes nothing.
|
|
1579
|
+
if (!nsOverride) {
|
|
1580
|
+
for (const e of entries) {
|
|
1581
|
+
if (e && typeof e.key === 'string' && e.namespace !== undefined) {
|
|
1582
|
+
const v = validateIdentifier(String(e.namespace), 'namespace');
|
|
1583
|
+
if (!v.valid)
|
|
1584
|
+
throw new Error(v.error);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// #3570 follow-up: keys get the same up-front, all-or-nothing check.
|
|
1589
|
+
for (const e of entries) {
|
|
1590
|
+
if (e && typeof e.key === 'string') {
|
|
1591
|
+
const keyError = memoryKeyError(e.key);
|
|
1592
|
+
if (keyError)
|
|
1593
|
+
throw new Error(`${keyError}: ${JSON.stringify(e.key)}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1511
1596
|
let imported = 0;
|
|
1512
1597
|
let skipped = 0;
|
|
1513
1598
|
for (const e of entries) {
|
|
@@ -1517,8 +1602,14 @@ export const memoryTools = [
|
|
|
1517
1602
|
}
|
|
1518
1603
|
const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
|
|
1519
1604
|
try {
|
|
1520
|
-
await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false });
|
|
1521
|
-
|
|
1605
|
+
const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
|
|
1606
|
+
if (result.success) {
|
|
1607
|
+
imported++;
|
|
1608
|
+
if (result.embedding)
|
|
1609
|
+
vectors++;
|
|
1610
|
+
}
|
|
1611
|
+
else
|
|
1612
|
+
skipped++;
|
|
1522
1613
|
}
|
|
1523
1614
|
catch {
|
|
1524
1615
|
skipped++;
|
|
@@ -1526,7 +1617,7 @@ export const memoryTools = [
|
|
|
1526
1617
|
}
|
|
1527
1618
|
return {
|
|
1528
1619
|
inputPath,
|
|
1529
|
-
imported: { entries: imported, vectors
|
|
1620
|
+
imported: { entries: imported, vectors, patterns: 0 },
|
|
1530
1621
|
skipped,
|
|
1531
1622
|
duration: Date.now() - t0,
|
|
1532
1623
|
};
|
|
@@ -62,6 +62,12 @@ export interface McpPolicy {
|
|
|
62
62
|
harnessId?: string;
|
|
63
63
|
defaultDeny?: boolean;
|
|
64
64
|
auditLog?: boolean;
|
|
65
|
+
/** Explicit destination; takes precedence over the environment and project default. */
|
|
66
|
+
auditLogPath?: string;
|
|
67
|
+
/** Maximum bytes per audit segment. Default 10 MiB; oversized records fail closed. */
|
|
68
|
+
auditLogMaxBytes?: number;
|
|
69
|
+
/** Rotated segments retained in addition to the active log. Default 5, maximum 100. */
|
|
70
|
+
auditLogMaxFiles?: number;
|
|
65
71
|
requireApprovalForDangerous?: boolean;
|
|
66
72
|
toolTimeoutMs?: number;
|
|
67
73
|
/**
|
|
@@ -103,10 +109,11 @@ export interface AuditLogEntry {
|
|
|
103
109
|
toolName: string;
|
|
104
110
|
allowed: boolean;
|
|
105
111
|
reason?: string;
|
|
112
|
+
projectPath?: string;
|
|
106
113
|
}
|
|
107
114
|
/** Test-only: redirect the audit log to a temp file instead of the default path. */
|
|
108
115
|
export declare function setAuditLogPathForTesting(p: string | null): void;
|
|
109
|
-
export declare function getAuditLogPath(): string;
|
|
116
|
+
export declare function getAuditLogPath(options?: Pick<McpPolicy, 'auditLogPath'>): string;
|
|
110
117
|
/**
|
|
111
118
|
* Appends one JSONL audit record. Returns `true` if `policy.auditLog` is not
|
|
112
119
|
* set (nothing was required) or the write succeeded; `false` only when
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
*/
|
|
59
59
|
import * as fs from 'fs';
|
|
60
60
|
import * as path from 'path';
|
|
61
|
-
import * as os from 'os';
|
|
62
61
|
export function isPolicyEnforcementEnabled(env = process.env) {
|
|
63
62
|
const v = env.RUFLO_MCP_ENFORCE_POLICY;
|
|
64
63
|
return v === '1' || (v ?? '').toLowerCase() === 'true';
|
|
@@ -118,11 +117,18 @@ let auditLogPathOverride = null;
|
|
|
118
117
|
export function setAuditLogPathForTesting(p) {
|
|
119
118
|
auditLogPathOverride = p;
|
|
120
119
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
export function getAuditLogPath(options = {}) {
|
|
121
|
+
if (auditLogPathOverride !== null)
|
|
122
|
+
return auditLogPathOverride;
|
|
123
|
+
if (options.auditLogPath !== undefined &&
|
|
124
|
+
(typeof options.auditLogPath !== 'string' || options.auditLogPath.trim().length === 0)) {
|
|
125
|
+
throw new Error('auditLogPath must be a non-empty string');
|
|
126
|
+
}
|
|
127
|
+
// Precedence: explicit caller/policy destination, environment, project default.
|
|
128
|
+
const configured = options.auditLogPath ?? process.env.RUFLO_MCP_AUDIT_LOG_PATH;
|
|
129
|
+
return configured
|
|
130
|
+
? path.resolve(process.cwd(), configured)
|
|
131
|
+
: path.join(process.cwd(), '.claude-flow', 'logs', 'mcp-audit.jsonl');
|
|
126
132
|
}
|
|
127
133
|
/**
|
|
128
134
|
* Appends one JSONL audit record. Returns `true` if `policy.auditLog` is not
|
|
@@ -135,8 +141,74 @@ export function appendAuditLog(policy, entry) {
|
|
|
135
141
|
if (!policy.auditLog)
|
|
136
142
|
return true;
|
|
137
143
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
const maxBytes = policy.auditLogMaxBytes ?? 10 * 1024 * 1024;
|
|
145
|
+
const maxFiles = policy.auditLogMaxFiles ?? 5;
|
|
146
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 ||
|
|
147
|
+
!Number.isSafeInteger(maxFiles) || maxFiles < 1 || maxFiles > 100)
|
|
148
|
+
return false;
|
|
149
|
+
const line = `${JSON.stringify({ ...entry, projectPath: path.resolve(process.cwd()) })}\n`;
|
|
150
|
+
const bytes = Buffer.byteLength(line, 'utf8');
|
|
151
|
+
if (bytes > maxBytes)
|
|
152
|
+
return false;
|
|
153
|
+
const logPath = getAuditLogPath(policy);
|
|
154
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true, mode: 0o700 });
|
|
155
|
+
// All writers of a configured path serialize rotation AND append. On
|
|
156
|
+
// contention (or a stale lock after a crash), deny rather than lose audit
|
|
157
|
+
// evidence by racing a rename. Only the lock owner removes its lock.
|
|
158
|
+
const lockPath = `${logPath}.lock`;
|
|
159
|
+
const lock = fs.openSync(lockPath, 'wx', 0o600);
|
|
160
|
+
try {
|
|
161
|
+
let size = 0;
|
|
162
|
+
try {
|
|
163
|
+
const stat = fs.lstatSync(logPath);
|
|
164
|
+
if (!stat.isFile())
|
|
165
|
+
throw new Error('Audit log is not a regular file');
|
|
166
|
+
size = stat.size;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code !== 'ENOENT')
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
if (size + bytes > maxBytes) {
|
|
173
|
+
// Validate the entire chain before moving or expiring any evidence.
|
|
174
|
+
for (let index = 1; index <= maxFiles; index++) {
|
|
175
|
+
try {
|
|
176
|
+
if (!fs.lstatSync(`${logPath}.${index}`).isFile())
|
|
177
|
+
throw new Error('Audit segment is not a regular file');
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code !== 'ENOENT')
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (let index = maxFiles; index >= 1; index--) {
|
|
185
|
+
const source = index === 1 ? logPath : `${logPath}.${index - 1}`;
|
|
186
|
+
const target = `${logPath}.${index}`;
|
|
187
|
+
if (index === maxFiles) {
|
|
188
|
+
try {
|
|
189
|
+
fs.unlinkSync(target);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error.code !== 'ENOENT')
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
fs.renameSync(source, target);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error.code !== 'ENOENT')
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
fs.appendFileSync(logPath, line, { encoding: 'utf8', mode: 0o600 });
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
fs.closeSync(lock);
|
|
210
|
+
fs.unlinkSync(lockPath);
|
|
211
|
+
}
|
|
140
212
|
}
|
|
141
213
|
catch {
|
|
142
214
|
return false;
|
|
@@ -4,5 +4,20 @@
|
|
|
4
4
|
* Tool definitions for session management with file persistence.
|
|
5
5
|
*/
|
|
6
6
|
import { type MCPTool } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* #3573: what session_save learned about memory, so callers can say
|
|
9
|
+
* "not included" or "no store" instead of printing an invented 0.
|
|
10
|
+
*/
|
|
11
|
+
export interface MemoryCaptureReport {
|
|
12
|
+
requested: boolean;
|
|
13
|
+
status: 'not-requested' | 'captured' | 'no-store' | 'error';
|
|
14
|
+
entries: number;
|
|
15
|
+
sources: {
|
|
16
|
+
memoryDb: number;
|
|
17
|
+
agentdb: number;
|
|
18
|
+
legacyJson: number;
|
|
19
|
+
};
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
7
22
|
export declare const sessionTools: MCPTool[];
|
|
8
23
|
//# sourceMappingURL=session-tools.d.ts.map
|