@remnic/core 9.4.2 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +28 -28
  3. package/dist/access-boundary.d.ts +27 -8
  4. package/dist/access-boundary.js +33 -29
  5. package/dist/access-cli.js +83 -81
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +52 -6
  8. package/dist/access-http.js +40 -38
  9. package/dist/access-identity-continuity-surface.d.ts +5 -5
  10. package/dist/access-identity-continuity-surface.js +28 -28
  11. package/dist/access-lcm-surface.d.ts +6 -6
  12. package/dist/access-lcm-surface.js +28 -28
  13. package/dist/access-mcp.d.ts +16 -6
  14. package/dist/access-mcp.js +37 -35
  15. package/dist/access-observe-write-surface.d.ts +6 -6
  16. package/dist/access-observe-write-surface.js +28 -28
  17. package/dist/access-operations-batch.js +33 -32
  18. package/dist/access-operations.d.ts +9 -9
  19. package/dist/access-operations.js +35 -34
  20. package/dist/access-recall-surface.d.ts +6 -6
  21. package/dist/access-recall-surface.js +28 -28
  22. package/dist/access-schema.d.ts +4 -4
  23. package/dist/access-schema.js +4 -4
  24. package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
  25. package/dist/access-service.d.ts +6 -6
  26. package/dist/access-service.js +28 -28
  27. package/dist/access-surface-catalog.d.ts +6 -6
  28. package/dist/access-surface-catalog.js +1 -0
  29. package/dist/access-surface-catalog.js.map +1 -1
  30. package/dist/access-token-capabilities.d.ts +174 -0
  31. package/dist/access-token-capabilities.js +33 -0
  32. package/dist/action-confidence.d.ts +1 -1
  33. package/dist/active-memory-bridge.d.ts +1 -1
  34. package/dist/active-recall.d.ts +1 -1
  35. package/dist/active-recall.js +2 -2
  36. package/dist/adapters/index.js +4 -4
  37. package/dist/adapters/registry.js +2 -2
  38. package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
  39. package/dist/behavior-learner.d.ts +1 -1
  40. package/dist/behavior-signals.d.ts +1 -1
  41. package/dist/bootstrap.d.ts +5 -5
  42. package/dist/briefing.d.ts +2 -2
  43. package/dist/briefing.js +9 -9
  44. package/dist/buffer-surprise-report.d.ts +1 -1
  45. package/dist/buffer.d.ts +2 -2
  46. package/dist/calibration.d.ts +1 -1
  47. package/dist/capabilities.d.ts +1 -1
  48. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
  49. package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
  50. package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
  51. package/dist/causal-behavior.d.ts +1 -1
  52. package/dist/causal-behavior.js +5 -5
  53. package/dist/causal-chain.js +3 -3
  54. package/dist/causal-consolidation.d.ts +1 -1
  55. package/dist/causal-consolidation.js +13 -13
  56. package/dist/causal-retrieval.js +3 -3
  57. package/dist/causal-trajectory-graph.d.ts +1 -1
  58. package/dist/causal-trajectory.js +1 -1
  59. package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
  60. package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
  61. package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
  62. package/dist/chunk-4GNEDXDI.js.map +1 -0
  63. package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
  64. package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
  65. package/dist/chunk-4OKLXWDZ.js.map +1 -0
  66. package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
  67. package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
  68. package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
  69. package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
  70. package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
  71. package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
  72. package/dist/chunk-B3ABVQ36.js.map +1 -0
  73. package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
  74. package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
  75. package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
  76. package/dist/chunk-D7FOKIY5.js +173 -0
  77. package/dist/chunk-D7FOKIY5.js.map +1 -0
  78. package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
  79. package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
  80. package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
  81. package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
  82. package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
  83. package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
  84. package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
  85. package/dist/chunk-IC2TCZJ2.js +54 -0
  86. package/dist/chunk-IC2TCZJ2.js.map +1 -0
  87. package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
  88. package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
  89. package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
  90. package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
  91. package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
  92. package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
  93. package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
  94. package/dist/chunk-LADFWYHY.js.map +1 -0
  95. package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
  96. package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
  97. package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
  98. package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
  99. package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
  100. package/dist/chunk-P454KXD6.js.map +1 -0
  101. package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
  102. package/dist/chunk-PCLIWBGD.js.map +1 -0
  103. package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
  104. package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
  105. package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
  106. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  107. package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
  108. package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
  109. package/dist/chunk-SVOHGUMI.js.map +1 -0
  110. package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
  111. package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
  112. package/dist/chunk-UFUKHGZD.js.map +1 -0
  113. package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
  114. package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
  115. package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
  116. package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
  117. package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
  118. package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
  119. package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
  120. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  121. package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
  122. package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
  123. package/dist/cli.d.ts +7 -7
  124. package/dist/cli.js +61 -59
  125. package/dist/compounding/engine.d.ts +2 -2
  126. package/dist/compounding/engine.js +9 -9
  127. package/dist/compounding/preference-consolidator.d.ts +1 -1
  128. package/dist/compression-optimizer.d.ts +1 -1
  129. package/dist/config.d.ts +1 -1
  130. package/dist/config.js +2 -2
  131. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  132. package/dist/connectors/codex-materialize-runner.js +9 -9
  133. package/dist/connectors/codex-materialize.d.ts +1 -1
  134. package/dist/connectors/index.d.ts +1 -1
  135. package/dist/connectors/index.js +13 -11
  136. package/dist/consolidation-provenance-check.d.ts +2 -2
  137. package/dist/consolidation-undo.d.ts +2 -2
  138. package/dist/contradiction/index.d.ts +2 -2
  139. package/dist/contradiction/index.js +4 -4
  140. package/dist/conversation-index/backend.d.ts +1 -1
  141. package/dist/conversation-index/backend.js +2 -2
  142. package/dist/conversation-index/chunker.d.ts +1 -1
  143. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  144. package/dist/conversation-index/indexer.d.ts +1 -1
  145. package/dist/conversation-index/search.d.ts +1 -1
  146. package/dist/dashboard-runtime.js +2 -2
  147. package/dist/day-summary.d.ts +1 -1
  148. package/dist/delinearize.d.ts +1 -1
  149. package/dist/direct-answer-wiring.d.ts +1 -1
  150. package/dist/direct-answer.d.ts +1 -1
  151. package/dist/embedding-fallback.d.ts +1 -1
  152. package/dist/enrichment/index.d.ts +1 -1
  153. package/dist/entity-retrieval.d.ts +2 -2
  154. package/dist/entity-retrieval.js +9 -9
  155. package/dist/entity-schema.d.ts +1 -1
  156. package/dist/explicit-capture.d.ts +5 -5
  157. package/dist/extraction-faithfulness.d.ts +1 -1
  158. package/dist/extraction-judge-telemetry.d.ts +1 -1
  159. package/dist/extraction-judge-training.d.ts +1 -1
  160. package/dist/extraction-judge.d.ts +1 -1
  161. package/dist/extraction.d.ts +1 -1
  162. package/dist/fallback-llm.d.ts +1 -1
  163. package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
  164. package/dist/graph-dashboard-diff.d.ts +1 -1
  165. package/dist/graph-dashboard-key.d.ts +1 -1
  166. package/dist/graph-dashboard-parser.d.ts +1 -1
  167. package/dist/graph-edge-reinforcement.d.ts +1 -1
  168. package/dist/graph-snapshot.d.ts +1 -1
  169. package/dist/graph.d.ts +1 -1
  170. package/dist/identity-continuity.d.ts +1 -1
  171. package/dist/importance.d.ts +1 -1
  172. package/dist/index.d.ts +271 -17
  173. package/dist/index.js +163 -99
  174. package/dist/intent.d.ts +1 -1
  175. package/dist/lcm/engine.d.ts +1 -1
  176. package/dist/lcm/engine.js +2 -2
  177. package/dist/lcm/index.d.ts +1 -1
  178. package/dist/lcm/index.js +2 -2
  179. package/dist/lcm/tools.d.ts +1 -1
  180. package/dist/lifecycle.d.ts +1 -1
  181. package/dist/live-connectors-runner.d.ts +1 -1
  182. package/dist/local-llm.d.ts +1 -1
  183. package/dist/local-model-endpoint.d.ts +1 -1
  184. package/dist/maintenance/memory-governance.d.ts +1 -1
  185. package/dist/maintenance/memory-governance.js +10 -10
  186. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
  187. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  188. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  189. package/dist/mcp-read-only-tools.d.ts +16 -0
  190. package/dist/mcp-read-only-tools.js +8 -0
  191. package/dist/mcp-read-only-tools.js.map +1 -0
  192. package/dist/memory-action-policy.d.ts +1 -1
  193. package/dist/memory-cache.d.ts +1 -1
  194. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  195. package/dist/memory-projection-store.d.ts +1 -1
  196. package/dist/memory-provenance.d.ts +1 -1
  197. package/dist/memory-worth-outcomes.d.ts +2 -2
  198. package/dist/models-json.d.ts +1 -1
  199. package/dist/namespaces/migrate.d.ts +3 -3
  200. package/dist/namespaces/migrate.js +19 -19
  201. package/dist/namespaces/principal.d.ts +1 -1
  202. package/dist/namespaces/search.d.ts +1 -1
  203. package/dist/namespaces/search.js +10 -10
  204. package/dist/namespaces/storage.d.ts +3 -3
  205. package/dist/namespaces/storage.js +9 -9
  206. package/dist/native-knowledge.d.ts +1 -1
  207. package/dist/operator-toolkit.d.ts +2 -2
  208. package/dist/operator-toolkit.js +25 -25
  209. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  210. package/dist/orchestration/maintenance.d.ts +2 -2
  211. package/dist/orchestration/maintenance.js +11 -11
  212. package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
  213. package/dist/orchestrator.d.ts +5 -5
  214. package/dist/orchestrator.js +85 -83
  215. package/dist/patterns-cli.d.ts +1 -1
  216. package/dist/policy-runtime.d.ts +1 -1
  217. package/dist/provenance.d.ts +1 -1
  218. package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
  219. package/dist/qmd-preflight.d.ts +2 -2
  220. package/dist/qmd-recall-cache.d.ts +1 -1
  221. package/dist/qmd.d.ts +2 -2
  222. package/dist/recall-disclosure-escalation.d.ts +1 -1
  223. package/dist/recall-explain-renderer.d.ts +1 -1
  224. package/dist/recall-planner-llm.d.ts +1 -1
  225. package/dist/recall-state.d.ts +1 -1
  226. package/dist/recall-tag-filter.d.ts +1 -1
  227. package/dist/recall-timings.d.ts +1 -1
  228. package/dist/recall-xray-cli.d.ts +1 -1
  229. package/dist/recall-xray-renderer.d.ts +1 -1
  230. package/dist/recall-xray.d.ts +1 -1
  231. package/dist/resolve-auth-token.d.ts +1 -1
  232. package/dist/resume-bundles.js +4 -4
  233. package/dist/retrieval-agents.d.ts +2 -2
  234. package/dist/retrieval-tiers.d.ts +1 -1
  235. package/dist/routing/engine.d.ts +1 -1
  236. package/dist/routing/store.d.ts +1 -1
  237. package/dist/schemas.d.ts +22 -22
  238. package/dist/search/document-scanner.js +2 -2
  239. package/dist/search/embed-helper.d.ts +1 -1
  240. package/dist/search/factory.d.ts +1 -1
  241. package/dist/search/factory.js +9 -9
  242. package/dist/search/index.d.ts +1 -1
  243. package/dist/search/index.js +13 -13
  244. package/dist/search/lancedb-backend.d.ts +1 -1
  245. package/dist/search/lancedb-backend.js +3 -3
  246. package/dist/search/meilisearch-backend.d.ts +1 -1
  247. package/dist/search/meilisearch-backend.js +3 -3
  248. package/dist/search/noop-backend.d.ts +1 -1
  249. package/dist/search/orama-backend.d.ts +1 -1
  250. package/dist/search/orama-backend.js +3 -3
  251. package/dist/search/port.d.ts +1 -1
  252. package/dist/search/remote-backend.d.ts +1 -1
  253. package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
  254. package/dist/semantic-consolidation.d.ts +2 -2
  255. package/dist/semantic-consolidation.js +11 -11
  256. package/dist/semantic-rule-promotion.js +9 -9
  257. package/dist/semantic-rule-verifier.d.ts +1 -1
  258. package/dist/semantic-rule-verifier.js +10 -10
  259. package/dist/session-observer-bands.d.ts +1 -1
  260. package/dist/session-observer-state.d.ts +1 -1
  261. package/dist/shared-context/manager.d.ts +1 -1
  262. package/dist/signal.d.ts +1 -1
  263. package/dist/storage-C1maCNbc.d.ts +1591 -0
  264. package/dist/storage.d.ts +5 -1239
  265. package/dist/storage.js +8 -8
  266. package/dist/summarizer.d.ts +1 -1
  267. package/dist/summary-snapshot.d.ts +1 -1
  268. package/dist/temporal-supersession.d.ts +2 -2
  269. package/dist/temporal-validity.d.ts +1 -1
  270. package/dist/threading.d.ts +1 -1
  271. package/dist/tier-migration.d.ts +2 -2
  272. package/dist/tier-routing.d.ts +1 -1
  273. package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
  274. package/dist/tokens.d.ts +17 -2
  275. package/dist/tokens.js +3 -1
  276. package/dist/topics.d.ts +1 -1
  277. package/dist/transcript.d.ts +1 -1
  278. package/dist/transfer/backup.js +2 -2
  279. package/dist/transfer/capsule-export.js +3 -3
  280. package/dist/transfer/capsule-import.js +3 -3
  281. package/dist/transfer/types.d.ts +12 -12
  282. package/dist/trust-score-stage.d.ts +1 -1
  283. package/dist/trust-score.d.ts +1 -1
  284. package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
  285. package/dist/types.d.ts +1 -1
  286. package/dist/utility-runtime.d.ts +1 -1
  287. package/dist/verified-recall.js +10 -10
  288. package/package.json +2 -2
  289. package/src/access-boundary.test.ts +62 -0
  290. package/src/access-boundary.ts +165 -126
  291. package/src/access-http.test.ts +781 -0
  292. package/src/access-http.ts +334 -108
  293. package/src/access-mcp.test.ts +264 -0
  294. package/src/access-mcp.ts +45 -70
  295. package/src/access-operations-batch.ts +35 -12
  296. package/src/access-surface-catalog.test.ts +22 -14
  297. package/src/access-surface-catalog.ts +6 -3
  298. package/src/access-token-capabilities.test.ts +844 -0
  299. package/src/access-token-capabilities.ts +391 -0
  300. package/src/chat/chat-mcp.test.ts +232 -0
  301. package/src/chat/chat-session.ts +34 -0
  302. package/src/cli-wearables-fuse.test.ts +197 -0
  303. package/src/cli.ts +25 -0
  304. package/src/index.ts +14 -0
  305. package/src/mcp-read-only-tools.ts +61 -0
  306. package/src/storage.ts +16 -0
  307. package/src/tokens.ts +32 -3
  308. package/src/wearables/cli.test.ts +102 -0
  309. package/src/wearables/cli.ts +65 -0
  310. package/src/wearables/config.test.ts +32 -0
  311. package/src/wearables/config.ts +52 -0
  312. package/src/wearables/day-store.test.ts +246 -0
  313. package/src/wearables/day-store.ts +272 -3
  314. package/src/wearables/fusion/cluster.ts +230 -0
  315. package/src/wearables/fusion/fuse.ts +218 -0
  316. package/src/wearables/fusion/fusion.test.ts +2575 -0
  317. package/src/wearables/fusion/index.ts +50 -0
  318. package/src/wearables/fusion/reconcile.ts +831 -0
  319. package/src/wearables/fusion/reconstruct.ts +251 -0
  320. package/src/wearables/fusion/store.ts +480 -0
  321. package/src/wearables/fusion/types.ts +228 -0
  322. package/src/wearables/index.ts +41 -0
  323. package/src/wearables/service.test.ts +1030 -2
  324. package/src/wearables/service.ts +258 -16
  325. package/src/wearables/speakers.test.ts +128 -0
  326. package/src/wearables/speakers.ts +58 -4
  327. package/src/wearables/storage-io.test.ts +146 -0
  328. package/src/wearables/types.ts +31 -0
  329. package/dist/chunk-2FOD2YY3.js.map +0 -1
  330. package/dist/chunk-7QWMTOIF.js.map +0 -1
  331. package/dist/chunk-DR6PEJU3.js.map +0 -1
  332. package/dist/chunk-E2SPGGUI.js.map +0 -1
  333. package/dist/chunk-FZGGZAAU.js.map +0 -1
  334. package/dist/chunk-ILFJ7AVS.js.map +0 -1
  335. package/dist/chunk-M7GGMGMZ.js.map +0 -1
  336. package/dist/chunk-M7XQSUBB.js.map +0 -1
  337. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
  338. /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
  339. /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
  340. /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
  341. /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
  342. /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
  343. /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
  344. /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
  345. /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
  346. /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
  347. /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
  348. /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
  349. /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
  350. /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
  351. /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
  352. /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
  353. /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
  354. /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
  355. /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
  356. /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
  357. /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
  358. /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
  359. /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
  360. /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
  361. /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
  362. /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
  363. /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
  364. /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
  365. /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
  366. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  367. /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
  368. /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
  369. /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
  370. /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
  371. /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
  372. /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
  373. /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
  374. /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
  375. /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
  376. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  377. /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
  378. /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
  379. /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
@@ -7,8 +7,11 @@ import { gzipSync } from "node:zlib";
7
7
 
8
8
  import { EngramAccessHttpServer, type RemnicAdminControls, type RemnicAdminDashboardStatus } from "./access-http.js";
9
9
  import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
10
+ import { tokenCapabilityStore, type TokenCapabilities } from "./access-token-capabilities.js";
10
11
  import { parseConfig } from "./config.js";
11
12
  import { readPair, writePair } from "./contradiction/contradiction-review.js";
13
+ import { createChatSession } from "./chat/chat-session.js";
14
+ import { DEFAULT_CHAT_CONFIG } from "./chat/chat-config.js";
12
15
  import { projectTagProjectId } from "./coding/coding-namespace.js";
13
16
  import { OFFLINE_SYNC_MAX_MTIME_MS } from "./offline-sync.js";
14
17
  import type { StorageManager } from "./storage.js";
@@ -1804,3 +1807,781 @@ test("HTTP authTokenEntriesGetter is authoritative: scope policy binds connector
1804
1807
  await server.stop();
1805
1808
  }
1806
1809
  });
1810
+
1811
+ test("HTTP scoped-token namespace allow-list is enforced on every namespace route (issue #1837 bypass fix)", async () => {
1812
+ // A scoped token may touch ONLY ns_a. The server default tenant is "default"
1813
+ // (≠ ns_a), so OMITTING ?namespace= must NOT silently fall through to the
1814
+ // default tenant — the EFFECTIVE namespace (explicit OR defaulted) must be a
1815
+ // member of the token's allow-list on every namespace-scoped route. Fail
1816
+ // closed everywhere; the previous behavior let a scoped bearer drop the
1817
+ // param and reach the server default tenant (memory_list / memory_get).
1818
+ const browseCalls: { namespace?: string }[] = [];
1819
+ const getCalls: { memoryId: string; namespace: string | undefined }[] = [];
1820
+ const service = {
1821
+ configRef: parseConfig({
1822
+ memoryDir: "/tmp/remnic-http-scoped-namespace-allow-list",
1823
+ namespacesEnabled: true,
1824
+ defaultNamespace: "default",
1825
+ }),
1826
+ memoryBrowse: async (request: { namespace?: string }) => {
1827
+ browseCalls.push(request);
1828
+ return { total: 0, memories: [] };
1829
+ },
1830
+ memoryGet: async (memoryId: string, namespace: string | undefined) => {
1831
+ getCalls.push({ memoryId, namespace });
1832
+ return { found: true };
1833
+ },
1834
+ peerList: async () => ({ peers: [] }),
1835
+ } as unknown as EngramAccessService;
1836
+ const server = new EngramAccessHttpServer({
1837
+ service,
1838
+ port: 0,
1839
+ // Entry-based tokens with a capabilities record. The scoped token carries
1840
+ // a namespaces allow-list; the operator token is explicit-unrestricted.
1841
+ authTokenEntriesGetter: () => [
1842
+ { token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
1843
+ { token: "operator", capabilities: { version: 1 } },
1844
+ ],
1845
+ adminConsoleEnabled: false,
1846
+ });
1847
+ const status = await server.start();
1848
+ const request = (token: string, urlPath: string) =>
1849
+ fetch(`http://127.0.0.1:${status.port}${urlPath}`, {
1850
+ headers: { authorization: `Bearer ${token}` },
1851
+ });
1852
+ try {
1853
+ // ── memory_list: the named bypass route ──
1854
+ assert.equal(
1855
+ (await request("scoped-ns-a", "/engram/v1/memories")).status,
1856
+ 403,
1857
+ "scoped: omitting namespace must be rejected — server default is not in the allow-list",
1858
+ );
1859
+ assert.equal(
1860
+ (await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_b")).status,
1861
+ 403,
1862
+ "scoped: an unlisted namespace must be rejected",
1863
+ );
1864
+ const okList = await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_a");
1865
+ assert.equal(okList.status, 200, "scoped: an allowed namespace must succeed");
1866
+ assert.equal(browseCalls.at(-1)?.namespace, "ns_a", "scoped: the allowed namespace is forwarded to the service");
1867
+
1868
+ // ── memory_get: the other named bypass route (dispatches via the op registry) ──
1869
+ assert.equal(
1870
+ (await request("scoped-ns-a", "/engram/v1/memories/m_1")).status,
1871
+ 403,
1872
+ "scoped memory_get: omitting namespace must be rejected",
1873
+ );
1874
+ assert.equal(
1875
+ (await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_b")).status,
1876
+ 403,
1877
+ "scoped memory_get: an unlisted namespace must be rejected",
1878
+ );
1879
+ const okGet = await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_a");
1880
+ assert.equal(okGet.status, 200, "scoped memory_get: an allowed namespace must succeed");
1881
+ assert.deepEqual(
1882
+ getCalls.at(-1),
1883
+ { memoryId: "m_1", namespace: "ns_a" },
1884
+ "scoped memory_get: the allowed namespace reaches the service",
1885
+ );
1886
+
1887
+ // ── a non-namespace-scoped route is unaffected by namespace scoping ──
1888
+ assert.equal(
1889
+ (await request("scoped-ns-a", "/engram/v1/peers")).status,
1890
+ 200,
1891
+ "a non-namespace-scoped route must be unaffected",
1892
+ );
1893
+
1894
+ // ── unrestricted token: omitting namespace still reaches the default tenant ──
1895
+ assert.equal(
1896
+ (await request("operator", "/engram/v1/memories")).status,
1897
+ 200,
1898
+ "unrestricted: omitting namespace must still reach the default tenant",
1899
+ );
1900
+ } finally {
1901
+ await server.stop();
1902
+ }
1903
+ });
1904
+
1905
+ test("HTTP scoped-token namespace allow-list gates id-loaded contradiction routes (issue #1850 round 2)", async () => {
1906
+ // The contradiction detail GET and review/resolve routes load the pair BY
1907
+ // ID — its namespace comes from the record, NOT a ?namespace= query param
1908
+ // that resolveNamespace() already gates. A namespace-scoped bearer that
1909
+ // knows a pair id in a namespace outside its allow-list must NOT be able to
1910
+ // read (GET) or mutate (resolve) it. Fail closed (403) everywhere; the pair
1911
+ // in the allowed namespace is reachable.
1912
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-http-scoped-contradiction-"));
1913
+ const allowedPair = writePair(dir, {
1914
+ namespace: "ns_a",
1915
+ memoryIds: ["a-1", "a-2"],
1916
+ verdict: "contradicts",
1917
+ rationale: "pair in the allowed namespace",
1918
+ confidence: 0.9,
1919
+ detectedAt: new Date().toISOString(),
1920
+ });
1921
+ const deniedPair = writePair(dir, {
1922
+ namespace: "ns_b",
1923
+ memoryIds: ["b-1", "b-2"],
1924
+ verdict: "contradicts",
1925
+ rationale: "pair in a denied namespace",
1926
+ confidence: 0.9,
1927
+ detectedAt: new Date().toISOString(),
1928
+ });
1929
+ const storage = { dir } as unknown as StorageManager;
1930
+ const service = {
1931
+ configRef: parseConfig({
1932
+ memoryDir: dir,
1933
+ namespacesEnabled: true,
1934
+ defaultNamespace: "default",
1935
+ }),
1936
+ memoryDir: dir,
1937
+ storageRef: storage,
1938
+ getReadableStorageForNamespace: async (namespace: string | undefined) => ({
1939
+ namespace: namespace ?? "default",
1940
+ storage,
1941
+ }),
1942
+ getWritableStorageForNamespace: async (namespace: string | undefined) => ({
1943
+ namespace: namespace ?? "default",
1944
+ storage,
1945
+ }),
1946
+ } as unknown as EngramAccessService;
1947
+ const server = new EngramAccessHttpServer({
1948
+ service,
1949
+ port: 0,
1950
+ authTokenEntriesGetter: () => [
1951
+ { token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
1952
+ { token: "operator", capabilities: { version: 1 } },
1953
+ ],
1954
+ adminConsoleEnabled: false,
1955
+ });
1956
+ const status = await server.start();
1957
+ const getDetail = (token: string, pairId: string) =>
1958
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/review/contradictions/${pairId}`, {
1959
+ headers: { authorization: `Bearer ${token}` },
1960
+ });
1961
+ const resolve = (token: string, pairId: string, verb: string) =>
1962
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/review/resolve`, {
1963
+ method: "POST",
1964
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1965
+ body: JSON.stringify({ pairId, verb }),
1966
+ });
1967
+ try {
1968
+ // ── GET detail: scoped token, denied namespace → 403 (fail closed, no leak) ──
1969
+ assert.equal(
1970
+ (await getDetail("scoped-ns-a", deniedPair.pairId)).status,
1971
+ 403,
1972
+ "scoped GET: a pair in an unlisted namespace must be denied (403), not leaked",
1973
+ );
1974
+ // ── GET detail: scoped token, allowed namespace → 200 ──
1975
+ assert.equal(
1976
+ (await getDetail("scoped-ns-a", allowedPair.pairId)).status,
1977
+ 200,
1978
+ "scoped GET: a pair in the allowed namespace must succeed",
1979
+ );
1980
+
1981
+ // ── review/resolve: scoped token, denied namespace → 403 before any mutation ──
1982
+ const deniedResolve = await resolve("scoped-ns-a", deniedPair.pairId, "both-valid");
1983
+ assert.equal(
1984
+ deniedResolve.status,
1985
+ 403,
1986
+ "scoped resolve: a pair in an unlisted namespace must be denied (403) before any mutation",
1987
+ );
1988
+ // The denied pair must NOT have been mutated (no resolution leak).
1989
+ assert.notEqual(
1990
+ readPair(dir, deniedPair.pairId)?.resolution,
1991
+ "both-valid",
1992
+ "scoped resolve: the denied pair must remain unresolved",
1993
+ );
1994
+
1995
+ // ── review/resolve: scoped token, allowed namespace → succeeds (not 403) ──
1996
+ const allowedResolve = await resolve("scoped-ns-a", allowedPair.pairId, "both-valid");
1997
+ assert.equal(
1998
+ allowedResolve.status,
1999
+ 200,
2000
+ "scoped resolve: a pair in the allowed namespace must succeed",
2001
+ );
2002
+ assert.equal(
2003
+ readPair(dir, allowedPair.pairId)?.resolution,
2004
+ "both-valid",
2005
+ "scoped resolve: the allowed pair is marked resolved",
2006
+ );
2007
+
2008
+ // ── unrestricted operator token: the denied-namespace pair is reachable ──
2009
+ assert.equal(
2010
+ (await getDetail("operator", deniedPair.pairId)).status,
2011
+ 200,
2012
+ "unrestricted GET: the denied-namespace pair is reachable for an unrestricted token",
2013
+ );
2014
+ } finally {
2015
+ await server.stop();
2016
+ await rm(dir, { recursive: true, force: true });
2017
+ }
2018
+ });
2019
+
2020
+ // ===========================================================================
2021
+ // Issue #1850 round 4 — uniform effective-namespace chokepoint coverage.
2022
+ // One helper (enforceNamespaceAllowList) gates every surface; these tests
2023
+ // prove the gate fires on the surfaces round 3 missed: MCP tools/call, HTTP
2024
+ // body-namespace (coding) routes, and the legacy undefined-namespace pair.
2025
+ // ===========================================================================
2026
+
2027
+ test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 finding 1)", async () => {
2028
+ // MCP `tools/call` previously never applied the namespace allow-list, so a
2029
+ // scoped bearer could reach another tenant via tool args. capsule_list is a
2030
+ // namespace-scoped MCP tool (accepts a `namespace` argument), so the gate
2031
+ // must fire on it. The 403 is thrown BEFORE op.run, so the denied case never
2032
+ // reaches the service.
2033
+ const capsuleCalls: Array<Record<string, unknown>> = [];
2034
+ const service = {
2035
+ configRef: parseConfig({ memoryDir: "/tmp/remnic-mcp-ns", namespacesEnabled: true, defaultNamespace: "default" }),
2036
+ capsuleList: async (args: Record<string, unknown>) => {
2037
+ capsuleCalls.push(args);
2038
+ return { capsules: [] };
2039
+ },
2040
+ } as unknown as EngramAccessService;
2041
+ const server = new EngramAccessHttpServer({
2042
+ service,
2043
+ port: 0,
2044
+ authTokenEntriesGetter: () => [
2045
+ { token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
2046
+ { token: "operator", capabilities: { version: 1 } },
2047
+ ],
2048
+ adminConsoleEnabled: false,
2049
+ });
2050
+ const status = await server.start();
2051
+ const mcpCall = (token: string, namespace: string) =>
2052
+ fetch(`http://127.0.0.1:${status.port}/mcp`, {
2053
+ method: "POST",
2054
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
2055
+ body: JSON.stringify({
2056
+ jsonrpc: "2.0",
2057
+ id: 1,
2058
+ method: "tools/call",
2059
+ params: { name: "engram.capsule_list", arguments: { namespace } },
2060
+ }),
2061
+ });
2062
+ try {
2063
+ // scoped: an unlisted namespace is rejected with 403 BEFORE op.run.
2064
+ assert.equal(
2065
+ (await mcpCall("scoped-ns-a", "ns_b")).status,
2066
+ 403,
2067
+ "scoped MCP: an unlisted namespace must be denied (403) before op.run",
2068
+ );
2069
+ assert.equal(capsuleCalls.length, 0, "scoped MCP: the service must NOT be reached for a denied namespace");
2070
+ // scoped: the allowed namespace proceeds past the gate (200, not 403).
2071
+ const allowed = await mcpCall("scoped-ns-a", "ns_a");
2072
+ assert.notEqual(allowed.status, 403, "scoped MCP: the allowed namespace must proceed past the gate");
2073
+ assert.equal(capsuleCalls.at(-1)?.namespace, "ns_a", "scoped MCP: the allowed namespace reaches the service");
2074
+ // unrestricted: an unlisted namespace is a no-op (proceeds, not 403).
2075
+ const unrestricted = await mcpCall("operator", "ns_b");
2076
+ assert.notEqual(unrestricted.status, 403, "unrestricted MCP: namespace scoping must be a no-op");
2077
+ } finally {
2078
+ await server.stop();
2079
+ }
2080
+ });
2081
+
2082
+ test("HTTP coding POST routes gate the body namespace through the allow-list (issue #1850 finding 2)", async () => {
2083
+ // coding_decision passes `body` straight to op.run; a scoped bearer setting
2084
+ // body.namespace to another tenant must be rejected. The gate fires on the
2085
+ // body namespace field via the same resolveNamespace chokepoint as query
2086
+ // routes. 403 before op.run; the allowed namespace reaches the service.
2087
+ const decisionCalls: Array<Record<string, unknown>> = [];
2088
+ const service = {
2089
+ configRef: parseConfig({ memoryDir: "/tmp/remnic-coding-ns", namespacesEnabled: true, defaultNamespace: "default" }),
2090
+ codingDecision: async (input: Record<string, unknown>) => {
2091
+ decisionCalls.push(input);
2092
+ return { subcommand: "list", records: [], count: 0 };
2093
+ },
2094
+ } as unknown as EngramAccessService;
2095
+ const server = new EngramAccessHttpServer({
2096
+ service,
2097
+ port: 0,
2098
+ authTokenEntriesGetter: () => [
2099
+ { token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
2100
+ { token: "operator", capabilities: { version: 1 } },
2101
+ ],
2102
+ adminConsoleEnabled: false,
2103
+ });
2104
+ const status = await server.start();
2105
+ const postDecision = (token: string, namespace: string | undefined) =>
2106
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/coding/decisions`, {
2107
+ method: "POST",
2108
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
2109
+ body: JSON.stringify({ subcommand: "list", ...(namespace ? { namespace } : {}) }),
2110
+ });
2111
+ try {
2112
+ // scoped: body.namespace to an unlisted tenant → 403 before op.run.
2113
+ assert.equal(
2114
+ (await postDecision("scoped-ns-a", "ns_b")).status,
2115
+ 403,
2116
+ "scoped coding: body.namespace to an unlisted namespace must be denied (403)",
2117
+ );
2118
+ assert.equal(decisionCalls.length, 0, "scoped coding: the service must NOT be reached for a denied namespace");
2119
+ // scoped: the allowed body.namespace reaches the service.
2120
+ const allowed = await postDecision("scoped-ns-a", "ns_a");
2121
+ assert.notEqual(allowed.status, 403, "scoped coding: the allowed namespace must proceed past the gate");
2122
+ assert.equal(decisionCalls.at(-1)?.namespace, "ns_a", "scoped coding: the allowed namespace reaches the service");
2123
+ // unrestricted: any body.namespace is a no-op.
2124
+ assert.notEqual(
2125
+ (await postDecision("operator", "ns_b")).status,
2126
+ 403,
2127
+ "unrestricted coding: namespace scoping must be a no-op",
2128
+ );
2129
+ } finally {
2130
+ await server.stop();
2131
+ }
2132
+ });
2133
+
2134
+ test("Legacy undefined-namespace contradiction pair: default-allow-list token reads+resolves; non-default denied (issue #1850 findings 3+4)", async () => {
2135
+ // A legacy pair carries namespace:undefined, which downstream storage maps to
2136
+ // the server DEFAULT. Round 3's assertNamespaceAllowed(caps, undefined)
2137
+ // wrongly denied a scoped token whose allow-list INCLUDES the default. The
2138
+ // effective-namespace chokepoint maps undefined → default, so:
2139
+ // - namespaces:['default'] (== server default) ⇒ ALLOWED read + resolve;
2140
+ // - namespaces:['ns_a'] (not default) ⇒ DENIED.
2141
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-http-scoped-legacy-pair-"));
2142
+ const legacyPair = writePair(dir, {
2143
+ memoryIds: ["legacy-x", "legacy-y"],
2144
+ verdict: "contradicts",
2145
+ rationale: "legacy pair with no namespace",
2146
+ confidence: 0.9,
2147
+ detectedAt: new Date().toISOString(),
2148
+ });
2149
+ // Sanity: the fixture really is a legacy unscoped pair.
2150
+ assert.equal(readPair(dir, legacyPair.pairId)?.namespace, undefined);
2151
+ const storage = { dir } as unknown as StorageManager;
2152
+ const baseService = {
2153
+ configRef: parseConfig({ memoryDir: dir, namespacesEnabled: true, defaultNamespace: "default" }),
2154
+ memoryDir: dir,
2155
+ storageRef: storage,
2156
+ getReadableStorageForNamespace: async (namespace: string | undefined) => ({
2157
+ namespace: namespace ?? "default",
2158
+ storage,
2159
+ }),
2160
+ getWritableStorageForNamespace: async (namespace: string | undefined) => ({
2161
+ namespace: namespace ?? "default",
2162
+ storage,
2163
+ }),
2164
+ };
2165
+ const mkServer = (scopedToken: string, allowList: string[]) => {
2166
+ const service = { ...baseService } as unknown as EngramAccessService;
2167
+ return new EngramAccessHttpServer({
2168
+ service,
2169
+ port: 0,
2170
+ authTokenEntriesGetter: () => [{ token: scopedToken, capabilities: { version: 1, namespaces: allowList } }],
2171
+ adminConsoleEnabled: false,
2172
+ });
2173
+ };
2174
+
2175
+ // ── default-allow-list token: legacy pair is reachable + resolvable ──
2176
+ const defaultServer = mkServer("allow-default", ["default"]);
2177
+ const defaultStatus = await defaultServer.start();
2178
+ try {
2179
+ const detail = await fetch(
2180
+ `http://127.0.0.1:${defaultStatus.port}/engram/v1/review/contradictions/${legacyPair.pairId}`,
2181
+ { headers: { authorization: "Bearer allow-default" } },
2182
+ );
2183
+ assert.equal(detail.status, 200, "default-allow token: legacy undefined pair must be readable (undefined→default)");
2184
+ const resolve = await fetch(`http://127.0.0.1:${defaultStatus.port}/engram/v1/review/resolve`, {
2185
+ method: "POST",
2186
+ headers: { authorization: "Bearer allow-default", "content-type": "application/json" },
2187
+ body: JSON.stringify({ pairId: legacyPair.pairId, verb: "both-valid" }),
2188
+ });
2189
+ assert.equal(resolve.status, 200, "default-allow token: legacy undefined pair must be resolvable");
2190
+ assert.equal(
2191
+ readPair(dir, legacyPair.pairId)?.resolution,
2192
+ "both-valid",
2193
+ "default-allow token: the legacy pair is marked resolved",
2194
+ );
2195
+ } finally {
2196
+ await defaultServer.stop();
2197
+ }
2198
+
2199
+ // A fresh legacy pair for the deny case — independent of the pair the
2200
+ // default-allow token already resolved above, so "must remain unresolved"
2201
+ // is a clean assertion.
2202
+ const deniedLegacyPair = writePair(dir, {
2203
+ memoryIds: ["legacy-deny-x", "legacy-deny-y"],
2204
+ verdict: "contradicts",
2205
+ rationale: "legacy pair for the deny case",
2206
+ confidence: 0.9,
2207
+ detectedAt: new Date().toISOString(),
2208
+ });
2209
+ assert.equal(readPair(dir, deniedLegacyPair.pairId)?.namespace, undefined);
2210
+
2211
+ // ── non-default-allow-list token: legacy pair is DENIED (undefined→default not listed) ──
2212
+ const deniedServer = mkServer("allow-ns-a", ["ns_a"]);
2213
+ const deniedStatus = await deniedServer.start();
2214
+ try {
2215
+ const detail = await fetch(
2216
+ `http://127.0.0.1:${deniedStatus.port}/engram/v1/review/contradictions/${deniedLegacyPair.pairId}`,
2217
+ { headers: { authorization: "Bearer allow-ns-a" } },
2218
+ );
2219
+ assert.equal(
2220
+ detail.status,
2221
+ 403,
2222
+ "non-default token: legacy undefined pair must be denied (undefined→default not in allow-list)",
2223
+ );
2224
+ const resolve = await fetch(`http://127.0.0.1:${deniedStatus.port}/engram/v1/review/resolve`, {
2225
+ method: "POST",
2226
+ headers: { authorization: "Bearer allow-ns-a", "content-type": "application/json" },
2227
+ body: JSON.stringify({ pairId: deniedLegacyPair.pairId, verb: "both-valid" }),
2228
+ });
2229
+ assert.equal(resolve.status, 403, "non-default token: legacy undefined pair must not be resolvable");
2230
+ assert.notEqual(
2231
+ readPair(dir, deniedLegacyPair.pairId)?.resolution,
2232
+ "both-valid",
2233
+ "non-default token: the legacy pair must remain unresolved",
2234
+ );
2235
+ } finally {
2236
+ await deniedServer.stop();
2237
+ await rm(dir, { recursive: true, force: true });
2238
+ }
2239
+ });
2240
+
2241
+ // ──────────────────────────────────────────────────────────────────────────
2242
+ // Issue #1850 round 5: comprehensive access-surface coverage
2243
+ // ──────────────────────────────────────────────────────────────────────────
2244
+
2245
+ test("HTTP adapters route is op-gated: deny-all → 403, unrestricted → 200 (issue #1850 finding 1)", async () => {
2246
+ // The adapters route serves adapter metadata after auth but previously had
2247
+ // NO enforceTokenOp, so a deny-all / narrow-ops token reached it. It now
2248
+ // dispatches through the adapters_status op; a scoped token is rejected.
2249
+ const service = {
2250
+ configRef: parseConfig({ memoryDir: "/tmp/remnic-http-adapters-opgate", defaultNamespace: "default" }),
2251
+ } as unknown as EngramAccessService;
2252
+ const server = new EngramAccessHttpServer({
2253
+ service,
2254
+ port: 0,
2255
+ authTokenEntriesGetter: () => [
2256
+ { token: "denyall", capabilities: { version: 1, ops: [] } },
2257
+ { token: "narrow", capabilities: { version: 1, ops: ["memory_get"] } },
2258
+ { token: "operator", capabilities: { version: 1 } },
2259
+ ],
2260
+ adminConsoleEnabled: false,
2261
+ });
2262
+ const status = await server.start();
2263
+ const request = (token: string, urlPath: string) =>
2264
+ fetch(`http://127.0.0.1:${status.port}${urlPath}`, {
2265
+ headers: { authorization: `Bearer ${token}` },
2266
+ });
2267
+ try {
2268
+ assert.equal(
2269
+ (await request("denyall", "/engram/v1/adapters")).status,
2270
+ 403,
2271
+ "deny-all token must be rejected by the op gate",
2272
+ );
2273
+ assert.equal(
2274
+ (await request("narrow", "/engram/v1/adapters")).status,
2275
+ 403,
2276
+ "narrow-ops token without adapters_status must be rejected",
2277
+ );
2278
+ const ok = await request("operator", "/engram/v1/adapters");
2279
+ assert.equal(ok.status, 200, "unrestricted token must reach adapter metadata");
2280
+ const body = (await ok.json()) as { adaptersEnabled?: boolean };
2281
+ assert.equal(typeof body.adaptersEnabled, "boolean", "adapter metadata body shape preserved");
2282
+ } finally {
2283
+ await server.stop();
2284
+ }
2285
+ });
2286
+
2287
+ test("HTTP chat routes gate the resumed session's STORED namespace (issue #1850 finding 2)", async () => {
2288
+ // A chat session is an id-loaded record: its namespace comes from the stored
2289
+ // session header, NOT a ?namespace= query param. A namespace-scoped bearer
2290
+ // that knows a chatSessionId must not post to / stream a session in a
2291
+ // namespace outside its allow-list. The gate runs before delegation.
2292
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-chat-ns-gate-"));
2293
+ // Session bound to ns_a (no principal ⇒ accessible to anyone per
2294
+ // sessionBelongsToPrincipal, so the namespace gate is the ONLY gate in play).
2295
+ const session = await createChatSession(dir, { namespace: "ns_a" });
2296
+ const service = {
2297
+ configRef: parseConfig({ memoryDir: dir, namespacesEnabled: true, defaultNamespace: "default" }),
2298
+ memoryDir: dir,
2299
+ } as unknown as EngramAccessService;
2300
+ const server = new EngramAccessHttpServer({
2301
+ service,
2302
+ port: 0,
2303
+ authTokenEntriesGetter: () => [
2304
+ { token: "scoped-ns-b", capabilities: { version: 1, namespaces: ["ns_b"] } },
2305
+ { token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
2306
+ ],
2307
+ adminConsoleEnabled: false,
2308
+ });
2309
+ const status = await server.start();
2310
+ const authed = (token: string) => ({ headers: { authorization: `Bearer ${token}` } });
2311
+ try {
2312
+ // ── POST chat/message resuming a CROSS-namespace session → 403 ──
2313
+ const cross = await fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
2314
+ method: "POST",
2315
+ ...authed("scoped-ns-b"),
2316
+ headers: { ...authed("scoped-ns-b").headers, "content-type": "application/json" },
2317
+ body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
2318
+ });
2319
+ assert.equal(cross.status, 403, "scoped-ns-b must NOT post to a session bound to ns_a");
2320
+
2321
+ // ── POST chat/message resuming a SAME-namespace session → passes the gate ──
2322
+ // (Chat is unconfigured here so the handler returns chat_disabled 404; the
2323
+ // point is it is NOT 403 — the namespace gate let it through.)
2324
+ const same = await fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
2325
+ method: "POST",
2326
+ headers: { authorization: "Bearer scoped-ns-a", "content-type": "application/json" },
2327
+ body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
2328
+ });
2329
+ assert.notEqual(same.status, 403, "scoped-ns-a must pass the namespace gate for its own session");
2330
+
2331
+ // ── GET chat/events for a CROSS-namespace session → 403 ──
2332
+ const crossEvents = await fetch(
2333
+ `http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
2334
+ authed("scoped-ns-b"),
2335
+ );
2336
+ assert.equal(crossEvents.status, 403, "scoped-ns-b must NOT stream a session bound to ns_a");
2337
+
2338
+ // ── GET chat/events for a SAME-namespace session → passes the gate ──
2339
+ const sameEvents = await fetch(
2340
+ `http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
2341
+ authed("scoped-ns-a"),
2342
+ );
2343
+ assert.notEqual(sameEvents.status, 403, "scoped-ns-a must pass the namespace gate for its own session");
2344
+ } finally {
2345
+ await server.stop();
2346
+ await rm(dir, { recursive: true, force: true });
2347
+ }
2348
+ });
2349
+
2350
+ test("HTTP per-request token capabilities never bleed across concurrent async requests (issue #1850 r7 ALS run fix)", async () => {
2351
+ // The capability store must be bound via run() (NOT enterWith) so each
2352
+ // request's async scope observes its OWN resolved capabilities across every
2353
+ // await. Interleave concurrent requests with DIFFERENT scopes against an
2354
+ // op+namespace-gated route whose service read yields, then assert:
2355
+ // (1) a deny-all op token is ALWAYS rejected — never fail-opens to 200
2356
+ // because its caps leaked to undefined/unrestricted mid-handler;
2357
+ // (2) the store read AFTER an async yield inside the service still carries
2358
+ // the REQUESTING token's scope on every call — zero cross-request bleed.
2359
+ const observed: Array<TokenCapabilities | undefined> = [];
2360
+ const service = {
2361
+ configRef: parseConfig({
2362
+ memoryDir: "/tmp/remnic-http-als-concurrency",
2363
+ namespacesEnabled: true,
2364
+ defaultNamespace: "default",
2365
+ }),
2366
+ memoryBrowse: async () => {
2367
+ // Yield so concurrent requests overlap at this await — the exact window
2368
+ // where an enterWith-based store would read another request's caps (or
2369
+ // undefined). run() keeps each request in its own scope across the yield.
2370
+ await new Promise<void>((resolve) => setTimeout(resolve, 8));
2371
+ observed.push(tokenCapabilityStore.getStore());
2372
+ return { total: 0, memories: [] };
2373
+ },
2374
+ memoryGet: async () => ({ found: true }),
2375
+ peerList: async () => ({ peers: [] }),
2376
+ } as unknown as EngramAccessService;
2377
+ const server = new EngramAccessHttpServer({
2378
+ service,
2379
+ port: 0,
2380
+ authTokenEntriesGetter: () => [
2381
+ { token: "deny-all", capabilities: { version: 1, ops: [] } },
2382
+ { token: "scoped-ns-a", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_a"] } },
2383
+ { token: "scoped-ns-b", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_b"] } },
2384
+ ],
2385
+ adminConsoleEnabled: false,
2386
+ });
2387
+ const status = await server.start();
2388
+ const get = (token: string, namespace: string) =>
2389
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`, {
2390
+ headers: { authorization: `Bearer ${token}` },
2391
+ });
2392
+ try {
2393
+ const REPEATS = 15;
2394
+ const batch: Promise<Response>[] = [];
2395
+ for (let i = 0; i < REPEATS; i++) {
2396
+ batch.push(get("deny-all", "ns_a")); // op-gate must reject (ops: [])
2397
+ batch.push(get("scoped-ns-a", "ns_a")); // passes op + ns gate, reaches service
2398
+ batch.push(get("scoped-ns-b", "ns_b")); // passes op + ns gate, reaches service
2399
+ }
2400
+ const responses = await Promise.all(batch);
2401
+
2402
+ // (1) Op-gate holds under concurrency: every deny-all request is 403, never
2403
+ // fail-open 200 (which would mean its caps bled to unrestricted).
2404
+ const denyAllStatuses = responses.filter((_, i) => i % 3 === 0).map((r) => r.status);
2405
+ assert.ok(
2406
+ denyAllStatuses.every((s) => s === 403),
2407
+ `deny-all token must always be 403, got: ${JSON.stringify(denyAllStatuses)}`,
2408
+ );
2409
+
2410
+ // scoped tokens reach the service (200) — op + namespace gates both pass.
2411
+ const nsAStatuses = responses.filter((_, i) => i % 3 === 1).map((r) => r.status);
2412
+ const nsBStatuses = responses.filter((_, i) => i % 3 === 2).map((r) => r.status);
2413
+ assert.ok(nsAStatuses.every((s) => s === 200), `scoped-ns-a should all be 200: ${JSON.stringify(nsAStatuses)}`);
2414
+ assert.ok(nsBStatuses.every((s) => s === 200), `scoped-ns-b should all be 200: ${JSON.stringify(nsBStatuses)}`);
2415
+
2416
+ // (2) No cross-request bleed: the store read AFTER the async yield carried
2417
+ // the REQUESTING token's namespace on every service call. memoryBrowse
2418
+ // ran once per scoped request (2 * REPEATS).
2419
+ assert.equal(observed.length, 2 * REPEATS, "memoryBrowse called once per scoped request");
2420
+ const nsAObserved = observed.filter((c) => c?.namespaces?.includes("ns_a")).length;
2421
+ const nsBObserved = observed.filter((c) => c?.namespaces?.includes("ns_b")).length;
2422
+ assert.equal(nsAObserved, REPEATS, "every ns_a request observed its own scope — no bleed");
2423
+ assert.equal(nsBObserved, REPEATS, "every ns_b request observed its own scope — no bleed");
2424
+ } finally {
2425
+ await server.stop();
2426
+ }
2427
+ });
2428
+
2429
+ test("HTTP allow-capability is never inherited by a concurrent deny token across the async yield (issue #1850 r8 ALS non-inheritance)", async () => {
2430
+ // The r7 run() fix guarantees each request observes its OWN caps. The sharper
2431
+ // property: a token whose caps ALLOW the op+namespace (→ 200) interleaved
2432
+ // with deny tokens (→ 403) must NEVER let a deny request inherit the allow
2433
+ // token's capabilities — i.e. in the SAME batch where allow requests returned
2434
+ // 200, NO deny request ever returned 200. Distinct (op, namespace) per token
2435
+ // so the result can only come from each request's own resolved caps.
2436
+ const observed: Array<TokenCapabilities | undefined> = [];
2437
+ const service = {
2438
+ configRef: parseConfig({
2439
+ memoryDir: "/tmp/remnic-http-als-noninherit",
2440
+ namespacesEnabled: true,
2441
+ defaultNamespace: "default",
2442
+ }),
2443
+ memoryBrowse: async () => {
2444
+ // Real wall-clock yield — the LEGITIMATE timer exception: this test
2445
+ // exercises ALS async-scope isolation across CONCURRENT requests, so the
2446
+ // await must produce a genuine overlap window where an enterWith-based
2447
+ // store would read another request's caps. Deterministic fake timers
2448
+ // cannot force that cross-request overlap (mirrors the r7 sibling test).
2449
+ await new Promise<void>((resolve) => setTimeout(resolve, 8));
2450
+ observed.push(tokenCapabilityStore.getStore());
2451
+ return { total: 0, memories: [] };
2452
+ },
2453
+ memoryGet: async () => ({ found: true }),
2454
+ peerList: async () => ({ peers: [] }),
2455
+ } as unknown as EngramAccessService;
2456
+ const server = new EngramAccessHttpServer({
2457
+ service,
2458
+ port: 0,
2459
+ authTokenEntriesGetter: () => [
2460
+ // ALLOW: op + namespace both permitted → 200.
2461
+ { token: "allow", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_allow"] } },
2462
+ // DENY (op-level): empty ops → 403 at the op-gate.
2463
+ { token: "deny-all", capabilities: { version: 1, ops: [] } },
2464
+ // DENY (namespace-level): op permitted but scoped to a DIFFERENT
2465
+ // namespace ("ns_other") than the request ("ns_allow") → 403 at the
2466
+ // namespace-gate. The ONLY way this becomes 200 is by inheriting the
2467
+ // allow token's ns_allow cap — exactly the bleed run() prevents.
2468
+ { token: "deny-ns", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_other"] } },
2469
+ ],
2470
+ adminConsoleEnabled: false,
2471
+ });
2472
+ const status = await server.start();
2473
+ const get = (token: string, namespace: string) =>
2474
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`, {
2475
+ headers: { authorization: `Bearer ${token}` },
2476
+ });
2477
+ try {
2478
+ const REPEATS = 15;
2479
+ const batch: Promise<Response>[] = [];
2480
+ for (let i = 0; i < REPEATS; i++) {
2481
+ batch.push(get("allow", "ns_allow")); // 3i → 200
2482
+ batch.push(get("deny-all", "ns_allow")); // 3i+1 → 403 (op-gate)
2483
+ batch.push(get("deny-ns", "ns_allow")); // 3i+2 → 403 (namespace-gate)
2484
+ }
2485
+ const responses = await Promise.all(batch);
2486
+ const allowStatuses = responses.filter((_, i) => i % 3 === 0).map((r) => r.status);
2487
+ const denyAllStatuses = responses.filter((_, i) => i % 3 === 1).map((r) => r.status);
2488
+ const denyNsStatuses = responses.filter((_, i) => i % 3 === 2).map((r) => r.status);
2489
+
2490
+ // ALLOW requests succeed (200) — the caps DID resolve correctly and the
2491
+ // service read happened under the allow scope.
2492
+ assert.ok(allowStatuses.every((s) => s === 200), `allow token should all be 200: ${JSON.stringify(allowStatuses)}`);
2493
+
2494
+ // NON-INHERITANCE: in the SAME interleaved batch where allow got 200, every
2495
+ // deny request stayed 403 — neither deny token ever inherited the allow
2496
+ // token's capabilities (which would have been 200). Covers BOTH op-level
2497
+ // and namespace-level denial.
2498
+ assert.ok(
2499
+ denyAllStatuses.every((s) => s === 403),
2500
+ `deny-all token must never be 200 (never inherits allow caps): ${JSON.stringify(denyAllStatuses)}`,
2501
+ );
2502
+ assert.ok(
2503
+ denyNsStatuses.every((s) => s === 403),
2504
+ `deny-ns token must never be 200 (never inherits allow namespace): ${JSON.stringify(denyNsStatuses)}`,
2505
+ );
2506
+ // Both deny arrays are ALL 403 ⟹ neither contains a 200: in the same
2507
+ // interleaved batch where the allow token returned 200, no deny request
2508
+ // ever inherited the allow token's capabilities.
2509
+
2510
+ // Only allow requests reached the service; the store read after the async
2511
+ // yield carried the allow token's namespace every time — zero deny bleed.
2512
+ assert.equal(observed.length, REPEATS, "only allow requests reached the service");
2513
+ assert.ok(
2514
+ observed.every((c) => c?.namespaces?.includes("ns_allow")),
2515
+ "every service read carried the allow token's namespace — no deny bleed",
2516
+ );
2517
+ } finally {
2518
+ await server.stop();
2519
+ }
2520
+ });
2521
+
2522
+ test("HTTP chat/message gates the NEW session's namespace — scoped token cannot start a chat in a disallowed namespace (issue #1850 r7)", async () => {
2523
+ // A NEW chat turn (no chatSessionId) previously never hit the effective-
2524
+ // namespace chokepoint, so a namespace-scoped token could start a chat in the
2525
+ // server DEFAULT namespace even when "default" was outside its allow-list.
2526
+ // The new-session path must resolve the effective namespace (the server
2527
+ // default, since the HTTP chat handler forwards no namespace) and fail closed
2528
+ // against the token allow-list — same as the resume path.
2529
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-chat-new-ns-"));
2530
+ try {
2531
+ const service = {
2532
+ configRef: parseConfig({
2533
+ memoryDir: dir,
2534
+ namespacesEnabled: true,
2535
+ defaultNamespace: "default",
2536
+ chat: { ...DEFAULT_CHAT_CONFIG, enabled: true },
2537
+ }),
2538
+ memoryDir: dir,
2539
+ fallbackLlmRef: { chatCompletion: async () => ({ content: "stub reply" }) },
2540
+ localLlmRef: null,
2541
+ memoryGet: () => Promise.resolve(null),
2542
+ memoryTimeline: () => Promise.resolve([]),
2543
+ memorySearch: () => Promise.resolve({ results: [], count: 0 }),
2544
+ recallExplain: () => Promise.resolve(null),
2545
+ entityGet: () => Promise.resolve(null),
2546
+ memoryProfile: () => Promise.resolve({}),
2547
+ memoryEntitiesList: () => Promise.resolve({ items: [] }),
2548
+ memoryQuestions: () => Promise.resolve({ items: [] }),
2549
+ reviewQueue: () => Promise.resolve({ items: [] }),
2550
+ } as unknown as EngramAccessService;
2551
+ const server = new EngramAccessHttpServer({
2552
+ service,
2553
+ port: 0,
2554
+ authTokenEntriesGetter: () => [
2555
+ // scoped to ns_a (NOT the server default) — must be denied a new chat.
2556
+ { token: "scoped-ns-a", capabilities: { version: 1, ops: ["chat_message"], namespaces: ["ns_a"] } },
2557
+ // scoped to the server default — may start a new chat there.
2558
+ { token: "scoped-default", capabilities: { version: 1, ops: ["chat_message"], namespaces: ["default"] } },
2559
+ ],
2560
+ adminConsoleEnabled: false,
2561
+ });
2562
+ const status = await server.start();
2563
+ const post = (token: string) =>
2564
+ fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
2565
+ method: "POST",
2566
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
2567
+ body: JSON.stringify({ message: "hi" }), // no chatSessionId → NEW session
2568
+ });
2569
+ try {
2570
+ // DISALLOWED: token scoped to ns_a; the new session would land in the
2571
+ // server default ("default"), which is not in ["ns_a"] → 403.
2572
+ const denied = await post("scoped-ns-a");
2573
+ assert.equal(denied.status, 403, "scoped-to-ns_a must not start a new chat in the default namespace");
2574
+
2575
+ // ALLOWED: token scoped to the server default → gate passes → chat runs
2576
+ // and returns 200 with a reply.
2577
+ const allowed = await post("scoped-default");
2578
+ assert.equal(allowed.status, 200, "scoped-to-default may start a new chat in the default namespace");
2579
+ const allowedBody = (await allowed.json()) as { reply?: string };
2580
+ assert.ok(allowedBody.reply && allowedBody.reply.length > 0, "allowed new chat returns a reply");
2581
+ } finally {
2582
+ await server.stop();
2583
+ }
2584
+ } finally {
2585
+ await rm(dir, { recursive: true, force: true });
2586
+ }
2587
+ });