@remnic/core 9.3.665 → 9.3.667

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 (150) hide show
  1. package/dist/access-audit.js +2 -2
  2. package/dist/access-cli.js +41 -40
  3. package/dist/access-cli.js.map +1 -1
  4. package/dist/access-http.d.ts +3 -2
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +3 -2
  7. package/dist/access-mcp.js +22 -22
  8. package/dist/access-schema.d.ts +36 -36
  9. package/dist/access-schema.js +3 -3
  10. package/dist/{access-service-D0SLB4MH.d.ts → access-service-BCuaiNHa.d.ts} +1 -1
  11. package/dist/access-service.d.ts +3 -2
  12. package/dist/access-service.js +21 -21
  13. package/dist/adapters/index.js +4 -4
  14. package/dist/adapters/registry.js +2 -2
  15. package/dist/bootstrap.d.ts +2 -1
  16. package/dist/briefing.js +4 -3
  17. package/dist/capabilities.d.ts +73 -0
  18. package/dist/capabilities.js +8 -0
  19. package/dist/capabilities.js.map +1 -0
  20. package/dist/causal-behavior.js +2 -2
  21. package/dist/causal-chain.js +2 -2
  22. package/dist/causal-consolidation.js +7 -6
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/causal-retrieval.js +2 -2
  25. package/dist/causal-trajectory.js +1 -1
  26. package/dist/{chunk-ROHLEUTH.js → chunk-23EBQ27U.js} +5 -5
  27. package/dist/{chunk-YW52BQSU.js → chunk-2TCHDANJ.js} +2 -2
  28. package/dist/{chunk-IROWLAWG.js → chunk-46WUVFOD.js} +4 -4
  29. package/dist/{chunk-WH4SKYPX.js → chunk-4FJKKC2N.js} +107 -77
  30. package/dist/chunk-4FJKKC2N.js.map +1 -0
  31. package/dist/{chunk-7C4MPEPE.js → chunk-4T7P2HLJ.js} +3 -3
  32. package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
  33. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  34. package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
  35. package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
  36. package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
  37. package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
  38. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  39. package/dist/{chunk-UXA5L2DZ.js → chunk-HQCGRSRU.js} +2 -2
  40. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  41. package/dist/{chunk-XW3W4PV4.js → chunk-JTPXSXHC.js} +2 -2
  42. package/dist/{chunk-OHJFJ4HI.js → chunk-KOXGLQS7.js} +2 -2
  43. package/dist/{chunk-2OPARZ4B.js → chunk-MPXYHC35.js} +26 -26
  44. package/dist/{chunk-6JBKHTQD.js → chunk-MR4PJ277.js} +2 -2
  45. package/dist/{chunk-EXXBA5OM.js → chunk-OI4BXFSB.js} +4 -4
  46. package/dist/{chunk-SQZ42MKH.js → chunk-OQH5XUH3.js} +6 -3
  47. package/dist/chunk-OQH5XUH3.js.map +1 -0
  48. package/dist/{chunk-2HEZXPYU.js → chunk-Q2LQZYQ7.js} +3 -3
  49. package/dist/{chunk-XRSIGVTS.js → chunk-QHWJG5C5.js} +8 -8
  50. package/dist/{chunk-T2AN3BSP.js → chunk-QZ7ODIVL.js} +2 -2
  51. package/dist/chunk-RI5XBIZ6.js +23 -0
  52. package/dist/chunk-RI5XBIZ6.js.map +1 -0
  53. package/dist/{chunk-D7IXTY5E.js → chunk-TJ7HH5LB.js} +2 -2
  54. package/dist/{chunk-V25ZAOSB.js → chunk-UOBLE67F.js} +4 -4
  55. package/dist/{chunk-JIX3ZL2J.js → chunk-UVUTV7CM.js} +15 -15
  56. package/dist/{chunk-VH6EIKVS.js → chunk-WKMCC4NQ.js} +35 -16
  57. package/dist/chunk-WKMCC4NQ.js.map +1 -0
  58. package/dist/{chunk-SSOMTUCA.js → chunk-WXGTC424.js} +1 -1
  59. package/dist/{chunk-KHGE6PMF.js → chunk-WXXLSZHA.js} +2 -2
  60. package/dist/{chunk-DSLUOQDY.js → chunk-XMWF6AU3.js} +2 -2
  61. package/dist/{chunk-DQY7NJ5L.js → chunk-XS2CWEHZ.js} +2 -2
  62. package/dist/{cli-BQRqR9N-.d.ts → cli-C98xlwYA.d.ts} +2 -2
  63. package/dist/cli.d.ts +4 -3
  64. package/dist/cli.js +42 -42
  65. package/dist/compounding/engine.js +4 -3
  66. package/dist/connectors/codex-materialize-runner.js +4 -3
  67. package/dist/connectors/index.js +4 -3
  68. package/dist/consolidation-provenance-check.js +2 -2
  69. package/dist/conversation-index/backend.js +2 -2
  70. package/dist/dashboard-runtime.js +2 -2
  71. package/dist/direct-answer-wiring.d.ts +13 -3
  72. package/dist/direct-answer-wiring.js +1 -1
  73. package/dist/entity-retrieval.js +4 -3
  74. package/dist/explicit-capture.d.ts +2 -1
  75. package/dist/index.d.ts +5 -4
  76. package/dist/index.js +66 -65
  77. package/dist/index.js.map +1 -1
  78. package/dist/lcm/engine.js +2 -2
  79. package/dist/lcm/index.js +4 -4
  80. package/dist/maintenance/memory-governance.js +4 -4
  81. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
  82. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  83. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  84. package/dist/namespaces/migrate.js +11 -11
  85. package/dist/namespaces/search.js +7 -7
  86. package/dist/namespaces/storage.js +4 -3
  87. package/dist/operator-toolkit.js +15 -15
  88. package/dist/{orchestrator-Cg1UkvmO.d.ts → orchestrator-DyP9QYsh.d.ts} +16 -0
  89. package/dist/orchestrator.d.ts +2 -1
  90. package/dist/orchestrator.js +32 -31
  91. package/dist/recall-planner-llm.d.ts +2 -1
  92. package/dist/recall-planner-llm.js +3 -2
  93. package/dist/recall-planner-llm.js.map +1 -1
  94. package/dist/schemas.d.ts +64 -64
  95. package/dist/search/factory.js +6 -6
  96. package/dist/search/index.js +10 -10
  97. package/dist/search/lancedb-backend.js +1 -1
  98. package/dist/search/meilisearch-backend.js +1 -1
  99. package/dist/search/orama-backend.js +1 -1
  100. package/dist/semantic-consolidation.js +5 -4
  101. package/dist/semantic-rule-promotion.js +4 -3
  102. package/dist/semantic-rule-verifier.js +4 -3
  103. package/dist/shared-context/manager.d.ts +2 -2
  104. package/dist/storage.js +3 -2
  105. package/dist/transfer/backup.js +2 -2
  106. package/dist/transfer/capsule-export.js +2 -2
  107. package/dist/transfer/capsule-import.js +1 -1
  108. package/dist/transfer/types.d.ts +12 -12
  109. package/dist/verified-recall.js +4 -3
  110. package/package.json +1 -1
  111. package/src/capabilities.test.ts +97 -0
  112. package/src/capabilities.ts +86 -0
  113. package/src/direct-answer-wiring.test.ts +53 -2
  114. package/src/direct-answer-wiring.ts +18 -5
  115. package/src/orchestrator.ts +83 -22
  116. package/src/recall-planner-llm.test.ts +12 -11
  117. package/src/recall-planner-llm.ts +7 -1
  118. package/src/storage-fallback-category-dirs.test.ts +150 -1
  119. package/src/storage.ts +51 -14
  120. package/dist/chunk-SQZ42MKH.js.map +0 -1
  121. package/dist/chunk-VH6EIKVS.js.map +0 -1
  122. package/dist/chunk-WH4SKYPX.js.map +0 -1
  123. /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
  124. /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
  125. /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
  126. /package/dist/{chunk-7C4MPEPE.js.map → chunk-4T7P2HLJ.js.map} +0 -0
  127. /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
  128. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  129. /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
  130. /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
  131. /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
  132. /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
  133. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  134. /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
  135. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  136. /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
  137. /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
  138. /package/dist/{chunk-2OPARZ4B.js.map → chunk-MPXYHC35.js.map} +0 -0
  139. /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
  140. /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
  141. /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
  142. /package/dist/{chunk-XRSIGVTS.js.map → chunk-QHWJG5C5.js.map} +0 -0
  143. /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
  144. /package/dist/{chunk-D7IXTY5E.js.map → chunk-TJ7HH5LB.js.map} +0 -0
  145. /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
  146. /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
  147. /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
  148. /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
  149. /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
  150. /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
4
  import { parseConfig } from "./config.js";
5
+ import { resolveCapabilities } from "./capabilities.js";
5
6
  import {
6
7
  planRecallModeLLM,
7
8
  resolveRecallPlannerLlmOptions,
@@ -40,7 +41,7 @@ test("returns heuristic without calling the LLM when recallPlannerLlmEnabled is
40
41
  const captured: Array<Record<string, unknown>> = [];
41
42
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "no_recall" } });
42
43
 
43
- const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm);
44
+ const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm, undefined, resolveCapabilities(config));
44
45
 
45
46
  assert.equal(captured.length, 0, "LLM must not be contacted when disabled");
46
47
  assert.equal(result.source, "heuristic");
@@ -54,7 +55,7 @@ test("uses the LLM classification when enabled", async () => {
54
55
  const config = parseConfig({ recallPlannerLlmEnabled: true });
55
56
  const llm = stubLlm({ result: { mode: "graph_mode", reason: "asks for root cause" }, modelUsed: "anthropic/claude" });
56
57
 
57
- const result = await planRecallModeLLM("restart the gateway", undefined, config, llm);
58
+ const result = await planRecallModeLLM("restart the gateway", undefined, config, llm, undefined, resolveCapabilities(config));
58
59
 
59
60
  assert.equal(result.source, "llm");
60
61
  assert.equal(result.mode, "graph_mode");
@@ -74,7 +75,7 @@ test("forwards taskModelChain AND recallPlannerModel in gateway mode (provider-a
74
75
  const captured: Array<Record<string, unknown>> = [];
75
76
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
76
77
 
77
- await planRecallModeLLM("check status", undefined, config, llm);
78
+ await planRecallModeLLM("check status", undefined, config, llm, undefined, resolveCapabilities(config));
78
79
 
79
80
  assert.equal(captured.length, 1);
80
81
  // recallPlannerModel is tried first (prepended), taskModelChain is the fallback chain.
@@ -97,7 +98,7 @@ test("plugin mode passes only the explicit model, no gateway chain", async () =>
97
98
  const captured: Array<Record<string, unknown>> = [];
98
99
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
99
100
 
100
- await planRecallModeLLM("summarize the project", undefined, config, llm);
101
+ await planRecallModeLLM("summarize the project", undefined, config, llm, undefined, resolveCapabilities(config));
101
102
 
102
103
  assert.equal(captured.length, 1);
103
104
  assert.equal(captured[0]?.model, "openai/gpt-5.5");
@@ -109,7 +110,7 @@ test("falls back to heuristic when the LLM throws", async () => {
109
110
  const config = parseConfig({ recallPlannerLlmEnabled: true });
110
111
  const llm = stubLlm({ throwError: "boom" });
111
112
 
112
- const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm);
113
+ const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm, undefined, resolveCapabilities(config));
113
114
 
114
115
  assert.equal(result.source, "heuristic-fallback");
115
116
  assert.equal(result.fallbackUsed, true);
@@ -123,7 +124,7 @@ test("falls back to heuristic when the LLM returns no parseable result", async (
123
124
  const config = parseConfig({ recallPlannerLlmEnabled: true });
124
125
  const llm = stubLlm({ result: null });
125
126
 
126
- const result = await planRecallModeLLM("how did we get here?", undefined, config, llm);
127
+ const result = await planRecallModeLLM("how did we get here?", undefined, config, llm, undefined, resolveCapabilities(config));
127
128
 
128
129
  assert.equal(result.source, "heuristic-fallback");
129
130
  assert.equal(result.fallbackUsed, true);
@@ -139,7 +140,7 @@ test("falls back without a network attempt when the chain is empty and the model
139
140
  const captured: Array<Record<string, unknown>> = [];
140
141
  const llm = stubLlm({ available: false, capturedOptions: captured, result: { mode: "full" } });
141
142
 
142
- const result = await planRecallModeLLM("anything", undefined, config, llm);
143
+ const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
143
144
 
144
145
  assert.equal(captured.length, 0, "no network attempt when nothing is routable");
145
146
  assert.equal(result.source, "heuristic-fallback");
@@ -155,7 +156,7 @@ test("attempts the call (and falls back) when a provider-qualified model overrid
155
156
  const captured: Array<Record<string, unknown>> = [];
156
157
  const llm = stubLlm({ available: false, capturedOptions: captured, result: null });
157
158
 
158
- const result = await planRecallModeLLM("anything", undefined, config, llm);
159
+ const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
159
160
 
160
161
  assert.equal(captured.length, 1, "qualified model override → still attempt the call");
161
162
  assert.equal(captured[0]?.model, "openai/gpt-5.5");
@@ -170,7 +171,7 @@ test("an already-aborted recall short-circuits to the heuristic without an LLM c
170
171
  const ac = new AbortController();
171
172
  ac.abort();
172
173
 
173
- const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal);
174
+ const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal, resolveCapabilities(config));
174
175
 
175
176
  assert.equal(captured.length, 0, "no LLM call when the recall is already aborted");
176
177
  assert.equal(result.source, "heuristic-fallback");
@@ -184,7 +185,7 @@ test("forwards the abort signal into the LLM call (cancellation contract)", asyn
184
185
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
185
186
  const ac = new AbortController();
186
187
 
187
- await planRecallModeLLM("check status", undefined, config, llm, ac.signal);
188
+ await planRecallModeLLM("check status", undefined, config, llm, ac.signal, resolveCapabilities(config));
188
189
 
189
190
  assert.equal(captured.length, 1);
190
191
  assert.equal(captured[0]?.signal, ac.signal, "recall abort signal must reach FallbackLlmClient");
@@ -195,7 +196,7 @@ test("empty prompts skip the LLM entirely", async () => {
195
196
  const captured: Array<Record<string, unknown>> = [];
196
197
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
197
198
 
198
- const result = await planRecallModeLLM(" ", undefined, config, llm);
199
+ const result = await planRecallModeLLM(" ", undefined, config, llm, undefined, resolveCapabilities(config));
199
200
 
200
201
  assert.equal(captured.length, 0);
201
202
  assert.equal(result.mode, "no_recall"); // heuristic returns no_recall for empty
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import type { PluginConfig, RecallPlanMode } from "./types.js";
4
+ import type { CapabilitySet } from "./capabilities.js";
4
5
  import { planRecallMode } from "./intent.js";
5
6
  import {
6
7
  FallbackLlmClient,
@@ -189,10 +190,15 @@ export async function planRecallModeLLM(
189
190
  config: PluginConfig,
190
191
  llm?: FallbackLlmClient,
191
192
  signal?: AbortSignal,
193
+ caps?: CapabilitySet,
192
194
  ): Promise<RecallPlannerLlmResult> {
193
195
  const heuristicMode = planRecallMode(prompt);
194
196
 
195
- if (!config.recallPlannerLlmEnabled) {
197
+ // `caps` is OPTIONAL and additive (issue #1523). Prefer the resolved
198
+ // capability when supplied; fall back to the config flag so existing callers
199
+ // that pass only `config` keep identical gating.
200
+ const plannerLlmEnabled = caps?.recallPlannerLlm ?? config.recallPlannerLlmEnabled;
201
+ if (!plannerLlmEnabled) {
196
202
  return heuristicResult(heuristicMode, "heuristic", "llm-disabled", 0, false);
197
203
  }
198
204
 
@@ -27,7 +27,7 @@
27
27
  */
28
28
 
29
29
  import assert from "node:assert/strict";
30
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
30
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
31
31
  import os from "node:os";
32
32
  import path from "node:path";
33
33
  import test from "node:test";
@@ -356,6 +356,155 @@ test("question-queue items written by writeQuestion() do NOT leak into fallback
356
356
  * directly with a config object that explicitly disables QMD, proving the disk
357
357
  * scan a QMD-disabled deployment relies on returns every category.
358
358
  */
359
+ /**
360
+ * Symlink containment (same walker-hardening class as PR #1563 / issue #1546).
361
+ *
362
+ * `collectActiveMemoryPaths()` walks the RECALL_FALLBACK_DIRS category dirs for
363
+ * the QMD-unavailable filesystem recall fallback. A category dir symlinked
364
+ * outside `memoryDir` (e.g. `decisions/` -> an external directory) must NOT be
365
+ * followed — otherwise out-of-store files leak into recall results. The walker
366
+ * now `lstat`s each dir (skipping symlinks/non-dirs), realpaths it, and asserts
367
+ * it stays inside the memory root; per entry it skips symlinks and asserts the
368
+ * file resolves inside the root before including it.
369
+ */
370
+ test("category dir symlinked outside memoryDir does NOT leak files into fallback recall", async (t) => {
371
+ if (process.platform === "win32") {
372
+ t.skip("directory symlink setup is platform-specific");
373
+ return;
374
+ }
375
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-dir-"));
376
+ const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-out-"));
377
+ const storage = new StorageManager(baseDir);
378
+ StorageManager.clearAllStaticCaches();
379
+ storage.invalidateAllMemoriesCacheForDir();
380
+ try {
381
+ // A genuine in-store memory that MUST still be recalled.
382
+ await seedMemory(baseDir, "facts", "real-fact", "fact", "A genuine in-store fact.");
383
+
384
+ // An out-of-store memory reachable only via a symlinked category dir.
385
+ await writeFile(
386
+ path.join(outsideDir, "leaked.md"),
387
+ memoryFile("leaked-secret", "decision", "Out-of-store file must never be recalled."),
388
+ "utf-8",
389
+ );
390
+ // `decisions/` is a RECALL_FALLBACK_DIRS category dir; point it outside the store.
391
+ await symlink(outsideDir, path.join(baseDir, "decisions"), "dir");
392
+
393
+ storage.invalidateAllMemoriesCacheForDir();
394
+ const memories = await storage.readAllMemories();
395
+ const foundIds = new Set(memories.map((m) => m.frontmatter.id));
396
+
397
+ assert.ok(foundIds.has("real-fact"), "the genuine in-store fact must still be recalled");
398
+ assert.ok(
399
+ !foundIds.has("leaked-secret"),
400
+ "a symlinked-out category dir must not leak files into recall",
401
+ );
402
+ assert.equal(memories.length, 1, "only the in-store memory should be returned");
403
+ } finally {
404
+ StorageManager.clearAllStaticCaches();
405
+ await rm(baseDir, { recursive: true, force: true });
406
+ await rm(outsideDir, { recursive: true, force: true });
407
+ }
408
+ });
409
+
410
+ test("symlinked entry nested inside a category dir does NOT leak into fallback recall", async (t) => {
411
+ if (process.platform === "win32") {
412
+ t.skip("directory symlink setup is platform-specific");
413
+ return;
414
+ }
415
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-entry-"));
416
+ const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-entry-out-"));
417
+ const storage = new StorageManager(baseDir);
418
+ StorageManager.clearAllStaticCaches();
419
+ storage.invalidateAllMemoriesCacheForDir();
420
+ try {
421
+ // A genuine in-store memory in the same category dir that holds the symlink.
422
+ await seedMemory(baseDir, "facts", "real-fact", "fact", "A genuine in-store fact.");
423
+
424
+ await writeFile(
425
+ path.join(outsideDir, "leaked.md"),
426
+ memoryFile("nested-leak", "fact", "Out-of-store nested file must never be recalled."),
427
+ "utf-8",
428
+ );
429
+ // A symlinked *entry* inside facts/ that escapes the store.
430
+ await symlink(outsideDir, path.join(baseDir, "facts", "escape"), "dir");
431
+
432
+ storage.invalidateAllMemoriesCacheForDir();
433
+ const memories = await storage.readAllMemories();
434
+ const foundIds = new Set(memories.map((m) => m.frontmatter.id));
435
+
436
+ assert.ok(foundIds.has("real-fact"), "the genuine in-store fact must still be recalled");
437
+ assert.ok(
438
+ !foundIds.has("nested-leak"),
439
+ "a symlinked entry escaping the store must not leak files into recall",
440
+ );
441
+ assert.equal(memories.length, 1, "only the in-store memory should be returned");
442
+ } finally {
443
+ StorageManager.clearAllStaticCaches();
444
+ await rm(baseDir, { recursive: true, force: true });
445
+ await rm(outsideDir, { recursive: true, force: true });
446
+ }
447
+ });
448
+
449
+ /**
450
+ * Regression for Cursor Bugbot "Poisoned md skips sibling subdirs".
451
+ *
452
+ * Per-entry containment/realpath failures must be isolated so they cannot drop
453
+ * sibling `.md` files or, crucially, the nested-subdir recursion. This asserts
454
+ * that a category dir which also contains a symlinked-out entry still recurses
455
+ * into its real nested subdirectory and returns every in-store memory, while
456
+ * the out-of-store target stays excluded.
457
+ */
458
+ test("a guarded/skipped sibling entry does NOT drop nested in-store memories", async (t) => {
459
+ if (process.platform === "win32") {
460
+ t.skip("directory symlink setup is platform-specific");
461
+ return;
462
+ }
463
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-sibling-"));
464
+ const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-sibling-out-"));
465
+ const storage = new StorageManager(baseDir);
466
+ StorageManager.clearAllStaticCaches();
467
+ storage.invalidateAllMemoriesCacheForDir();
468
+ try {
469
+ const factsDir = path.join(baseDir, "facts");
470
+ await mkdir(factsDir, { recursive: true });
471
+ // A top-level in-store memory and a nested in-store memory in the SAME
472
+ // category dir that also holds the symlinked-out sibling.
473
+ await writeFile(
474
+ path.join(factsDir, "top.md"),
475
+ memoryFile("top-fact", "fact", "Top-level in-store fact."),
476
+ "utf-8",
477
+ );
478
+ await seedMemory(baseDir, "facts", "deep-fact", "fact", "Deeply nested in-store fact.", {
479
+ nested: true,
480
+ });
481
+
482
+ await writeFile(
483
+ path.join(outsideDir, "leaked.md"),
484
+ memoryFile("leaked-secret", "fact", "Out-of-store file must never be recalled."),
485
+ "utf-8",
486
+ );
487
+ // A symlinked-out sibling entry alongside the real files/subdir.
488
+ await symlink(outsideDir, path.join(factsDir, "escape"), "dir");
489
+
490
+ storage.invalidateAllMemoriesCacheForDir();
491
+ const memories = await storage.readAllMemories();
492
+ const foundIds = new Set(memories.map((m) => m.frontmatter.id));
493
+
494
+ assert.ok(foundIds.has("top-fact"), "the top-level in-store fact must be recalled");
495
+ assert.ok(
496
+ foundIds.has("deep-fact"),
497
+ "the nested in-store fact must still be recalled past a skipped sibling",
498
+ );
499
+ assert.ok(!foundIds.has("leaked-secret"), "the symlinked-out file must not leak");
500
+ assert.equal(memories.length, 2, "exactly the two in-store memories should be returned");
501
+ } finally {
502
+ StorageManager.clearAllStaticCaches();
503
+ await rm(baseDir, { recursive: true, force: true });
504
+ await rm(outsideDir, { recursive: true, force: true });
505
+ }
506
+ });
507
+
359
508
  test("QMD-disabled deployment: disk-scan collector returns all categories", async () => {
360
509
  const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-qmd-off-"));
361
510
  // entitySchemas is the only other constructor arg; QMD is never constructed by
package/src/storage.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { access, readdir, readFile, stat, writeFile, mkdir, unlink, rename, appendFile, open } from "node:fs/promises";
2
- import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync } from "node:fs";
1
+ import { access, lstat, readdir, readFile, realpath, stat, writeFile, mkdir, unlink, rename, appendFile, open } from "node:fs/promises";
2
+ import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync, type Dirent } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { log } from "./logger.js";
6
6
  import { isErrnoCode } from "./utils/errno.js";
7
7
  import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
8
+ import { assertPathInsideRoot } from "./utils/path-containment.js";
8
9
  import { getCachedEntities, invalidateAllForDir, setCachedEntities } from "./memory-cache.js";
9
10
  import { rotateMarkdownFileToArchive } from "./hygiene.js";
10
11
  import { sanitizeMemoryContent } from "./sanitize.js";
@@ -4082,23 +4083,59 @@ export class StorageManager {
4082
4083
  private async collectActiveMemoryPaths(): Promise<string[]> {
4083
4084
  const filePaths: string[] = [];
4084
4085
 
4086
+ // Resolve the memory root once for containment checks below. A category dir
4087
+ // symlinked outside memoryDir (e.g. decisions/ -> an external dir) must NOT
4088
+ // pull out-of-store files into the QMD-unavailable recall fallback (info
4089
+ // leak). Same walker-hardening pattern as document-scanner.ts / cli.ts /
4090
+ // consolidation-provenance-check.ts; reuses the shared containment helper.
4091
+ let memoryRootReal: string;
4092
+ try {
4093
+ memoryRootReal = await realpath(this.baseDir);
4094
+ } catch {
4095
+ return filePaths;
4096
+ }
4097
+
4085
4098
  const collectPaths = async (dir: string) => {
4099
+ // Directory-level guard, isolated from per-entry handling: skip symlinked
4100
+ // or non-directory category dirs and assert the resolved dir stays inside
4101
+ // the memory root before reading. A failure here means the whole subtree
4102
+ // does not exist or escaped the store — fail closed by skipping it.
4103
+ let entries: Dirent[];
4086
4104
  try {
4087
- const entries = await readdir(dir, { withFileTypes: true });
4088
- const subdirs: string[] = [];
4089
- for (const entry of entries) {
4090
- const fullPath = path.join(dir, entry.name);
4091
- if (entry.isDirectory()) {
4092
- subdirs.push(fullPath);
4093
- } else if (entry.name.endsWith(".md")) {
4105
+ const dirStat = await lstat(dir);
4106
+ if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
4107
+ assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);
4108
+ entries = await readdir(dir, { withFileTypes: true });
4109
+ } catch {
4110
+ return;
4111
+ }
4112
+
4113
+ const subdirs: string[] = [];
4114
+ for (const entry of entries) {
4115
+ // Never follow symlinked entries out of the store.
4116
+ if (entry.isSymbolicLink()) continue;
4117
+ const fullPath = path.join(dir, entry.name);
4118
+ if (entry.isDirectory()) {
4119
+ subdirs.push(fullPath);
4120
+ } else if (entry.name.endsWith(".md")) {
4121
+ // Isolate per-entry failures in their own try/catch: a containment or
4122
+ // realpath failure on ONE .md entry must not drop sibling files or,
4123
+ // crucially, the deferred subdir recursion below (Cursor Bugbot:
4124
+ // "Poisoned md skips sibling subdirs"). Mirrors the per-file try/catch
4125
+ // in search/document-scanner.ts scanDir and
4126
+ // consolidation-provenance-check.ts walkMarkdownFiles.
4127
+ try {
4128
+ assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
4094
4129
  filePaths.push(fullPath);
4130
+ } catch {
4131
+ // Skip just this entry (symlink/containment/realpath failure).
4095
4132
  }
4096
4133
  }
4097
- for (const subdir of subdirs) {
4098
- await collectPaths(subdir);
4099
- }
4100
- } catch {
4101
- // Directory does not exist yet.
4134
+ }
4135
+ // Recurse into real subdirectories regardless of any single poisoned entry
4136
+ // above, so valid nested in-store memories are never dropped.
4137
+ for (const subdir of subdirs) {
4138
+ await collectPaths(subdir);
4102
4139
  }
4103
4140
  };
4104
4141
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/taxonomy/resolver.ts","../src/direct-answer-wiring.ts"],"sourcesContent":["/**\n * Resolver decision tree for the MECE taxonomy.\n *\n * Given extracted content and its MemoryCategory, determines which\n * taxonomy category the knowledge should be filed under.\n */\n\nimport type { MemoryCategory } from \"../types.js\";\nimport type { ResolverDecision, Taxonomy, TaxonomyCategory } from \"./types.js\";\n\nconst DEFAULT_CATEGORY_ID = \"facts\";\n\n/**\n * Resolve a piece of content to a taxonomy category.\n *\n * Algorithm:\n * 1. Find all taxonomy categories whose `memoryCategories` include\n * the given `memoryCategory`.\n * 2. If exactly one match, return it with confidence 1.0.\n * 3. If multiple matches, prefer the strongest exact-token keyword\n * overlap from filing rules. Use priority only as a tie-breaker.\n * If no filing rules match, choose the explicit/default fallback\n * category instead of letting low-priority generic terms win.\n * 4. If no match, fall back to the \"facts\" category (or first\n * category if \"facts\" is absent) with low confidence.\n * 5. Always populate `alternatives` with other plausible categories.\n */\nexport function resolveCategory(\n content: string,\n memoryCategory: MemoryCategory,\n taxonomy: Taxonomy,\n): ResolverDecision {\n const contentTokens = tokenizeKeywordText(content);\n\n // Step 1: find matching categories\n const matches = taxonomy.categories.filter((cat) =>\n cat.memoryCategories.includes(memoryCategory),\n );\n\n if (matches.length === 0) {\n // No taxonomy category accepts this MemoryCategory — fall back\n const fallback =\n taxonomy.categories.find((c) => c.id === DEFAULT_CATEGORY_ID) ??\n taxonomy.categories[0];\n if (!fallback) {\n return {\n categoryId: DEFAULT_CATEGORY_ID,\n confidence: 0,\n reason: \"Taxonomy is empty; using default category\",\n alternatives: [],\n };\n }\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== fallback.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n return {\n categoryId: fallback.id,\n confidence: 0.3,\n reason: `No taxonomy category maps to MemoryCategory \"${memoryCategory}\"; falling back to \"${fallback.name}\"`,\n alternatives,\n };\n }\n\n if (matches.length === 1) {\n const match = matches[0]!;\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== match.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n return {\n categoryId: match.id,\n confidence: 1.0,\n reason: `Unique match: MemoryCategory \"${memoryCategory}\" maps to \"${match.name}\"`,\n alternatives,\n };\n }\n\n // Multiple matches — use filing rule keyword heuristics + priority\n const scored = matches.map((cat) => ({\n cat,\n keywordScore: computeKeywordScoreForTokens(contentTokens, cat),\n }));\n\n // Sort by keyword score descending, then priority ascending (lower wins)\n scored.sort((a, b) => {\n if (b.keywordScore !== a.keywordScore) return b.keywordScore - a.keywordScore;\n return a.cat.priority - b.cat.priority;\n });\n\n const topScored = scored[0]!;\n const best = topScored.keywordScore > 0\n ? topScored\n : {\n cat: selectFallbackCategory(matches) ?? topScored.cat,\n keywordScore: 0,\n };\n const runnerUp = scored[1];\n\n // Confidence is higher when keyword match clearly differentiates\n const confidence =\n best.keywordScore > 0 && (!runnerUp || best.keywordScore > runnerUp.keywordScore)\n ? 0.9\n : 0.7;\n\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== best.cat.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n\n const reason =\n best.keywordScore > 0\n ? `Filing rules for \"${best.cat.name}\" matched content keywords (priority ${best.cat.priority})`\n : `No filing rules matched content keywords; using fallback category \"${best.cat.name}\"`;\n\n return {\n categoryId: best.cat.id,\n confidence,\n reason,\n alternatives,\n };\n}\n\n/**\n * Compute a simple keyword overlap score between content and\n * a category's filing rules + description.\n */\nfunction computeKeywordScoreForTokens(contentTokens: Set<string>, cat: TaxonomyCategory): number {\n let score = 0;\n const ruleText = [...cat.filingRules, cat.description]\n .join(\" \")\n .toLowerCase();\n\n const keywords = tokenizeKeywordText(ruleText);\n\n for (const kw of keywords) {\n if (contentTokens.has(kw)) {\n score += 1;\n }\n }\n return score;\n}\n\nfunction tokenizeKeywordText(value: string): Set<string> {\n return new Set(\n value\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .map((word) => word.trim())\n .filter((word) => word.length >= 3 && !TAXONOMY_KEYWORD_STOPWORDS.has(word)),\n );\n}\n\nfunction selectFallbackCategory(\n categories: readonly TaxonomyCategory[],\n): TaxonomyCategory | undefined {\n return categories.find((cat) => cat.id === DEFAULT_CATEGORY_ID) ??\n categories.find((cat) => {\n const text = `${cat.id} ${cat.name} ${cat.description} ${cat.filingRules.join(\" \")}`.toLowerCase();\n return /\\b(general|fallback)\\b/.test(text);\n });\n}\n\nconst TAXONOMY_KEYWORD_STOPWORDS = new Set([\n \"about\",\n \"all\",\n \"and\",\n \"are\",\n \"for\",\n \"from\",\n \"has\",\n \"into\",\n \"not\",\n \"the\",\n \"this\",\n \"was\",\n \"with\",\n]);\n","/**\n * Direct-answer wiring (issue #518 slice 3).\n *\n * Binds the pure eligibility decision (`direct-answer.ts`) to the data\n * sources needed to build candidates: storage, trust-zones, taxonomy,\n * and importance scoring. Kept as a separate module so that:\n *\n * - The eligibility layer stays pure and unit-testable without stores.\n * - Each caller injects its own source accessors. The orchestrator\n * binding is a follow-on slice; tests here use mock sources.\n * - The wiring is safe to ship alone — nothing calls `tryDirectAnswer`\n * yet, so enabling this module's presence does not change recall\n * behavior. The next slice adds exactly one call site before QMD.\n *\n * Short-circuit contract:\n *\n * - When `config.recallDirectAnswerEnabled === false`, the function\n * returns the eligibility verdict with reason `\"disabled\"` without\n * touching any source accessor. This is the documented default.\n * - When enabled, the wiring cheaply drops non-trusted-zone memories\n * and ineligible taxonomy buckets before computing importance, so\n * the eligibility module sees a pre-filtered candidate set. The\n * eligibility module still performs the same checks itself — this\n * module is purely an I/O and prefiltering layer.\n */\n\nimport type { MemoryFile, PluginConfig } from \"./types.js\";\nimport type { TrustZoneName } from \"./trust-zones.js\";\nimport type { Taxonomy } from \"./taxonomy/types.js\";\nimport { resolveCategory } from \"./taxonomy/resolver.js\";\nimport { normalizeRecallTokens } from \"./recall-tokenization.js\";\nimport { throwIfAborted } from \"./abort-error.js\";\nimport {\n isDirectAnswerEligible,\n type DirectAnswerCandidate,\n type DirectAnswerConfig,\n type DirectAnswerResult,\n} from \"./direct-answer.js\";\n\n/**\n * Caller-provided accessors for candidate sourcing. Decouples the\n * wiring from any specific storage / trust-zone / importance backend.\n */\nexport interface DirectAnswerSources {\n /**\n * List memories eligible to be considered for direct-answer.\n * Callers are expected to return only active, non-superseded memories\n * in the requested namespace; the wiring will cheaply re-filter on\n * trust zone and taxonomy bucket and hand the rest to the eligibility\n * module, which applies the full gate ladder.\n */\n listCandidateMemories(options: {\n namespace: string;\n abortSignal?: AbortSignal;\n }): Promise<MemoryFile[]>;\n /**\n * Resolve the trust-zone record for a memory. Returns `null` when\n * the memory has no trust-zone record (treated as not trusted).\n */\n trustZoneFor(memoryId: string): Promise<TrustZoneName | null>;\n /**\n * Resolve a calibrated importance score in [0, 1] for a memory.\n */\n importanceFor(memory: MemoryFile): number;\n /**\n * Taxonomy used to classify memories into direct-answer buckets.\n */\n taxonomy: Taxonomy;\n}\n\nexport interface DirectAnswerWiringInput {\n query: string;\n namespace: string;\n config: Pick<\n PluginConfig,\n | \"recallDirectAnswerEnabled\"\n | \"recallDirectAnswerTokenOverlapFloor\"\n | \"recallDirectAnswerImportanceFloor\"\n | \"recallDirectAnswerAmbiguityMargin\"\n | \"recallDirectAnswerEligibleTaxonomyBuckets\"\n >;\n sources: DirectAnswerSources;\n queryEntityRefs?: string[];\n abortSignal?: AbortSignal;\n}\n\n/**\n * Attempt direct-answer resolution. Returns the eligibility verdict\n * produced by `isDirectAnswerEligible` with candidates materialized\n * from the caller-supplied sources.\n */\nexport async function tryDirectAnswer(\n input: DirectAnswerWiringInput,\n): Promise<DirectAnswerResult> {\n const { query, namespace, config, sources, queryEntityRefs, abortSignal } = input;\n\n const eligibilityConfig: DirectAnswerConfig = {\n enabled: config.recallDirectAnswerEnabled,\n tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,\n importanceFloor: config.recallDirectAnswerImportanceFloor,\n ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,\n eligibleTaxonomyBuckets: config.recallDirectAnswerEligibleTaxonomyBuckets,\n };\n\n // Short-circuit disabled case before touching any I/O.\n if (!eligibilityConfig.enabled) {\n return isDirectAnswerEligible({\n query,\n candidates: [],\n config: eligibilityConfig,\n queryEntityRefs,\n });\n }\n\n // Short-circuit empty-query case before any I/O. isDirectAnswerEligible\n // deterministically returns reason \"empty-query\" when the query\n // normalizes to zero searchable tokens; there's no point materializing\n // candidates just to reach the same verdict, and doing so would\n // surface avoidable upstream errors for requests that should exit\n // immediately.\n if (normalizeRecallTokens(query).length === 0) {\n return isDirectAnswerEligible({\n query,\n candidates: [],\n config: eligibilityConfig,\n queryEntityRefs,\n });\n }\n\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n const memories = await sources.listCandidateMemories({ namespace, abortSignal });\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n const candidates: DirectAnswerCandidate[] = [];\n\n for (const memory of memories) {\n // Throw rather than returning a partial verdict — a mid-loop abort\n // that left competing candidates unprocessed could otherwise surface\n // a spurious \"eligible\" result that the ambiguity gate never got a\n // chance to reject. The check repeats after every await so an\n // abort that lands during the in-flight I/O on the final memory\n // (after which no further iteration would exist) still stops us.\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n const trustZone = await sources.trustZoneFor(memory.frontmatter.id);\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n // Cheap pre-filter: non-trusted memories can't qualify, so skip\n // taxonomy and importance resolution for them.\n if (trustZone !== \"trusted\") continue;\n\n const decision = resolveCategory(\n memory.content,\n memory.frontmatter.category,\n sources.taxonomy,\n );\n const taxonomyBucket = decision.categoryId;\n if (!eligibilityConfig.eligibleTaxonomyBuckets.includes(taxonomyBucket)) continue;\n\n const importanceScore = sources.importanceFor(memory);\n\n candidates.push({\n memory,\n trustZone,\n taxonomyBucket,\n importanceScore,\n });\n }\n\n // Final check — if abort landed during the trust-zone await for the\n // last memory, the loop condition no longer fires. Guard before we\n // hand candidates to the eligibility gate.\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n return isDirectAnswerEligible({\n query,\n candidates,\n config: eligibilityConfig,\n queryEntityRefs,\n });\n}\n"],"mappings":"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","names":["alternatives"]}