@claude-flow/cli 3.32.25 → 3.32.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/auto-memory-hook.mjs +430 -430
  3. package/.claude/helpers/helpers.manifest.json +6 -6
  4. package/.claude/helpers/hook-handler.cjs +565 -565
  5. package/.claude/helpers/intelligence.cjs +1058 -1058
  6. package/.claude/helpers/statusline.cjs +1060 -1060
  7. package/catalog-manifest.json +4 -4
  8. package/dist/src/commands/index.d.ts +1 -0
  9. package/dist/src/commands/index.js +5 -3
  10. package/dist/src/commands/memory.js +49 -5
  11. package/dist/src/commands/metaharness.js +100 -2
  12. package/dist/src/commands/policy.d.ts +4 -0
  13. package/dist/src/commands/policy.js +107 -0
  14. package/dist/src/index.js +18 -0
  15. package/dist/src/mcp-client.js +25 -1
  16. package/dist/src/mcp-tools/capability-brain.d.ts +134 -0
  17. package/dist/src/mcp-tools/capability-brain.js +697 -0
  18. package/dist/src/mcp-tools/guidance-tools.d.ts +2 -0
  19. package/dist/src/mcp-tools/guidance-tools.js +369 -37
  20. package/dist/src/mcp-tools/index.d.ts +4 -1
  21. package/dist/src/mcp-tools/index.js +3 -1
  22. package/dist/src/mcp-tools/memory-tools.js +26 -0
  23. package/dist/src/mcp-tools/metaharness-tools.js +106 -1
  24. package/dist/src/mcp-tools/policy-tools.d.ts +3 -0
  25. package/dist/src/mcp-tools/policy-tools.js +121 -0
  26. package/dist/src/memory/memory-bridge.d.ts +11 -0
  27. package/dist/src/memory/memory-bridge.js +100 -21
  28. package/dist/src/memory/memory-initializer.d.ts +22 -1
  29. package/dist/src/memory/memory-initializer.js +184 -39
  30. package/dist/src/services/bounded-worker-pool.d.ts +28 -0
  31. package/dist/src/services/bounded-worker-pool.js +90 -0
  32. package/dist/src/services/flywheel-proposer.d.ts +87 -0
  33. package/dist/src/services/flywheel-proposer.js +165 -0
  34. package/dist/src/services/flywheel-receipt.d.ts +136 -0
  35. package/dist/src/services/flywheel-receipt.js +309 -0
  36. package/dist/src/services/flywheel-transaction.d.ts +77 -0
  37. package/dist/src/services/flywheel-transaction.js +378 -0
  38. package/dist/src/services/harness-flywheel-runtime.d.ts +11 -0
  39. package/dist/src/services/harness-flywheel-runtime.js +85 -2
  40. package/dist/src/services/harness-flywheel.d.ts +27 -1
  41. package/dist/src/services/harness-flywheel.js +138 -27
  42. package/dist/src/services/policy-runtime.d.ts +38 -0
  43. package/dist/src/services/policy-runtime.js +340 -0
  44. package/package.json +23 -5
  45. package/plugins/ruflo-metaharness/scripts/smoke.sh +22 -14
  46. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +3 -1
  47. package/.claude/.proven-config-version +0 -1
  48. package/.claude/proven-config.json +0 -42
@@ -264,6 +264,11 @@ export const memoryTools = [
264
264
  },
265
265
  ttl: { type: 'number', description: 'Time-to-live in seconds (optional)' },
266
266
  upsert: { type: 'boolean', description: 'Update existing key instead of failing (default: true, matching CLI `memory store`; set false for strict-insert). #2775 parity.' },
267
+ provenance_type: {
268
+ type: 'string',
269
+ enum: ['user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'],
270
+ description: 'ADR-323: who/what produced this value, so shared-namespace retrieval can filter by trust level instead of conflating a user\'s stated claim with an agent\'s own output. Default: "unknown".',
271
+ },
267
272
  },
268
273
  required: ['key', 'value'],
269
274
  },
@@ -278,6 +283,9 @@ export const memoryTools = [
278
283
  const ttl = input.ttl;
279
284
  // #2775 parity with CLI: default true; only explicit `upsert: false` opts out.
280
285
  const upsert = input.upsert !== false;
286
+ // ADR-323: leave undefined when omitted so storeEntry's own default
287
+ // ('unknown') applies uniformly across the CLI, MCP tool, and other callers.
288
+ const provenanceType = input.provenance_type;
281
289
  if (!value) {
282
290
  return {
283
291
  success: false,
@@ -298,6 +306,7 @@ export const memoryTools = [
298
306
  tags,
299
307
  ttl,
300
308
  upsert,
309
+ provenanceType,
301
310
  });
302
311
  const duration = performance.now() - startTime;
303
312
  return {
@@ -308,6 +317,7 @@ export const memoryTools = [
308
317
  storedAt: new Date().toISOString(),
309
318
  hasEmbedding: !!result.embedding,
310
319
  embeddingDimensions: result.embedding?.dimensions || null,
320
+ provenanceType: provenanceType || 'unknown',
311
321
  backend: 'sql.js + HNSW',
312
322
  storeTime: `${duration.toFixed(2)}ms`,
313
323
  error: result.error,
@@ -394,6 +404,11 @@ export const memoryTools = [
394
404
  limit: { type: 'number', description: 'Maximum results (default: 10)' },
395
405
  threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.3)' },
396
406
  smart: { type: 'boolean', description: 'Enable SmartRetrieval pipeline — query expansion, RRF fusion, recency boost, MMR diversity (default: false)' },
407
+ provenance_filter: {
408
+ type: 'array',
409
+ items: { type: 'string', enum: ['user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'] },
410
+ description: 'ADR-323: restrict results to these provenance types (e.g. exclude user_claim when fact-checking). Omit for no filtering. Enforced for standard and SmartRetrieval searches.',
411
+ },
397
412
  },
398
413
  required: ['query'],
399
414
  },
@@ -401,6 +416,7 @@ export const memoryTools = [
401
416
  await ensureInitialized();
402
417
  const { searchEntries } = await getMemoryFunctions();
403
418
  const query = input.query;
419
+ const provenanceFilter = input.provenance_filter;
404
420
  // #2646 (3rd occurrence of #1123/#1131 shape): do NOT coerce an omitted
405
421
  // namespace to the literal string 'default' here. Both searchEntries()
406
422
  // and bridgeSearchEntries() already resolve an omitted/undefined
@@ -442,6 +458,7 @@ export const memoryTools = [
442
458
  namespace: req.namespace || namespace,
443
459
  limit: req.limit || limit * 3,
444
460
  threshold: req.threshold ?? threshold,
461
+ provenanceFilter,
445
462
  });
446
463
  return {
447
464
  results: r.results.map(e => ({
@@ -450,6 +467,7 @@ export const memoryTools = [
450
467
  content: e.content,
451
468
  score: e.score,
452
469
  namespace: e.namespace,
470
+ provenanceType: e.provenanceType,
453
471
  })),
454
472
  };
455
473
  };
@@ -471,6 +489,7 @@ export const memoryTools = [
471
489
  namespace: r.namespace,
472
490
  value,
473
491
  similarity: r.score,
492
+ provenanceType: r.provenanceType,
474
493
  };
475
494
  });
476
495
  return {
@@ -490,12 +509,18 @@ export const memoryTools = [
490
509
  // Original non-smart path (unchanged) — also reached when smart was
491
510
  // requested but unavailable. We attach `smartFallback` to the
492
511
  // response so callers can see the degradation explicitly.
512
+ // ADR-323: the same filter is also passed into every raw search used
513
+ // by SmartRetrieval above, so query expansion cannot widen trust scope.
493
514
  const result = await searchEntries({
494
515
  query,
495
516
  namespace,
496
517
  limit,
497
518
  threshold,
519
+ provenanceFilter,
498
520
  });
521
+ if (!result.success) {
522
+ return { query, results: [], total: 0, error: result.error };
523
+ }
499
524
  const duration = performance.now() - startTime;
500
525
  // Parse JSON values in results
501
526
  const results = result.results.map(r => {
@@ -511,6 +536,7 @@ export const memoryTools = [
511
536
  namespace: r.namespace,
512
537
  value,
513
538
  similarity: r.score,
539
+ provenanceType: r.provenanceType,
514
540
  };
515
541
  });
516
542
  return {
@@ -48,9 +48,12 @@
48
48
  */
49
49
  import { getProjectCwd } from './types.js';
50
50
  import { spawn } from 'node:child_process';
51
- import { existsSync } from 'node:fs';
51
+ import { existsSync, readFileSync } from 'node:fs';
52
52
  import { join, dirname, resolve } from 'node:path';
53
53
  import { fileURLToPath } from 'node:url';
54
+ import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
55
+ import { evaluatePolicyRequest } from '../services/policy-runtime.js';
56
+ import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
54
57
  const __dirname = dirname(fileURLToPath(import.meta.url));
55
58
  /**
56
59
  * Walk up from this module to find plugins/ruflo-metaharness/scripts/.
@@ -680,5 +683,107 @@ export const metaharnessTools = [
680
683
  return { success: r.success, data: r.json, degraded: r.degraded, exitCode: r.exitCode };
681
684
  },
682
685
  },
686
+ {
687
+ name: 'metaharness_flywheel',
688
+ description: 'ADR-322 — evaluate candidates into immutable receipts, inspect the append-only promotion ledger, and explicitly promote one signed receipt with atomic compare-and-swap semantics. Use when running governed flywheel evaluation or promotion; evaluation never mutates the active champion, and promotion requires confirm=true plus a locally approved Ed25519 public-key PEM path.',
689
+ category: 'metaharness',
690
+ inputSchema: {
691
+ type: 'object',
692
+ properties: {
693
+ operation: { type: 'string', enum: ['run', 'status', 'receipts', 'history', 'promote'], description: 'Flywheel operation', default: 'status' },
694
+ projectRoot: { type: 'string', description: 'Target project root (default: active project cwd)' },
695
+ receiptId: { type: 'string', description: 'Receipt content ID for promote' },
696
+ sample: { type: 'number', description: 'Maximum harvested evaluation sample', default: 40 },
697
+ proposer: { type: 'string', enum: ['local', 'auto', 'darwin'], description: 'Candidate proposer. Auto fallback is evaluation-only; explicit darwin fails closed when no compatible adapter is installed.', default: 'auto' },
698
+ privateKeyPath: { type: 'string', description: 'Local Ed25519 private-key PEM path for signing run receipts; must be paired with publicKeyPath' },
699
+ publicKeyPath: { type: 'string', description: 'Local Ed25519 public-key PEM path used to sign a run or approve a promotion' },
700
+ confirm: { type: 'boolean', description: 'Required for promote; never inferred', default: false },
701
+ maxConcurrency: { type: 'number', description: 'ADR-324 hard local candidate-evaluation concurrency cap (1-8)', default: 2 },
702
+ timeoutMs: { type: 'number', description: 'Abort concurrent evaluation after this wall-clock limit', default: 120000 },
703
+ approvalIds: { type: 'array', items: { type: 'string' }, description: 'Scoped ADR-324 approval IDs for privileged promotion' },
704
+ },
705
+ required: ['operation'],
706
+ },
707
+ handler: async (input) => {
708
+ const operation = String(input.operation ?? 'status');
709
+ const projectRoot = resolve(String(input.projectRoot ?? getProjectCwd()));
710
+ if (operation === 'status') {
711
+ return {
712
+ success: true,
713
+ data: {
714
+ state: readFlywheelTransactionState(projectRoot),
715
+ ledger: verifyFlywheelLedger(projectRoot),
716
+ },
717
+ degraded: false,
718
+ exitCode: 0,
719
+ };
720
+ }
721
+ if (operation === 'receipts') {
722
+ const data = listFlywheelReceipts(projectRoot).map(({ receipt, state }) => ({
723
+ receiptId: receipt.payload.receiptId,
724
+ candidateId: receipt.payload.candidateId,
725
+ baselineRef: receipt.payload.baselineRef,
726
+ decision: receipt.payload.decision,
727
+ signed: !!receipt.signature,
728
+ issuedAt: receipt.payload.issuedAt,
729
+ state,
730
+ }));
731
+ return { success: true, data, degraded: false, exitCode: 0 };
732
+ }
733
+ if (operation === 'history') {
734
+ const state = readFlywheelTransactionState(projectRoot);
735
+ return { success: true, data: { ledgerHead: state.ledgerHead, commits: state.commits }, degraded: false, exitCode: 0 };
736
+ }
737
+ if (operation === 'run') {
738
+ const privateKeyPath = input.privateKeyPath ? resolve(String(input.privateKeyPath)) : undefined;
739
+ const publicKeyPath = input.publicKeyPath ? resolve(String(input.publicKeyPath)) : undefined;
740
+ if (!!privateKeyPath !== !!publicKeyPath) {
741
+ return { success: false, data: { reason: 'privateKeyPath and publicKeyPath must be supplied together' }, degraded: false, exitCode: 2 };
742
+ }
743
+ const data = await runFlywheelWorker(projectRoot, {
744
+ optInOverride: true,
745
+ sample: Number(input.sample ?? 40),
746
+ proposer: String(input.proposer ?? 'auto'),
747
+ receiptPrivateKeyPem: privateKeyPath ? readFileSync(privateKeyPath, 'utf8') : undefined,
748
+ receiptPublicKeyPem: publicKeyPath ? readFileSync(publicKeyPath, 'utf8') : undefined,
749
+ maxConcurrency: Number(input.maxConcurrency ?? 2),
750
+ evaluationTimeoutMs: Number(input.timeoutMs ?? 120_000),
751
+ });
752
+ return { success: data.ran, data, degraded: false, exitCode: data.ran ? 0 : 1 };
753
+ }
754
+ if (operation === 'promote') {
755
+ if (!input.receiptId || !input.publicKeyPath) {
756
+ return { success: false, data: { reason: 'receiptId and publicKeyPath are required' }, degraded: false, exitCode: 2 };
757
+ }
758
+ const policy = await evaluatePolicyRequest({
759
+ identity: { id: 'metaharness-mcp', type: 'agent', roles: ['optimizer'] },
760
+ action: {
761
+ type: 'metaharness.candidate.promote',
762
+ resource: String(input.receiptId),
763
+ environment: 'production',
764
+ destructive: true,
765
+ },
766
+ context: {
767
+ approvalIds: Array.isArray(input.approvalIds) ? input.approvalIds.map(String) : undefined,
768
+ },
769
+ }, projectRoot);
770
+ if (policy.enforcedOutcome !== 'allowed') {
771
+ return {
772
+ success: false,
773
+ data: { reason: `policy-${policy.enforcedOutcome}:${policy.reason}`, policyReceiptId: policy.receiptId },
774
+ degraded: false,
775
+ exitCode: 1,
776
+ };
777
+ }
778
+ const publicKey = readFileSync(resolve(String(input.publicKeyPath)), 'utf8');
779
+ const data = await promoteFlywheelCandidate(projectRoot, String(input.receiptId), {
780
+ confirm: input.confirm === true,
781
+ trustedPublicKeys: new Set([publicKey]),
782
+ });
783
+ return { success: data.success, data, degraded: false, exitCode: data.success ? 0 : 1 };
784
+ }
785
+ return { success: false, data: { reason: `unsupported operation: ${operation}` }, degraded: false, exitCode: 2 };
786
+ },
787
+ },
683
788
  ];
684
789
  //# sourceMappingURL=metaharness-tools.js.map
@@ -0,0 +1,3 @@
1
+ import type { MCPTool } from './types.js';
2
+ export declare const policyTools: MCPTool[];
3
+ //# sourceMappingURL=policy-tools.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { evaluatePolicyRequest, issuePolicyApproval, loadPolicyState, revokePolicyApproval, setPolicyBudget, setPolicyMode, upsertPolicyRule, verifyPolicyLedger, } from '../services/policy-runtime.js';
2
+ function requireAuthenticatedAdministrator(context) {
3
+ if (context?.principalType !== 'user' || typeof context.principalId !== 'string') {
4
+ throw new Error('policy administration requires an authenticated user context');
5
+ }
6
+ }
7
+ export const policyTools = [
8
+ {
9
+ name: 'policy_evaluate',
10
+ description: 'Evaluate an agent action against ADR-324 policy and persist a tamper-evident decision receipt. Use when a consequential tool, deployment, network, spend, or promotion action needs authorization.',
11
+ category: 'security',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ request: { type: 'object', description: 'Policy request containing identity, action, and optional evidence/envelope context' },
16
+ },
17
+ required: ['request'],
18
+ },
19
+ handler: async (input, context) => evaluatePolicyRequest(input.request, typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd()),
20
+ },
21
+ {
22
+ name: 'policy_status',
23
+ description: 'Inspect policy mode, migration state, rules, budgets, approvals, and ledger integrity. Use when diagnosing whether an installation is in compatibility, observation, or enforcement mode.',
24
+ category: 'security',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {},
28
+ },
29
+ handler: async (_input, context) => {
30
+ const root = typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd();
31
+ const state = loadPolicyState(root);
32
+ return {
33
+ version: state.version,
34
+ mode: state.mode,
35
+ migratedFrom: state.migratedFrom,
36
+ counts: {
37
+ rules: state.rules.length,
38
+ budgets: state.budgets.length,
39
+ approvals: state.approvals.length,
40
+ receipts: state.receipts.length,
41
+ },
42
+ ledger: await verifyPolicyLedger(root),
43
+ };
44
+ },
45
+ },
46
+ {
47
+ name: 'policy_rule_upsert',
48
+ description: 'Create or update a versioned agentic policy rule. Use when defining explicit allow, deny, or human-approval requirements; existing installations remain unchanged until rules and enforcement mode are configured.',
49
+ category: 'security',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ rule: { type: 'object' },
54
+ mode: { type: 'string', enum: ['legacy', 'observe', 'enforce'] },
55
+ },
56
+ required: ['rule'],
57
+ },
58
+ handler: async (input, context) => {
59
+ requireAuthenticatedAdministrator(context);
60
+ const root = typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd();
61
+ await upsertPolicyRule(input.rule, root);
62
+ if (input.mode)
63
+ await setPolicyMode(input.mode, root);
64
+ return { success: true, ruleId: input.rule.id, mode: loadPolicyState(root).mode };
65
+ },
66
+ },
67
+ {
68
+ name: 'policy_budget_set',
69
+ description: 'Create or update an atomic USD/token ceiling for a principal, action, and resource window. Use when constraining agent spend; budget checks and decision receipts commit under the same policy-state lock.',
70
+ category: 'security',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ budget: { type: 'object' },
75
+ },
76
+ required: ['budget'],
77
+ },
78
+ handler: async (input, context) => {
79
+ requireAuthenticatedAdministrator(context);
80
+ const budget = input.budget;
81
+ await setPolicyBudget(budget, typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd());
82
+ return { success: true, budgetId: budget.id };
83
+ },
84
+ },
85
+ {
86
+ name: 'policy_approve',
87
+ description: 'Issue a scoped, expiring, limited-use human approval. Use when policy_evaluate returns approval_required; self-approval is rejected.',
88
+ category: 'security',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ approval: { type: 'object' },
93
+ },
94
+ required: ['approval'],
95
+ },
96
+ handler: async (input, context) => {
97
+ requireAuthenticatedAdministrator(context);
98
+ const requested = input.approval;
99
+ return issuePolicyApproval({ ...requested, issuedBy: context.principalId }, typeof context.projectRoot === 'string' ? context.projectRoot : process.cwd(), (issuer) => issuer === context.principalId);
100
+ },
101
+ },
102
+ {
103
+ name: 'policy_revoke',
104
+ description: 'Revoke an outstanding policy approval immediately. Use when responding to an incident or when a previously approved action is no longer authorized.',
105
+ category: 'security',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ approvalId: { type: 'string' },
110
+ },
111
+ required: ['approvalId'],
112
+ },
113
+ handler: async (input, context) => {
114
+ requireAuthenticatedAdministrator(context);
115
+ return {
116
+ success: await revokePolicyApproval(String(input.approvalId), typeof context.projectRoot === 'string' ? context.projectRoot : process.cwd()),
117
+ };
118
+ },
119
+ },
120
+ ];
121
+ //# sourceMappingURL=policy-tools.js.map
@@ -30,6 +30,8 @@ export declare function bridgeStoreEntry(options: {
30
30
  ttl?: number;
31
31
  dbPath?: string;
32
32
  upsert?: boolean;
33
+ /** ADR-323: defaults to 'unknown' when omitted. */
34
+ provenanceType?: string;
33
35
  }): Promise<{
34
36
  success: boolean;
35
37
  id: string;
@@ -54,6 +56,8 @@ export declare function bridgeSearchEntries(options: {
54
56
  limit?: number;
55
57
  threshold?: number;
56
58
  dbPath?: string;
59
+ /** ADR-323: restrict results to these provenance types. */
60
+ provenanceFilter?: string[];
57
61
  }): Promise<{
58
62
  success: boolean;
59
63
  results: {
@@ -63,6 +67,10 @@ export declare function bridgeSearchEntries(options: {
63
67
  score: number;
64
68
  namespace: string;
65
69
  provenance?: string;
70
+ /** ADR-323 — NOT the same as `provenance` above (that's the
71
+ * ExplainableRecall score breakdown). This is the entry's
72
+ * user_claim/agent_output/... provenance type. */
73
+ provenanceType?: string;
66
74
  }[];
67
75
  searchTime: number;
68
76
  searchMethod?: string;
@@ -78,6 +86,8 @@ export declare function bridgeListEntries(options: {
78
86
  dbPath?: string;
79
87
  /** #2073: When true, include the entry's full `content` string in each result. */
80
88
  includeContent?: boolean;
89
+ /** ADR-323: restrict rows to these provenance types. */
90
+ provenanceFilter?: string[];
81
91
  }): Promise<{
82
92
  success: boolean;
83
93
  entries: {
@@ -91,6 +101,7 @@ export declare function bridgeListEntries(options: {
91
101
  hasEmbedding: boolean;
92
102
  /** #2073: Present when `includeContent: true` was requested. */
93
103
  content?: string;
104
+ provenanceType?: string;
94
105
  }[];
95
106
  total: number;
96
107
  error?: string;
@@ -23,6 +23,23 @@ import { createRequire } from 'node:module';
23
23
  let registryPromise = null;
24
24
  let registryInstance = null;
25
25
  let bridgeAvailable = null;
26
+ /**
27
+ * ADR-323: reuse memory-initializer's provenance-type allowlist rather than
28
+ * duplicating it (drift risk). Lazy CJS require for the same circular-ESM-
29
+ * dependency reason as getDbPath() below.
30
+ */
31
+ function isValidProvenanceType(value) {
32
+ try {
33
+ const cjsRequire = createRequire(import.meta.url);
34
+ const mod = cjsRequire('./memory-initializer.js');
35
+ if (typeof mod.isValidProvenanceType === 'function') {
36
+ return mod.isValidProvenanceType(value);
37
+ }
38
+ }
39
+ catch { /* memory-initializer not resolvable in this build */ }
40
+ // Fallback allowlist — keep in sync with memory-initializer.ts's PROVENANCE_TYPES.
41
+ return typeof value === 'string' && ['user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'].includes(value);
42
+ }
26
43
  /**
27
44
  * Resolve database path with path traversal protection.
28
45
  * Only allows paths within or below the project's working directory,
@@ -648,6 +665,16 @@ async function rescueAgentdbEmbedder(agentdb) {
648
665
  * Returns null to signal fallback to sql.js.
649
666
  */
650
667
  export async function bridgeStoreEntry(options) {
668
+ // ADR-323 — validated once in storeEntry() before this is reached on that
669
+ // path, but bridgeStoreEntry() also has direct internal callers, so check
670
+ // again here rather than trust every call site.
671
+ if (options.provenanceType !== undefined && !isValidProvenanceType(options.provenanceType)) {
672
+ return {
673
+ success: false,
674
+ id: '',
675
+ error: `Invalid provenance type "${options.provenanceType}"`,
676
+ };
677
+ }
651
678
  const registry = await getRegistry(options.dbPath);
652
679
  if (!registry)
653
680
  return null;
@@ -656,6 +683,17 @@ export async function bridgeStoreEntry(options) {
656
683
  return null;
657
684
  try {
658
685
  const { key, value, namespace = 'default', tags = [], ttl } = options;
686
+ let provenanceType = options.provenanceType ?? 'unknown';
687
+ // An omitted type on an upsert means "update the value", not "erase the
688
+ // existing trust label". New rows still receive the backward-compatible
689
+ // unknown default.
690
+ if (options.upsert && options.provenanceType === undefined) {
691
+ try {
692
+ const existing = ctx.db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1').get(namespace, key);
693
+ provenanceType = existing?.provenance_type || 'unknown';
694
+ }
695
+ catch { /* legacy schema or new row — keep unknown */ }
696
+ }
659
697
  const id = generateId('entry');
660
698
  const now = Date.now();
661
699
  // #2245 — record the activity so signalsProcessed stops being a dead
@@ -713,13 +751,13 @@ export async function bridgeStoreEntry(options) {
713
751
  ? `INSERT OR REPLACE INTO memory_entries (
714
752
  id, key, namespace, content, type,
715
753
  embedding, embedding_dimensions, embedding_model,
716
- tags, metadata, created_at, updated_at, expires_at, status
717
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
754
+ tags, metadata, provenance_type, created_at, updated_at, expires_at, status
755
+ ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
718
756
  : `INSERT INTO memory_entries (
719
757
  id, key, namespace, content, type,
720
758
  embedding, embedding_dimensions, embedding_model,
721
- tags, metadata, created_at, updated_at, expires_at, status
722
- ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, 'active')
759
+ tags, metadata, provenance_type, created_at, updated_at, expires_at, status
760
+ ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')
723
761
  ON CONFLICT(namespace, key) DO UPDATE SET
724
762
  id = excluded.id,
725
763
  content = excluded.content,
@@ -728,6 +766,7 @@ export async function bridgeStoreEntry(options) {
728
766
  embedding_model = excluded.embedding_model,
729
767
  tags = excluded.tags,
730
768
  metadata = excluded.metadata,
769
+ provenance_type = excluded.provenance_type,
731
770
  created_at = excluded.created_at,
732
771
  updated_at = excluded.updated_at,
733
772
  expires_at = excluded.expires_at,
@@ -745,7 +784,7 @@ export async function bridgeStoreEntry(options) {
745
784
  }
746
785
  catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
747
786
  const stmt = ctx.db.prepare(insertSql);
748
- const runResult = stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
787
+ const runResult = 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);
749
788
  // #2775: strict insert against an ACTIVE existing row → changes === 0
750
789
  // (the ON CONFLICT WHERE clause above suppressed the update). Surface
751
790
  // this as a typed data-level error rather than a bridge failure —
@@ -828,6 +867,11 @@ export async function bridgeStoreEntry(options) {
828
867
  * Combines cosine similarity (semantic) with BM25 (lexical) via reciprocal rank fusion.
829
868
  */
830
869
  export async function bridgeSearchEntries(options) {
870
+ if (options.provenanceFilter?.length) {
871
+ const invalid = options.provenanceFilter.filter(p => !isValidProvenanceType(p));
872
+ if (invalid.length > 0)
873
+ return null; // caller (searchEntries) demotes to sql.js, which returns a typed error
874
+ }
831
875
  const registry = await getRegistry(options.dbPath);
832
876
  if (!registry)
833
877
  return null;
@@ -835,7 +879,7 @@ export async function bridgeSearchEntries(options) {
835
879
  if (!ctx)
836
880
  return null;
837
881
  try {
838
- const { query: queryStr, namespace, limit = 10, threshold = 0.3 } = options;
882
+ const { query: queryStr, namespace, limit = 10, threshold = 0.3, provenanceFilter } = options;
839
883
  const effectiveNamespace = namespace || 'all';
840
884
  const startTime = Date.now();
841
885
  // Generate query embedding
@@ -851,18 +895,27 @@ export async function bridgeSearchEntries(options) {
851
895
  // Fall back to keyword search
852
896
  }
853
897
  // better-sqlite3: .prepare().all() returns array of objects
854
- const nsFilter = effectiveNamespace !== 'all'
855
- ? `AND namespace = ?`
856
- : '';
898
+ // ADR-323: compose namespace + provenance filters into one WHERE clause.
899
+ const filters = [];
900
+ const filterParams = [];
901
+ if (effectiveNamespace !== 'all') {
902
+ filters.push('namespace = ?');
903
+ filterParams.push(effectiveNamespace);
904
+ }
905
+ if (provenanceFilter?.length) {
906
+ filters.push(`provenance_type IN (${provenanceFilter.map(() => '?').join(',')})`);
907
+ filterParams.push(...provenanceFilter);
908
+ }
909
+ const whereExtra = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
857
910
  let rows;
858
911
  try {
859
912
  const stmt = ctx.db.prepare(`
860
- SELECT id, key, namespace, content, embedding
913
+ SELECT id, key, namespace, content, embedding, provenance_type
861
914
  FROM memory_entries
862
- WHERE status = 'active' ${nsFilter}
915
+ WHERE status = 'active' ${whereExtra}
863
916
  LIMIT 1000
864
917
  `);
865
- rows = effectiveNamespace !== 'all' ? stmt.all(effectiveNamespace) : stmt.all();
918
+ rows = filterParams.length > 0 ? stmt.all(...filterParams) : stmt.all();
866
919
  }
867
920
  catch {
868
921
  return null;
@@ -930,6 +983,7 @@ export async function bridgeSearchEntries(options) {
930
983
  score,
931
984
  namespace: row.namespace || 'default',
932
985
  provenance,
986
+ provenanceType: row.provenance_type || 'unknown',
933
987
  });
934
988
  }
935
989
  }
@@ -956,9 +1010,20 @@ export async function bridgeListEntries(options) {
956
1010
  if (!ctx)
957
1011
  return null;
958
1012
  try {
959
- const { namespace, limit = 20, offset = 0 } = options;
960
- const nsFilter = namespace ? `AND namespace = ?` : '';
961
- const nsParams = namespace ? [namespace] : [];
1013
+ const { namespace, limit = 20, offset = 0, provenanceFilter } = options;
1014
+ if (provenanceFilter?.some(p => !isValidProvenanceType(p)))
1015
+ return null;
1016
+ const filters = [];
1017
+ const filterParams = [];
1018
+ if (namespace) {
1019
+ filters.push('namespace = ?');
1020
+ filterParams.push(namespace);
1021
+ }
1022
+ if (provenanceFilter?.length) {
1023
+ filters.push(`provenance_type IN (${provenanceFilter.map(() => '?').join(',')})`);
1024
+ filterParams.push(...provenanceFilter);
1025
+ }
1026
+ const extraFilter = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
962
1027
  // #2120 — `status IS NULL` accepted alongside `'active'`. Old
963
1028
  // databases imported by the auto-memory bridge (before the status
964
1029
  // column existed) end up with NULL status after schema migration if
@@ -971,8 +1036,8 @@ export async function bridgeListEntries(options) {
971
1036
  // Count
972
1037
  let total = 0;
973
1038
  try {
974
- const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE ${statusFilter} ${nsFilter}`);
975
- const countRow = countStmt.get(...nsParams);
1039
+ const countStmt = ctx.db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE ${statusFilter} ${extraFilter}`);
1040
+ const countRow = countStmt.get(...filterParams);
976
1041
  total = countRow?.cnt ?? 0;
977
1042
  }
978
1043
  catch {
@@ -982,13 +1047,13 @@ export async function bridgeListEntries(options) {
982
1047
  const entries = [];
983
1048
  try {
984
1049
  const stmt = ctx.db.prepare(`
985
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
1050
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type
986
1051
  FROM memory_entries
987
- WHERE ${statusFilter} ${nsFilter}
1052
+ WHERE ${statusFilter} ${extraFilter}
988
1053
  ORDER BY updated_at DESC
989
1054
  LIMIT ? OFFSET ?
990
1055
  `);
991
- const rows = stmt.all(...nsParams, limit, offset);
1056
+ const rows = stmt.all(...filterParams, limit, offset);
992
1057
  for (const row of rows) {
993
1058
  const entry = {
994
1059
  // #2073: don't truncate id when content is requested — callers
@@ -1001,6 +1066,7 @@ export async function bridgeListEntries(options) {
1001
1066
  createdAt: row.created_at || new Date().toISOString(),
1002
1067
  updatedAt: row.updated_at || new Date().toISOString(),
1003
1068
  hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
1069
+ provenanceType: row.provenance_type || 'unknown',
1004
1070
  };
1005
1071
  if (options.includeContent) {
1006
1072
  entry.content = row.content || '';
@@ -1410,12 +1476,25 @@ export async function bridgeAddToHNSW(id, embedding, entry, dbPath) {
1410
1476
  try {
1411
1477
  const now = Date.now();
1412
1478
  const embeddingJson = JSON.stringify(embedding);
1479
+ // Do not use INSERT OR REPLACE here. storeEntry() has already written the
1480
+ // authoritative row, including provenance/tags/metadata; REPLACE deleted
1481
+ // that row and recreated it without provenance_type, silently resetting
1482
+ // every typed CLI/MCP write to "unknown".
1413
1483
  ctx.db.prepare(`
1414
- INSERT OR REPLACE INTO memory_entries (
1484
+ INSERT INTO memory_entries (
1415
1485
  id, key, namespace, content, type,
1416
1486
  embedding, embedding_dimensions, embedding_model,
1417
1487
  created_at, updated_at, status
1418
1488
  ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, 'Xenova/all-MiniLM-L6-v2', ?, ?, 'active')
1489
+ ON CONFLICT(namespace, key) DO UPDATE SET
1490
+ id = excluded.id,
1491
+ content = excluded.content,
1492
+ type = excluded.type,
1493
+ embedding = excluded.embedding,
1494
+ embedding_dimensions = excluded.embedding_dimensions,
1495
+ embedding_model = excluded.embedding_model,
1496
+ updated_at = excluded.updated_at,
1497
+ status = 'active'
1419
1498
  `).run(id, entry.key, entry.namespace, entry.content, embeddingJson, embedding.length, now, now);
1420
1499
  return true;
1421
1500
  }