@claude-flow/cli 3.55.0 → 3.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/.claude/settings.json +9 -9
- package/catalog-manifest.json +2 -2
- package/dist/src/auth/client.js +40 -24
- package/dist/src/commands/agent.js +12 -0
- package/dist/src/commands/analyze.js +18 -14
- package/dist/src/commands/config.js +1 -1
- package/dist/src/commands/daemon.js +11 -19
- package/dist/src/commands/doctor.js +65 -58
- package/dist/src/commands/memory-backup.d.ts +1 -1
- package/dist/src/commands/memory-backup.js +23 -9
- package/dist/src/commands/memory.d.ts +0 -4
- package/dist/src/commands/memory.js +10 -2
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/commands/task.js +12 -0
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-tools/agent-execute-core.js +244 -188
- package/dist/src/mcp-tools/agent-tools.js +15 -16
- package/dist/src/mcp-tools/agentdb-tools.js +22 -3
- package/dist/src/mcp-tools/claims-tools.js +3 -11
- package/dist/src/mcp-tools/config-tools.js +118 -13
- package/dist/src/mcp-tools/hooks-tools.js +47 -2
- package/dist/src/mcp-tools/memory-tools.js +29 -10
- package/dist/src/mcp-tools/record-store.d.ts +3 -0
- package/dist/src/mcp-tools/record-store.js +21 -0
- package/dist/src/mcp-tools/session-tools.js +47 -13
- package/dist/src/mcp-tools/task-tools.js +5 -11
- package/dist/src/mcp-tools/workflow-tools.js +78 -29
- package/dist/src/memory/append-conditions.d.ts +28 -0
- package/dist/src/memory/append-conditions.js +56 -0
- package/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/dist/src/memory/graph-edge-writer.js +90 -0
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +50 -9
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +100 -62
- package/dist/src/memory/memory-root.d.ts +2 -0
- package/dist/src/memory/memory-root.js +28 -0
- package/dist/src/memory/sona-optimizer.d.ts +2 -0
- package/dist/src/memory/sona-optimizer.js +4 -0
- package/dist/src/parser.js +7 -0
- package/dist/src/plugins/store/discovery.d.ts +6 -0
- package/dist/src/plugins/store/discovery.js +7 -3
- package/dist/src/ruvector/neural-router.js +7 -2
- package/dist/src/services/claim-service.js +39 -18
- package/dist/src/services/claims-store.d.ts +3 -0
- package/dist/src/services/claims-store.js +40 -0
- package/dist/src/services/config-file-manager.d.ts +2 -2
- package/dist/src/services/config-file-manager.js +22 -3
- package/dist/src/services/distill-tuning.js +17 -10
- package/dist/src/services/memory-backup.d.ts +7 -0
- package/dist/src/services/memory-backup.js +29 -2
- package/dist/src/services/memory-distillation.js +2 -2
- package/dist/src/services/policy-ledger-anchor.d.ts +8 -0
- package/dist/src/services/policy-ledger-anchor.js +18 -3
- package/dist/src/services/policy-runtime.js +2 -2
- package/dist/src/services/worker-daemon.d.ts +4 -2
- package/dist/src/services/worker-daemon.js +14 -6
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
- package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +4 -4
- package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
- package/.claude/agents/tmp.json +0 -0
|
@@ -41,6 +41,22 @@ function saveWorkflowStore(store) {
|
|
|
41
41
|
ensureWorkflowDir();
|
|
42
42
|
writeFileSync(getWorkflowPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
43
43
|
}
|
|
44
|
+
// A paused in-flight step still owns its executor. Resume joins that execution
|
|
45
|
+
// instead of dispatching the same side effect again (within this process).
|
|
46
|
+
const workflowExecutions = new Map();
|
|
47
|
+
const workflowExecutionKey = (workflowId) => JSON.stringify([getWorkflowPath(), workflowId]);
|
|
48
|
+
function withWorkflowExecution(workflowId, execute) {
|
|
49
|
+
const key = workflowExecutionKey(workflowId);
|
|
50
|
+
if (workflowExecutions.has(key)) {
|
|
51
|
+
return Promise.resolve({ workflowId, error: 'Workflow already executing' });
|
|
52
|
+
}
|
|
53
|
+
const execution = execute();
|
|
54
|
+
workflowExecutions.set(key, execution);
|
|
55
|
+
return execution.finally(() => {
|
|
56
|
+
if (workflowExecutions.get(key) === execution)
|
|
57
|
+
workflowExecutions.delete(key);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
44
60
|
export const workflowTools = [
|
|
45
61
|
{
|
|
46
62
|
name: 'workflow_run',
|
|
@@ -227,7 +243,7 @@ export const workflowTools = [
|
|
|
227
243
|
},
|
|
228
244
|
required: ['workflowId'],
|
|
229
245
|
},
|
|
230
|
-
handler: async (input) => {
|
|
246
|
+
handler: async (input) => withWorkflowExecution(input.workflowId, async () => {
|
|
231
247
|
// Validate user-provided input (#1425)
|
|
232
248
|
const vId = validateIdentifier(input.workflowId, 'workflowId');
|
|
233
249
|
if (!vId.valid)
|
|
@@ -241,13 +257,50 @@ export const workflowTools = [
|
|
|
241
257
|
if (workflow.status === 'running') {
|
|
242
258
|
return { workflowId, error: 'Workflow already running' };
|
|
243
259
|
}
|
|
260
|
+
// Indices may name a step or the end boundary (used when resuming
|
|
261
|
+
// after the final step). Reject invalid control flow before any writes.
|
|
262
|
+
const validIndex = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= workflow.steps.length;
|
|
263
|
+
const startFromStep = input.startFromStep === undefined ? 0 : input.startFromStep;
|
|
264
|
+
if (!validIndex(startFromStep)) {
|
|
265
|
+
return { workflowId, error: `startFromStep must be an integer between 0 and ${workflow.steps.length}` };
|
|
266
|
+
}
|
|
267
|
+
for (const step of workflow.steps) {
|
|
268
|
+
if (step.type !== 'condition')
|
|
269
|
+
continue;
|
|
270
|
+
for (const target of ['thenStep', 'elseStep']) {
|
|
271
|
+
if (step.config[target] !== undefined && !validIndex(step.config[target])) {
|
|
272
|
+
return { workflowId, error: `${step.stepId}.${target} must be an integer between 0 and ${workflow.steps.length}` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const persistProgress = () => {
|
|
277
|
+
// An awaited step may overlap pause/stop/delete or a sibling workflow
|
|
278
|
+
// mutation. Merge only this workflow into the latest store snapshot.
|
|
279
|
+
const latest = loadWorkflowStore();
|
|
280
|
+
const current = latest.workflows[workflowId];
|
|
281
|
+
if (!current) {
|
|
282
|
+
workflow.status = 'failed';
|
|
283
|
+
workflow.error = 'Workflow was deleted during execution';
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (current.status === 'failed') {
|
|
287
|
+
Object.assign(workflow, current);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (current.status === 'paused' && workflow.status !== 'failed')
|
|
291
|
+
workflow.status = 'paused';
|
|
292
|
+
latest.workflows[workflowId] = workflow;
|
|
293
|
+
saveWorkflowStore(latest);
|
|
294
|
+
};
|
|
244
295
|
// Inject runtime variables
|
|
245
296
|
if (input.variables) {
|
|
246
297
|
workflow.variables = { ...workflow.variables, ...input.variables };
|
|
247
298
|
}
|
|
248
299
|
workflow.status = 'running';
|
|
249
300
|
workflow.startedAt = new Date().toISOString();
|
|
250
|
-
workflow.
|
|
301
|
+
delete workflow.error;
|
|
302
|
+
delete workflow.completedAt;
|
|
303
|
+
workflow.currentStep = startFromStep;
|
|
251
304
|
saveWorkflowStore(store);
|
|
252
305
|
// ADR-095 G3: real workflow runtime. Walk the steps in order;
|
|
253
306
|
// dispatch each by type. Persist progress after each step so a
|
|
@@ -277,22 +330,26 @@ export const workflowTools = [
|
|
|
277
330
|
while (i < workflow.steps.length) {
|
|
278
331
|
// Honor pause/cancel signals between steps.
|
|
279
332
|
const live = loadWorkflowStore().workflows[workflowId];
|
|
280
|
-
if (!live
|
|
333
|
+
if (!live) {
|
|
334
|
+
workflow.status = 'failed';
|
|
335
|
+
workflow.error = 'Workflow was deleted during execution';
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (live.status === 'paused') {
|
|
281
339
|
workflow.status = 'paused';
|
|
282
340
|
workflow.currentStep = i;
|
|
283
|
-
|
|
341
|
+
persistProgress();
|
|
284
342
|
break;
|
|
285
343
|
}
|
|
286
344
|
if (live.status === 'failed') {
|
|
287
|
-
workflow
|
|
288
|
-
saveWorkflowStore(store);
|
|
345
|
+
Object.assign(workflow, live);
|
|
289
346
|
break;
|
|
290
347
|
}
|
|
291
348
|
const step = workflow.steps[i];
|
|
292
349
|
step.status = 'running';
|
|
293
350
|
step.startedAt = new Date().toISOString();
|
|
294
351
|
const stepStart = Date.now();
|
|
295
|
-
|
|
352
|
+
persistProgress();
|
|
296
353
|
let stepEntry = { stepId: step.stepId, type: step.type, status: 'running' };
|
|
297
354
|
try {
|
|
298
355
|
if (step.type === 'task') {
|
|
@@ -364,7 +421,7 @@ export const workflowTools = [
|
|
|
364
421
|
workflow.status = 'failed';
|
|
365
422
|
workflow.error = msg;
|
|
366
423
|
workflow.completedAt = new Date().toISOString();
|
|
367
|
-
|
|
424
|
+
persistProgress();
|
|
368
425
|
return {
|
|
369
426
|
workflowId,
|
|
370
427
|
status: 'failed',
|
|
@@ -379,17 +436,18 @@ export const workflowTools = [
|
|
|
379
436
|
stepEntry.durationMs = Date.now() - stepStart;
|
|
380
437
|
stepResults.push(stepEntry);
|
|
381
438
|
workflow.currentStep = i + 1;
|
|
382
|
-
|
|
439
|
+
persistProgress();
|
|
383
440
|
i++;
|
|
384
441
|
}
|
|
385
442
|
if (workflow.status === 'running') {
|
|
386
443
|
workflow.status = 'completed';
|
|
387
444
|
workflow.completedAt = new Date().toISOString();
|
|
388
|
-
|
|
445
|
+
persistProgress();
|
|
389
446
|
}
|
|
390
447
|
return {
|
|
391
448
|
workflowId,
|
|
392
449
|
status: workflow.status,
|
|
450
|
+
...(workflow.error ? { error: workflow.error } : {}),
|
|
393
451
|
totalSteps: workflow.steps.length,
|
|
394
452
|
stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
|
|
395
453
|
stepsSkipped: stepResults.filter(s => s.status === 'skipped').length,
|
|
@@ -399,7 +457,7 @@ export const workflowTools = [
|
|
|
399
457
|
completedAt: workflow.completedAt,
|
|
400
458
|
durationMs: Date.now() - startedAt,
|
|
401
459
|
};
|
|
402
|
-
},
|
|
460
|
+
}),
|
|
403
461
|
},
|
|
404
462
|
{
|
|
405
463
|
name: 'workflow_status',
|
|
@@ -560,24 +618,15 @@ export const workflowTools = [
|
|
|
560
618
|
if (workflow.status !== 'paused') {
|
|
561
619
|
return { workflowId, error: 'Workflow not paused' };
|
|
562
620
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
workflowId,
|
|
574
|
-
status: workflow.status,
|
|
575
|
-
resumed: true,
|
|
576
|
-
currentStep: workflow.currentStep,
|
|
577
|
-
remainingSteps,
|
|
578
|
-
steps: stepStates,
|
|
579
|
-
_note: 'Workflow resumed. Steps remain in their current state and must be executed via task tools.',
|
|
580
|
-
};
|
|
621
|
+
const active = workflowExecutions.get(workflowExecutionKey(workflowId));
|
|
622
|
+
if (active) {
|
|
623
|
+
workflow.status = 'running';
|
|
624
|
+
saveWorkflowStore(store);
|
|
625
|
+
return { ...(await active), resumed: true };
|
|
626
|
+
}
|
|
627
|
+
const execute = workflowTools.find(tool => tool.name === 'workflow_execute');
|
|
628
|
+
const result = await execute.handler({ workflowId, startFromStep: workflow.currentStep });
|
|
629
|
+
return { ...result, resumed: true };
|
|
581
630
|
},
|
|
582
631
|
},
|
|
583
632
|
{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type AppendCondition = {
|
|
2
|
+
namespace: string;
|
|
3
|
+
} & ({
|
|
4
|
+
key: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
latestPrefix?: string;
|
|
7
|
+
} | {
|
|
8
|
+
absent: true;
|
|
9
|
+
latestPrefix: string;
|
|
10
|
+
} | {
|
|
11
|
+
keysSha256: string;
|
|
12
|
+
count: number;
|
|
13
|
+
});
|
|
14
|
+
export declare class AppendConditionFailed extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare function validateAppendConditions(input: unknown): AppendCondition[];
|
|
17
|
+
interface NativeReader {
|
|
18
|
+
prepare(sql: string): {
|
|
19
|
+
all(...parameters: string[]): Array<{
|
|
20
|
+
key: string;
|
|
21
|
+
content: string;
|
|
22
|
+
status: string | null;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function assertAppendConditions(db: NativeReader, conditions: AppendCondition[]): void;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=append-conditions.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Native append predicates checked in the SAME write transaction as an immutable insert. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
export class AppendConditionFailed extends Error {
|
|
4
|
+
}
|
|
5
|
+
const text = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
|
|
6
|
+
const digest = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
7
|
+
export function validateAppendConditions(input) {
|
|
8
|
+
if (!Array.isArray(input) || input.length < 1 || input.length > 8)
|
|
9
|
+
throw new TypeError('append conditions must contain 1-8 predicates');
|
|
10
|
+
return input.map(condition => {
|
|
11
|
+
if (!condition || typeof condition !== 'object' || Array.isArray(condition))
|
|
12
|
+
throw new TypeError('invalid append condition');
|
|
13
|
+
const row = condition;
|
|
14
|
+
const fields = Object.keys(row).sort().join(',');
|
|
15
|
+
const validRecord = ['key,namespace,sha256', 'key,latestPrefix,namespace,sha256'].includes(fields)
|
|
16
|
+
&& text(row.key, 1024) && digest(row.sha256) && (row.latestPrefix === undefined || text(row.latestPrefix, 1024));
|
|
17
|
+
const validAbsent = fields === 'absent,latestPrefix,namespace' && row.absent === true && text(row.latestPrefix, 1024);
|
|
18
|
+
const validKeys = fields === 'count,keysSha256,namespace' && digest(row.keysSha256)
|
|
19
|
+
&& Number.isSafeInteger(row.count) && row.count >= 0 && row.count <= 100_000;
|
|
20
|
+
if (!text(row.namespace, 256) || !(validRecord || validAbsent || validKeys))
|
|
21
|
+
throw new TypeError('invalid append condition');
|
|
22
|
+
return row;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
26
|
+
export function assertAppendConditions(db, conditions) {
|
|
27
|
+
for (const condition of conditions) {
|
|
28
|
+
if ('keysSha256' in condition) {
|
|
29
|
+
const rows = db.prepare("SELECT key FROM memory_entries WHERE namespace = ? AND (status = 'active' OR status IS NULL)")
|
|
30
|
+
.all(condition.namespace);
|
|
31
|
+
const keys = rows.map(row => row.key).sort();
|
|
32
|
+
if (keys.length !== condition.count || sha(JSON.stringify(keys)) !== condition.keysSha256) {
|
|
33
|
+
throw new AppendConditionFailed('immutable append rejected: namespace membership changed');
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if ('key' in condition) {
|
|
38
|
+
const rows = db.prepare('SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key = ?')
|
|
39
|
+
.all(condition.namespace, condition.key);
|
|
40
|
+
if (rows.length !== 1 || !['active', null].includes(rows[0].status)
|
|
41
|
+
|| typeof rows[0].content !== 'string' || sha(rows[0].content) !== condition.sha256) {
|
|
42
|
+
throw new AppendConditionFailed('immutable append rejected: expected parent/authority bytes changed or absent');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (condition.latestPrefix !== undefined) {
|
|
46
|
+
// Include tombstones: deleting a new authority slot must never rewind to an older authority.
|
|
47
|
+
const pattern = condition.latestPrefix.replace(/[\\%_]/g, character => `\\${character}`) + '%';
|
|
48
|
+
const latest = db.prepare("SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key LIKE ? ESCAPE '\\' ORDER BY key COLLATE BINARY DESC LIMIT 1")
|
|
49
|
+
.all(condition.namespace, pattern);
|
|
50
|
+
if ('absent' in condition ? latest.length !== 0 : latest.length !== 1 || latest[0].key !== condition.key) {
|
|
51
|
+
throw new AppendConditionFailed('immutable append rejected: newer parent/authority slot exists');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=append-conditions.js.map
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
*
|
|
30
30
|
* @module v3/cli/memory/graph-edge-writer
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* True iff the memory.db bridge at `customDbPath` (or the default location)
|
|
34
|
+
* is currently RFE1-encrypted-at-rest. Callers that receive `null` from
|
|
35
|
+
* getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
|
|
36
|
+
* destructive `memory init` command for this specific, non-corrupt case.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isBridgeDbEncryptedAtRest(customDbPath?: string): boolean;
|
|
39
|
+
/** Test-only: clear the once-per-path warning dedup set. */
|
|
40
|
+
export declare function _resetRfe1WarningFlag(): void;
|
|
32
41
|
/**
|
|
33
42
|
* Checkpoint and close the cached handle if it is open (optionally only if it
|
|
34
43
|
* is open on `dbPath`). Returns true if a handle was released. Never throws.
|
|
@@ -35,6 +35,87 @@ import * as crypto from 'crypto';
|
|
|
35
35
|
import { getMemoryRoot } from './memory-initializer.js';
|
|
36
36
|
import { encodeEmbedding } from './embedding-quantization.js';
|
|
37
37
|
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
38
|
+
import { isEncryptedBlob } from '../encryption/vault.js';
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// #2889 — RFE1 encryption-at-rest guard
|
|
41
|
+
// ============================================================================
|
|
42
|
+
//
|
|
43
|
+
// When CLAUDE_FLOW_ENCRYPT_AT_REST is on, memory.db is an opaque RFE1 blob
|
|
44
|
+
// (see encryption/vault.ts). `new BetterSqlite3(dbPath)` has no RFE1 sniff
|
|
45
|
+
// and throws SQLITE_NOTADB on it, which the generic catch below swallows —
|
|
46
|
+
// indistinguishable from genuine corruption, a missing native module, or a
|
|
47
|
+
// permissions failure. That ambiguity is what let #2889 happen: nothing
|
|
48
|
+
// stopped a caller from concluding the store was broken and running the
|
|
49
|
+
// destructive `memory init`, which creates a fresh EMPTY store instead of
|
|
50
|
+
// using the existing encrypted data.
|
|
51
|
+
//
|
|
52
|
+
// Fix: sniff the header bytes (never buffer the whole file — memory.db can
|
|
53
|
+
// be multi-GB) before attempting the native open. If it's RFE1-encrypted,
|
|
54
|
+
// skip the native open (it would only throw) and warn once per distinct
|
|
55
|
+
// dbPath (not once per process — a process can legitimately touch more
|
|
56
|
+
// than one encrypted store via a custom `dbPath` argument).
|
|
57
|
+
/** Read just the first `len` bytes of a file without loading the whole file
|
|
58
|
+
* into memory. Mirrors the pattern in commands/doctor.ts's readHeaderBytes
|
|
59
|
+
* (not imported from there — this is a memory-layer module and must not
|
|
60
|
+
* depend on the command layer). */
|
|
61
|
+
function readHeaderBytes(filePath, len) {
|
|
62
|
+
const fd = fs.openSync(filePath, 'r');
|
|
63
|
+
try {
|
|
64
|
+
const buf = Buffer.alloc(len);
|
|
65
|
+
const bytesRead = fs.readSync(fd, buf, 0, len, 0);
|
|
66
|
+
return buf.subarray(0, bytesRead);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs.closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
|
|
73
|
+
// shortest possible blob is 32 bytes. 64 gives headroom.
|
|
74
|
+
const ENCRYPTION_SNIFF_LEN = 64;
|
|
75
|
+
/** True iff `filePath` is a legitimately RFE1-encrypted-at-rest file. A read
|
|
76
|
+
* failure (permissions, races) is treated as "not encrypted" — the caller's
|
|
77
|
+
* own open/query error handling still runs right after. */
|
|
78
|
+
function isRfe1EncryptedAtRest(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
return isEncryptedBlob(readHeaderBytes(filePath, ENCRYPTION_SNIFF_LEN));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Keyed by resolved path (not a single process-wide boolean) — a process
|
|
87
|
+
// that touches more than one RFE1-encrypted dbPath (a custom `dbPath` passed
|
|
88
|
+
// to insertGraphEdge/queryEdgesBySource/countGraphEdges, or any multi-store
|
|
89
|
+
// scenario) must still get the warning for EACH distinct path, not just the
|
|
90
|
+
// first one it happens to see.
|
|
91
|
+
const _warnedRfe1EncryptedPaths = new Set();
|
|
92
|
+
function warnRfe1EncryptedOnce(dbPath) {
|
|
93
|
+
const resolved = path.resolve(dbPath);
|
|
94
|
+
if (_warnedRfe1EncryptedPaths.has(resolved))
|
|
95
|
+
return;
|
|
96
|
+
_warnedRfe1EncryptedPaths.add(resolved);
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(`[graph-edge-writer] ${dbPath} is encrypted at rest (CLAUDE_FLOW_ENCRYPT_AT_REST). ` +
|
|
99
|
+
'The native graph-edge bridge cannot open an RFE1-encrypted store directly — ' +
|
|
100
|
+
'this is a known limitation, not corruption. Your existing data is NOT lost ' +
|
|
101
|
+
'or damaged. Do NOT run `memory init` in response to this — that would create ' +
|
|
102
|
+
'a fresh, empty store instead of using your existing encrypted data. ' +
|
|
103
|
+
'See #2889 (https://github.com/ruvnet/ruflo/issues/2889) for details and recovery options.');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True iff the memory.db bridge at `customDbPath` (or the default location)
|
|
107
|
+
* is currently RFE1-encrypted-at-rest. Callers that receive `null` from
|
|
108
|
+
* getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
|
|
109
|
+
* destructive `memory init` command for this specific, non-corrupt case.
|
|
110
|
+
*/
|
|
111
|
+
export function isBridgeDbEncryptedAtRest(customDbPath) {
|
|
112
|
+
const dbPath = customDbPath ?? path.join(getMemoryRoot(), 'memory.db');
|
|
113
|
+
return fs.existsSync(dbPath) && isRfe1EncryptedAtRest(dbPath);
|
|
114
|
+
}
|
|
115
|
+
/** Test-only: clear the once-per-path warning dedup set. */
|
|
116
|
+
export function _resetRfe1WarningFlag() {
|
|
117
|
+
_warnedRfe1EncryptedPaths.clear();
|
|
118
|
+
}
|
|
38
119
|
// ============================================================================
|
|
39
120
|
// Lazy-cached better-sqlite3 db handle
|
|
40
121
|
// ============================================================================
|
|
@@ -134,6 +215,15 @@ export async function getBridgeDb(customDbPath, opts) {
|
|
|
134
215
|
const dbExists = fs.existsSync(dbPath);
|
|
135
216
|
if (!dbExists && !createIfMissing)
|
|
136
217
|
return null;
|
|
218
|
+
// #2889 — detect RFE1 encryption-at-rest before even attempting the
|
|
219
|
+
// native open: `new BetterSqlite3(dbPath)` on an encrypted file would
|
|
220
|
+
// only throw SQLITE_NOTADB, caught by the generic catch below with no
|
|
221
|
+
// way to tell it apart from genuine corruption. Skip straight to a
|
|
222
|
+
// clear, actionable warning instead.
|
|
223
|
+
if (dbExists && isRfe1EncryptedAtRest(dbPath)) {
|
|
224
|
+
warnRfe1EncryptedOnce(dbPath);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
137
227
|
// Ensure parent dir exists for createIfMissing case.
|
|
138
228
|
if (!dbExists && createIfMissing) {
|
|
139
229
|
const dir = path.dirname(dbPath);
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
|
+
import { type AppendCondition } from './append-conditions.js';
|
|
19
20
|
/**
|
|
20
21
|
* #3024: AgentDB's optional native controller stack can abort the whole Node
|
|
21
22
|
* process on Windows during registry initialization (a Rust allocation panic),
|
|
@@ -67,6 +68,9 @@ export declare function bridgeStoreEntry(options: {
|
|
|
67
68
|
ttl?: number;
|
|
68
69
|
dbPath?: string;
|
|
69
70
|
upsert?: boolean;
|
|
71
|
+
requireNative?: boolean;
|
|
72
|
+
appendOnly?: boolean;
|
|
73
|
+
appendConditions?: AppendCondition[];
|
|
70
74
|
/** ADR-323: defaults to 'unknown' when omitted. */
|
|
71
75
|
provenanceType?: string;
|
|
72
76
|
}): Promise<{
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
|
+
import { AppendConditionFailed, assertAppendConditions, validateAppendConditions } from './append-conditions.js';
|
|
19
20
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
20
21
|
import { resolveAgentdbBetterSqlite3 } from './shared-sqlite.js';
|
|
21
22
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
@@ -1025,13 +1026,24 @@ export async function bridgeStoreEntry(options) {
|
|
|
1025
1026
|
const ctx = getDb(registry);
|
|
1026
1027
|
if (!ctx)
|
|
1027
1028
|
return null;
|
|
1029
|
+
if (options.appendOnly && !options.requireNative) {
|
|
1030
|
+
return { success: false, id: '', error: 'Immutable append requires a native writer' };
|
|
1031
|
+
}
|
|
1032
|
+
if (options.appendConditions && (!options.requireNative || !options.appendOnly)) {
|
|
1033
|
+
return { success: false, id: "", error: "Conditional append requires native append-only storage" };
|
|
1034
|
+
}
|
|
1035
|
+
if (options.requireNative && (ctx.agentdb?.isWasm === true || typeof ctx.db.inTransaction !== 'boolean'
|
|
1036
|
+
|| typeof ctx.db.transaction !== 'function' || ctx.db.memory !== false || typeof ctx.db.name !== 'string'
|
|
1037
|
+
|| !ctx.db.name || canonicalDbPath(ctx.db.name) !== canonicalDbPath(options.dbPath))) {
|
|
1038
|
+
return { success: false, id: '', error: 'Native memory writer required; refusing non-native bridge driver' };
|
|
1039
|
+
}
|
|
1028
1040
|
try {
|
|
1029
1041
|
const { key, value, namespace = 'default', tags = [], ttl } = options;
|
|
1030
1042
|
let provenanceType = options.provenanceType ?? 'unknown';
|
|
1031
1043
|
// An omitted type on an upsert means "update the value", not "erase the
|
|
1032
1044
|
// existing trust label". New rows still receive the backward-compatible
|
|
1033
1045
|
// unknown default.
|
|
1034
|
-
if (options.upsert && options.provenanceType === undefined) {
|
|
1046
|
+
if (options.upsert && !options.appendOnly && options.provenanceType === undefined) {
|
|
1035
1047
|
try {
|
|
1036
1048
|
const existing = ctx.db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1').get(namespace, key);
|
|
1037
1049
|
provenanceType = existing?.provenance_type || 'unknown';
|
|
@@ -1089,7 +1101,12 @@ export async function bridgeStoreEntry(options) {
|
|
|
1089
1101
|
// below → typed "already exists"
|
|
1090
1102
|
// error, NEVER a null demotion).
|
|
1091
1103
|
// Upsert path (INSERT OR REPLACE) is unchanged.
|
|
1092
|
-
const
|
|
1104
|
+
const immutableInsertSql = `INSERT INTO memory_entries (
|
|
1105
|
+
id, key, namespace, content, type,
|
|
1106
|
+
embedding, embedding_dimensions, embedding_model,
|
|
1107
|
+
tags, metadata, provenance_type, created_at, updated_at, expires_at, status
|
|
1108
|
+
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
|
|
1109
|
+
const insertSql = options.appendOnly ? immutableInsertSql : options.upsert
|
|
1093
1110
|
? `INSERT OR REPLACE INTO memory_entries (
|
|
1094
1111
|
id, key, namespace, content, type,
|
|
1095
1112
|
embedding, embedding_dimensions, embedding_model,
|
|
@@ -1126,7 +1143,28 @@ export async function bridgeStoreEntry(options) {
|
|
|
1126
1143
|
}
|
|
1127
1144
|
catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
|
|
1128
1145
|
const stmt = ctx.db.prepare(insertSql);
|
|
1129
|
-
const
|
|
1146
|
+
const insert = () => stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
|
|
1147
|
+
let runResult;
|
|
1148
|
+
try {
|
|
1149
|
+
runResult = options.appendOnly
|
|
1150
|
+
? ctx.db.transaction(() => {
|
|
1151
|
+
// Legacy native tables may predate UNIQUE(namespace,key). Serialize the exact
|
|
1152
|
+
// logical-slot check with INSERT, including tombstones, instead of trusting DDL.
|
|
1153
|
+
if (ctx.db.prepare('SELECT key FROM memory_entries WHERE namespace = ? AND key = ?').all(namespace, key).length) {
|
|
1154
|
+
throw new AppendConditionFailed('immutable append rejected: logical key already exists');
|
|
1155
|
+
}
|
|
1156
|
+
if (options.appendConditions)
|
|
1157
|
+
assertAppendConditions(ctx.db, validateAppendConditions(options.appendConditions));
|
|
1158
|
+
return insert();
|
|
1159
|
+
}).immediate()
|
|
1160
|
+
: insert();
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
if (error instanceof AppendConditionFailed || error instanceof TypeError) {
|
|
1164
|
+
return { success: false, id: '', error: error.message };
|
|
1165
|
+
}
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1130
1168
|
// A completed native write proves the bridge is currently healthy. Do not
|
|
1131
1169
|
// retain a diagnostic from an earlier transient failure and append it to a
|
|
1132
1170
|
// later, unrelated sql.js fallback refusal.
|
|
@@ -1143,18 +1181,21 @@ export async function bridgeStoreEntry(options) {
|
|
|
1143
1181
|
error: `key "${key}" already exists in namespace "${namespace}" — pass upsert=true (--upsert on the CLI) to update it`,
|
|
1144
1182
|
};
|
|
1145
1183
|
}
|
|
1184
|
+
// A non-vector immutable INSERT cannot change the vector count, even with concurrent
|
|
1185
|
+
// writers. Unlike historical strict mode, it never resurrects a vector tombstone.
|
|
1146
1186
|
// #2558: keep `vector_indexes.total_vectors` accurate so status/tooling
|
|
1147
1187
|
// stop reporting "HNSW index: 0 vectors" while embedded entries exist.
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
.
|
|
1188
|
+
if (!(options.appendOnly && embeddingJson === null))
|
|
1189
|
+
try {
|
|
1190
|
+
ctx.db
|
|
1191
|
+
.prepare(`UPDATE vector_indexes SET
|
|
1151
1192
|
total_vectors = (SELECT COUNT(*) FROM memory_entries
|
|
1152
1193
|
WHERE namespace = ? AND embedding IS NOT NULL),
|
|
1153
1194
|
updated_at = ?
|
|
1154
1195
|
WHERE name = ?`)
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1196
|
+
.run(namespace, now, namespace);
|
|
1197
|
+
}
|
|
1198
|
+
catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
|
|
1158
1199
|
// #2558: better-sqlite3 opens the DB in WAL mode and, for the small write
|
|
1159
1200
|
// volumes typical of CLI usage, may never reach the auto-checkpoint
|
|
1160
1201
|
// threshold — leaving committed rows only in the -wal file. WAL-blind
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module v3/cli/memory-initializer
|
|
10
10
|
*/
|
|
11
|
+
import type { AppendCondition } from './append-conditions.js';
|
|
11
12
|
import { type EmbeddingQ8 } from './embedding-q8.js';
|
|
12
13
|
/**
|
|
13
14
|
* ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
|
|
@@ -442,6 +443,9 @@ export declare function storeEntry(options: {
|
|
|
442
443
|
ttl?: number;
|
|
443
444
|
dbPath?: string;
|
|
444
445
|
upsert?: boolean;
|
|
446
|
+
requireNative?: boolean;
|
|
447
|
+
appendOnly?: boolean;
|
|
448
|
+
appendConditions?: AppendCondition[];
|
|
445
449
|
/** ADR-323: defaults to 'unknown' when omitted. */
|
|
446
450
|
provenanceType?: string;
|
|
447
451
|
}): Promise<{
|