@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
@@ -35,7 +35,14 @@ import {
35
35
  } from "./graph-events.js";
36
36
  import { expandTildePath } from "./utils/path.js";
37
37
  import { projectTagProjectId } from "./coding/coding-namespace.js";
38
- import { getOperation } from "./access-boundary.js";
38
+ import { getOperation, type OperationName } from "./access-boundary.js";
39
+ import {
40
+ assertOperationAllowed,
41
+ enforceNamespaceAllowList,
42
+ isCapabilityRestricted,
43
+ tokenCapabilityStore,
44
+ type TokenCapabilities,
45
+ } from "./access-token-capabilities.js";
39
46
  // Importing access-operations registers the pilot boundary operations
40
47
  // (memory_get / memory_store) as a side effect; the HTTP handlers below
41
48
  // dispatch the migrated routes through the registry (issue #1525).
@@ -43,7 +50,9 @@ import "./access-operations.js";
43
50
  import { handleChatMessage, handleChatEventsSSE } from "./chat/chat-http.js";
44
51
  // cleanupExpiredChatSessions wires the chat session TTL sweep into the
45
52
  // server lifecycle (issue #1685 item 1 / #1687 Thread 21).
46
- import { cleanupExpiredChatSessions } from "./chat/chat-session.js";
53
+ import { cleanupExpiredChatSessions, enforceChatSessionNamespace } from "./chat/chat-session.js";
54
+ import { isDefaultReviewNamespace, listPairs, readPair } from "./contradiction/contradiction-review.js";
55
+ import { isValidResolutionVerb, executeResolution } from "./contradiction/resolution.js";
47
56
 
48
57
  export interface AccessHttpReadinessState {
49
58
  ready: boolean;
@@ -66,7 +75,7 @@ export interface EngramAccessHttpServerOptions {
66
75
  * is set: the connector used for the policy decision comes from the SAME
67
76
  * entry that validated, so identity can never lag validation.
68
77
  */
69
- authTokenEntriesGetter?: () => ReadonlyArray<{ token: string; connector?: string }>;
78
+ authTokenEntriesGetter?: () => ReadonlyArray<{ token: string; connector?: string; capabilities?: TokenCapabilities }>;
70
79
  /**
71
80
  * Optional per-request scope policy for tokens sourced from
72
81
  * `authTokenEntriesGetter`. Return false to deny the (validated) token
@@ -343,7 +352,7 @@ export class EngramAccessHttpServer {
343
352
  private readonly authToken?: string;
344
353
  private readonly authTokens: string[];
345
354
  private readonly authTokensGetter?: () => string[];
346
- private readonly authTokenEntriesGetter?: () => ReadonlyArray<{ token: string; connector?: string }>;
355
+ private readonly authTokenEntriesGetter?: () => ReadonlyArray<{ token: string; connector?: string; capabilities?: TokenCapabilities }>;
347
356
  private readonly tokenPathPolicy?: (connector: string, pathname: string | undefined) => boolean;
348
357
  private readonly authenticatedPrincipal?: string;
349
358
  private readonly maxBodyBytes: number;
@@ -659,7 +668,41 @@ export class EngramAccessHttpServer {
659
668
  * is intentionally NOT used as a fallback for REST requests — omitting namespace
660
669
  * should default to the server's global namespace, not silently scope to an adapter. */
661
670
  private resolveNamespace(_req: IncomingMessage, bodyNamespace?: string): string | undefined {
662
- return bodyNamespace || undefined;
671
+ const namespace = bodyNamespace || undefined;
672
+ // Per-token namespace enforcement (issues #1837/#1850): every HTTP
673
+ // namespace-scoped route routes through this helper so none can dodge the
674
+ // allow-list by dropping the param. The check is delegated to the SINGLE
675
+ // chokepoint (`enforceNamespaceAllowList`) shared with the MCP dispatch
676
+ // and the id-loaded contradiction routes — one rule, every surface. The
677
+ // effective namespace (explicit OR server default) must be a member; fail
678
+ // closed. No-op for unrestricted tokens (no namespaces allow-list).
679
+ enforceNamespaceAllowList(
680
+ tokenCapabilityStore.getStore(),
681
+ namespace,
682
+ this.service.configRef?.defaultNamespace,
683
+ );
684
+ return namespace;
685
+ }
686
+
687
+ /**
688
+ * Resolve + enforce the `namespace` field carried in a POST request body,
689
+ * returning a shallow-copied envelope with the gated namespace stamped on.
690
+ * Used by the coding/correction POST routes that pass `body` straight to
691
+ * `op.run`: the body `namespace` is a user-controlled field that MUST flow
692
+ * through the same effective-namespace allow-list gate as the query-param
693
+ * routes, otherwise a scoped bearer can scope its call to another tenant
694
+ * by setting `body.namespace` (issue #1850 finding 2). Throws 403 for a
695
+ * scoped token whose allow-list does not cover the effective namespace.
696
+ */
697
+ private gatedBodyNamespace(
698
+ req: IncomingMessage,
699
+ body: Record<string, unknown>,
700
+ ): Record<string, unknown> {
701
+ const namespace = this.resolveNamespace(
702
+ req,
703
+ typeof body.namespace === "string" ? body.namespace : undefined,
704
+ );
705
+ return { ...body, namespace };
663
706
  }
664
707
 
665
708
  /**
@@ -726,7 +769,11 @@ export class EngramAccessHttpServer {
726
769
  // here so the handler can implement operator-only endpoints
727
770
  // (e.g. /oauth/pending) without owning token validation.
728
771
  if (this.externalRequestHandler) {
729
- const authorized = this.isAuthorized(req, pathname);
772
+ // Operator-only endpoints (OAuth pending/approve/deny) must only pass
773
+ // for unrestricted tokens — a scoped (least-privileged) token must NOT
774
+ // reach operator surfaces (issue #1837).
775
+ const authorized = this.isAuthorized(req, pathname) &&
776
+ !isCapabilityRestricted(this.resolveTokenCapabilities(req, pathname));
730
777
  if (await this.externalRequestHandler(req, res, { authorized })) {
731
778
  return;
732
779
  }
@@ -743,6 +790,29 @@ export class EngramAccessHttpServer {
743
790
  return;
744
791
  }
745
792
 
793
+ // Bind the presenting token's capabilities to this request's async
794
+ // context via run() (NOT enterWith). enterWith mutates the CURRENT async
795
+ // resource and does NOT reliably isolate the store across the awaits and
796
+ // concurrent requests that fill this handler — the store could read
797
+ // undefined mid-handler, and undefined caps == unrestricted/legacy,
798
+ // which would SILENTLY FAIL OPEN, bypassing the op + namespace gates.
799
+ // run() establishes a fresh, request-private async scope for the WHOLE
800
+ // dispatch so concurrent requests never bleed capabilities and the store
801
+ // stays bound across every await (issue #1850 round 7).
802
+ const caps = this.resolveTokenCapabilities(req, pathname);
803
+ await tokenCapabilityStore.run(caps, async () => {
804
+ await this.dispatchAuthorizedRequest(req, res, parsed, pathname, correlationId, abortSignal);
805
+ });
806
+ }
807
+
808
+ private async dispatchAuthorizedRequest(
809
+ req: IncomingMessage,
810
+ res: ServerResponse,
811
+ parsed: URL,
812
+ pathname: string,
813
+ correlationId: string,
814
+ abortSignal: AbortSignal,
815
+ ): Promise<void> {
746
816
  // Method-conformance for the streamable-HTTP MCP endpoint:
747
817
  // GET/DELETE on /mcp must return 405 + Allow: POST instead of
748
818
  // silently falling through to the generic 404. POST continues
@@ -769,6 +839,12 @@ export class EngramAccessHttpServer {
769
839
  }
770
840
 
771
841
  if (req.method === "GET" && pathname === "/engram/v1/adapters") {
842
+ // Issue #1850 round 5 (finding 1): adapter metadata is served after
843
+ // auth but was NOT op-gated, so a deny-all or narrow-ops token reached
844
+ // it. enforceTokenOp fail-closes it under the per-token ops allow-list;
845
+ // no-op for unrestricted/legacy tokens. There is no namespace axis —
846
+ // adapter metadata is not tenant-scoped.
847
+ this.enforceTokenOp("adapters_status"); // boundary dispatch (issue #1850)
772
848
  const identity = this.resolveAdapterIdentity(req);
773
849
  this.respondJson(res, 200, {
774
850
  adaptersEnabled: this.adapterRegistry !== null,
@@ -782,6 +858,7 @@ export class EngramAccessHttpServer {
782
858
  req.method === "GET" &&
783
859
  (pathname === "/engram/v1/admin/dashboard" || pathname === "/remnic/v1/admin/dashboard")
784
860
  ) {
861
+ this.requireOperatorToken();
785
862
  if (!this.adminControls) {
786
863
  this.respondJson(res, 404, { error: "admin_controls_unavailable", code: "admin_controls_unavailable" });
787
864
  return;
@@ -794,6 +871,7 @@ export class EngramAccessHttpServer {
794
871
  req.method === "PATCH" &&
795
872
  (pathname === "/engram/v1/admin/config" || pathname === "/remnic/v1/admin/config")
796
873
  ) {
874
+ this.requireOperatorToken();
797
875
  if (!this.adminControls?.update) {
798
876
  this.respondJson(res, 404, { error: "admin_controls_unavailable", code: "admin_controls_unavailable" });
799
877
  return;
@@ -811,7 +889,7 @@ export class EngramAccessHttpServer {
811
889
  }
812
890
 
813
891
  if (req.method === "POST" && pathname === "/engram/v1/recall") {
814
- void getOperation("recall"); // boundary dispatch (issue #1525)
892
+ this.enforceTokenOp("recall"); // boundary dispatch (issue #1525)
815
893
  const body = await this.readValidatedBody(req, "recall");
816
894
  // Preserve the distinction between `codingContext: null` (explicit
817
895
  // clear) and `codingContext` missing from the JSON payload
@@ -934,7 +1012,7 @@ export class EngramAccessHttpServer {
934
1012
  // at session start after resolving a git context for the cwd; `remnic
935
1013
  // doctor` (PR 8) surfaces the attached context.
936
1014
  if (req.method === "POST" && pathname === "/engram/v1/coding-context") {
937
- void getOperation("set_coding_context"); // boundary dispatch (issue #1525)
1015
+ this.enforceTokenOp("set_coding_context"); // boundary dispatch (issue #1525)
938
1016
  const body = await this.readValidatedBody(req, "setCodingContext");
939
1017
  const codingContext =
940
1018
  body.codingContext !== undefined
@@ -956,7 +1034,7 @@ export class EngramAccessHttpServer {
956
1034
  req.method === "POST" &&
957
1035
  (pathname === "/engram/v1/capsules/export" || pathname === "/remnic/v1/capsules/export")
958
1036
  ) {
959
- void getOperation("capsule_export"); // boundary dispatch (issue #1525)
1037
+ this.enforceTokenOp("capsule_export"); // boundary dispatch (issue #1525)
960
1038
  const body = await this.readValidatedBody(req, "capsuleExport");
961
1039
  this.ensureWriteRateLimitAvailable();
962
1040
  const result = await this.service.capsuleExport({
@@ -978,7 +1056,7 @@ export class EngramAccessHttpServer {
978
1056
  req.method === "POST" &&
979
1057
  (pathname === "/engram/v1/capsules/import" || pathname === "/remnic/v1/capsules/import")
980
1058
  ) {
981
- void getOperation("capsule_import"); // boundary dispatch (issue #1525)
1059
+ this.enforceTokenOp("capsule_import"); // boundary dispatch (issue #1525)
982
1060
  const body = await this.readValidatedBody(req, "capsuleImport");
983
1061
  this.ensureWriteRateLimitAvailable();
984
1062
  const result = await this.service.capsuleImport({
@@ -997,7 +1075,7 @@ export class EngramAccessHttpServer {
997
1075
  req.method === "GET" &&
998
1076
  (pathname === "/engram/v1/offline-sync/snapshot" || pathname === "/remnic/v1/offline-sync/snapshot")
999
1077
  ) {
1000
- void getOperation("offline_sync_snapshot"); // boundary dispatch (issue #1525)
1078
+ this.enforceTokenOp("offline_sync_snapshot"); // boundary dispatch (issue #1525)
1001
1079
  const includeTranscriptsRaw = parsed.searchParams.get("include_transcripts");
1002
1080
  const includeContentRaw = parsed.searchParams.get("content");
1003
1081
  if (
@@ -1037,7 +1115,7 @@ export class EngramAccessHttpServer {
1037
1115
  (pathname === "/engram/v1/offline-sync/snapshot-stream" ||
1038
1116
  pathname === "/remnic/v1/offline-sync/snapshot-stream")
1039
1117
  ) {
1040
- void getOperation("offline_sync_snapshot_stream"); // boundary dispatch (issue #1525)
1118
+ this.enforceTokenOp("offline_sync_snapshot_stream"); // boundary dispatch (issue #1525)
1041
1119
  const includeTranscriptsRaw = parsed.searchParams.get("include_transcripts");
1042
1120
  const includeContentRaw = parsed.searchParams.get("content");
1043
1121
  if (
@@ -1074,7 +1152,7 @@ export class EngramAccessHttpServer {
1074
1152
  req.method === "POST" &&
1075
1153
  (pathname === "/engram/v1/offline-sync/snapshot" || pathname === "/remnic/v1/offline-sync/snapshot")
1076
1154
  ) {
1077
- void getOperation("offline_sync_snapshot"); // boundary dispatch (issue #1525)
1155
+ this.enforceTokenOp("offline_sync_snapshot"); // boundary dispatch (issue #1525)
1078
1156
  const body = await this.readValidatedBody(
1079
1157
  req,
1080
1158
  "offlineSyncSnapshot",
@@ -1097,7 +1175,7 @@ export class EngramAccessHttpServer {
1097
1175
  req.method === "POST" &&
1098
1176
  (pathname === "/engram/v1/offline-sync/files" || pathname === "/remnic/v1/offline-sync/files")
1099
1177
  ) {
1100
- void getOperation("offline_sync_files"); // boundary dispatch (issue #1525)
1178
+ this.enforceTokenOp("offline_sync_files"); // boundary dispatch (issue #1525)
1101
1179
  const body = await this.readValidatedBody(req, "offlineSyncFiles");
1102
1180
  const result = await this.service.offlineSyncFiles({
1103
1181
  namespace: this.resolveNamespace(req, body.namespace),
@@ -1116,7 +1194,7 @@ export class EngramAccessHttpServer {
1116
1194
  pathname === "/remnic/v1/offline-sync/file-content"
1117
1195
  )
1118
1196
  ) {
1119
- void getOperation("offline_sync_file_content"); // boundary dispatch (issue #1525)
1197
+ this.enforceTokenOp("offline_sync_file_content"); // boundary dispatch (issue #1525)
1120
1198
  const body = await this.readValidatedBody(req, "offlineSyncFileContent");
1121
1199
  const result = await this.service.offlineSyncFileContent({
1122
1200
  namespace: this.resolveNamespace(req, body.namespace),
@@ -1145,7 +1223,7 @@ export class EngramAccessHttpServer {
1145
1223
  pathname === "/remnic/v1/offline-sync/apply-file-content"
1146
1224
  )
1147
1225
  ) {
1148
- void getOperation("offline_sync_apply_file_content"); // boundary dispatch (issue #1525)
1226
+ this.enforceTokenOp("offline_sync_apply_file_content"); // boundary dispatch (issue #1525)
1149
1227
  const namespaceParam = parsed.searchParams.get("namespace");
1150
1228
  const bytes = this.readRequiredIntegerHeader(req, "x-remnic-file-bytes");
1151
1229
  const offset = this.readOptionalIntegerHeader(req, "x-remnic-chunk-offset") ?? 0;
@@ -1178,7 +1256,7 @@ export class EngramAccessHttpServer {
1178
1256
  req.method === "POST" &&
1179
1257
  (pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
1180
1258
  ) {
1181
- void getOperation("offline_sync_apply"); // boundary dispatch (issue #1525)
1259
+ this.enforceTokenOp("offline_sync_apply"); // boundary dispatch (issue #1525)
1182
1260
  const body = await this.readValidatedBody(req, "offlineSyncApply", OFFLINE_SYNC_APPLY_MAX_BODY_BYTES);
1183
1261
  const result = await this.service.offlineSyncApply({
1184
1262
  namespace: this.resolveNamespace(req, body.namespace),
@@ -1191,7 +1269,7 @@ export class EngramAccessHttpServer {
1191
1269
  }
1192
1270
 
1193
1271
  if (req.method === "POST" && pathname === "/engram/v1/recall/explain") {
1194
- void getOperation("recall_explain"); // boundary dispatch (issue #1525)
1272
+ this.enforceTokenOp("recall_explain"); // boundary dispatch (issue #1525)
1195
1273
  const body = await this.readValidatedBody(req, "recallExplain");
1196
1274
  const response = await this.service.recallExplain({
1197
1275
  sessionKey: body.sessionKey,
@@ -1206,7 +1284,7 @@ export class EngramAccessHttpServer {
1206
1284
  req.method === "POST" &&
1207
1285
  (pathname === "/engram/v1/action-confidence" || pathname === "/remnic/v1/action-confidence")
1208
1286
  ) {
1209
- void getOperation("action_confidence"); // boundary dispatch (issue #1525)
1287
+ this.enforceTokenOp("action_confidence"); // boundary dispatch (issue #1525)
1210
1288
  const body = await this.readValidatedBody(req, "actionConfidence");
1211
1289
  this.respondJson(res, 200, await this.service.actionConfidence(body));
1212
1290
  return;
@@ -1216,7 +1294,7 @@ export class EngramAccessHttpServer {
1216
1294
  // the direct-answer retrieval tier. Orthogonal to /recall/explain
1217
1295
  // above, which returns a graph-path explanation document.
1218
1296
  if (req.method === "GET" && pathname === "/engram/v1/recall/tier-explain") {
1219
- void getOperation("recall_tier_explain"); // boundary dispatch (issue #1525)
1297
+ this.enforceTokenOp("recall_tier_explain"); // boundary dispatch (issue #1525)
1220
1298
  const sessionParam = parsed.searchParams.get("session");
1221
1299
  const sessionKey = sessionParam && sessionParam.length > 0 ? sessionParam : undefined;
1222
1300
  const namespaceParam = parsed.searchParams.get("namespace");
@@ -1240,7 +1318,7 @@ export class EngramAccessHttpServer {
1240
1318
  // GET stays cacheable; `namespace` / `session` / `budget` are
1241
1319
  // optional.
1242
1320
  if (req.method === "GET" && pathname === "/engram/v1/recall/xray") {
1243
- void getOperation("recall_xray"); // boundary dispatch (issue #1525)
1321
+ this.enforceTokenOp("recall_xray"); // boundary dispatch (issue #1525)
1244
1322
  const queryParam = parsed.searchParams.get("q");
1245
1323
  if (!queryParam || queryParam.trim().length === 0) {
1246
1324
  this.respondJson(res, 400, {
@@ -1349,7 +1427,7 @@ export class EngramAccessHttpServer {
1349
1427
  req.method === "GET" &&
1350
1428
  (pathname === "/engram/v1/wearables/status" || pathname === "/remnic/v1/wearables/status")
1351
1429
  ) {
1352
- void getOperation("wearables_status"); // boundary dispatch (issue #1525)
1430
+ this.enforceTokenOp("wearables_status"); // boundary dispatch (issue #1525)
1353
1431
  this.respondJson(res, 200, await this.service.wearablesStatus());
1354
1432
  return;
1355
1433
  }
@@ -1358,7 +1436,7 @@ export class EngramAccessHttpServer {
1358
1436
  req.method === "POST" &&
1359
1437
  (pathname === "/engram/v1/wearables/sync" || pathname === "/remnic/v1/wearables/sync")
1360
1438
  ) {
1361
- void getOperation("wearables_sync"); // boundary dispatch (issue #1525)
1439
+ this.enforceTokenOp("wearables_sync"); // boundary dispatch (issue #1525)
1362
1440
  const body = (await this.readJsonBody(req)) as Record<string, unknown>;
1363
1441
  const source = optionalQueryString(body.source, "source");
1364
1442
  const date = optionalQueryString(body.date, "date");
@@ -1399,7 +1477,7 @@ export class EngramAccessHttpServer {
1399
1477
  req.method === "GET" &&
1400
1478
  (pathname === "/engram/v1/wearables/transcript" || pathname === "/remnic/v1/wearables/transcript")
1401
1479
  ) {
1402
- void getOperation("transcript_day"); // boundary dispatch (issue #1525)
1480
+ this.enforceTokenOp("transcript_day"); // boundary dispatch (issue #1525)
1403
1481
  const date = parsed.searchParams.get("date");
1404
1482
  if (!date || date.trim().length === 0) {
1405
1483
  throw new EngramAccessInputError(
@@ -1425,7 +1503,7 @@ export class EngramAccessHttpServer {
1425
1503
  (pathname === "/engram/v1/wearables/transcripts/search" ||
1426
1504
  pathname === "/remnic/v1/wearables/transcripts/search")
1427
1505
  ) {
1428
- void getOperation("transcript_search"); // boundary dispatch (issue #1525)
1506
+ this.enforceTokenOp("transcript_search"); // boundary dispatch (issue #1525)
1429
1507
  const queryParam = parsed.searchParams.get("q");
1430
1508
  if (!queryParam || queryParam.trim().length === 0) {
1431
1509
  throw new EngramAccessInputError(
@@ -1452,7 +1530,7 @@ export class EngramAccessHttpServer {
1452
1530
  req.method === "GET" &&
1453
1531
  (pathname === "/engram/v1/wearables/memories" || pathname === "/remnic/v1/wearables/memories")
1454
1532
  ) {
1455
- void getOperation("transcript_memories"); // boundary dispatch (issue #1525)
1533
+ this.enforceTokenOp("transcript_memories"); // boundary dispatch (issue #1525)
1456
1534
  try {
1457
1535
  const memories = await this.service.wearablesTranscriptMemories({
1458
1536
  source: nonEmptyQueryParam(parsed.searchParams.get("source")),
@@ -1468,7 +1546,7 @@ export class EngramAccessHttpServer {
1468
1546
  }
1469
1547
 
1470
1548
  if (req.method === "POST" && pathname === "/engram/v1/observe") {
1471
- void getOperation("observe"); // boundary dispatch (issue #1525)
1549
+ this.enforceTokenOp("observe"); // boundary dispatch (issue #1525)
1472
1550
  const body = await this.readValidatedBody(req, "observe");
1473
1551
  const response = await this.service.observe(
1474
1552
  {
@@ -1508,7 +1586,7 @@ export class EngramAccessHttpServer {
1508
1586
  }
1509
1587
 
1510
1588
  if (req.method === "POST" && pathname === "/engram/v1/lcm/search") {
1511
- void getOperation("lcm_search"); // boundary dispatch (issue #1525)
1589
+ this.enforceTokenOp("lcm_search"); // boundary dispatch (issue #1525)
1512
1590
  const body = await this.readValidatedBody(req, "lcmSearch");
1513
1591
  const response = await this.service.lcmSearch({
1514
1592
  query: body.query,
@@ -1526,7 +1604,7 @@ export class EngramAccessHttpServer {
1526
1604
  req.method === "POST" &&
1527
1605
  (pathname === "/engram/v1/lcm/compaction/flush" || pathname === "/remnic/v1/lcm/compaction/flush")
1528
1606
  ) {
1529
- void getOperation("lcm_compaction_flush"); // boundary dispatch (issue #1525)
1607
+ this.enforceTokenOp("lcm_compaction_flush"); // boundary dispatch (issue #1525)
1530
1608
  const body = await this.readValidatedBody(req, "lcmCompactionFlush");
1531
1609
  this.ensureWriteRateLimitAvailable();
1532
1610
  const response = await this.service.lcmCompactionFlush({
@@ -1543,7 +1621,7 @@ export class EngramAccessHttpServer {
1543
1621
  req.method === "POST" &&
1544
1622
  (pathname === "/engram/v1/lcm/compaction/record" || pathname === "/remnic/v1/lcm/compaction/record")
1545
1623
  ) {
1546
- void getOperation("lcm_compaction_record"); // boundary dispatch (issue #1525)
1624
+ this.enforceTokenOp("lcm_compaction_record"); // boundary dispatch (issue #1525)
1547
1625
  const body = await this.readValidatedBody(req, "lcmCompactionRecord");
1548
1626
  this.ensureWriteRateLimitAvailable();
1549
1627
  const response = await this.service.lcmCompactionRecord({
@@ -1559,7 +1637,7 @@ export class EngramAccessHttpServer {
1559
1637
  }
1560
1638
 
1561
1639
  if (req.method === "GET" && pathname === "/engram/v1/lcm/status") {
1562
- void getOperation("lcm_status"); // boundary dispatch (issue #1525)
1640
+ this.enforceTokenOp("lcm_status"); // boundary dispatch (issue #1525)
1563
1641
  this.respondJson(res, 200, await this.service.lcmStatus());
1564
1642
  return;
1565
1643
  }
@@ -1612,7 +1690,7 @@ export class EngramAccessHttpServer {
1612
1690
  if (!op) {
1613
1691
  throw new EngramAccessInputError("access-boundary: operation not registered: coding_decision");
1614
1692
  }
1615
- const output = (await op.run(body, {
1693
+ const output = (await op.run(this.gatedBodyNamespace(req, body), {
1616
1694
  service: this.service,
1617
1695
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1618
1696
  })) as { result: unknown };
@@ -1636,7 +1714,7 @@ export class EngramAccessHttpServer {
1636
1714
  if (!op) {
1637
1715
  throw new EngramAccessInputError("access-boundary: operation not registered: coding_architecture");
1638
1716
  }
1639
- const output = (await op.run(body, {
1717
+ const output = (await op.run(this.gatedBodyNamespace(req, body), {
1640
1718
  service: this.service,
1641
1719
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1642
1720
  })) as { result: unknown };
@@ -1658,7 +1736,7 @@ export class EngramAccessHttpServer {
1658
1736
  if (!op) {
1659
1737
  throw new EngramAccessInputError("access-boundary: operation not registered: coding_delta");
1660
1738
  }
1661
- const output = (await op.run(body, {
1739
+ const output = (await op.run(this.gatedBodyNamespace(req, body), {
1662
1740
  service: this.service,
1663
1741
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1664
1742
  })) as { result: unknown };
@@ -1681,7 +1759,7 @@ export class EngramAccessHttpServer {
1681
1759
  if (!op) {
1682
1760
  throw new EngramAccessInputError("access-boundary: operation not registered: memory_correct_plan");
1683
1761
  }
1684
- const output = (await op.run(body, {
1762
+ const output = (await op.run(this.gatedBodyNamespace(req, body), {
1685
1763
  service: this.service,
1686
1764
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1687
1765
  })) as { result: unknown };
@@ -1697,7 +1775,7 @@ export class EngramAccessHttpServer {
1697
1775
  if (!op) {
1698
1776
  throw new EngramAccessInputError("access-boundary: operation not registered: memory_correct_apply");
1699
1777
  }
1700
- const output = (await op.run(body, {
1778
+ const output = (await op.run(this.gatedBodyNamespace(req, body), {
1701
1779
  service: this.service,
1702
1780
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1703
1781
  })) as { result: unknown };
@@ -1710,12 +1788,12 @@ export class EngramAccessHttpServer {
1710
1788
  const op = getOperation("correction_pending");
1711
1789
  if (op) {
1712
1790
  const output = (await op.run(
1713
- { namespace: parsed.searchParams.get("namespace") ?? undefined, sessionKey: parsed.searchParams.get("sessionKey") ?? undefined },
1791
+ { namespace: this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined), sessionKey: parsed.searchParams.get("sessionKey") ?? undefined },
1714
1792
  { service: this.service, authenticatedPrincipal: this.resolveRequestPrincipal(req) },
1715
1793
  )) as { result: unknown };
1716
1794
  this.respondJson(res, 200, output.result);
1717
1795
  } else {
1718
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
1796
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1719
1797
  const sessionKey = parsed.searchParams.get("sessionKey") ?? undefined;
1720
1798
  const plans = await this.service.correctionListPending({
1721
1799
  ...(namespace ? { namespace } : {}),
@@ -1728,7 +1806,7 @@ export class EngramAccessHttpServer {
1728
1806
  }
1729
1807
 
1730
1808
  if (req.method === "POST" && pathname === "/engram/v1/suggestions") {
1731
- void getOperation("suggestion_submit"); // boundary dispatch (issue #1525)
1809
+ this.enforceTokenOp("suggestion_submit"); // boundary dispatch (issue #1525)
1732
1810
  const body = await this.readValidatedBody(req, "suggestionSubmit");
1733
1811
  const request = {
1734
1812
  schemaVersion: body.schemaVersion,
@@ -1761,7 +1839,7 @@ export class EngramAccessHttpServer {
1761
1839
  }
1762
1840
 
1763
1841
  if (req.method === "GET" && pathname === "/engram/v1/memories") {
1764
- void getOperation("memory_list"); // boundary dispatch (issue #1525)
1842
+ this.enforceTokenOp("memory_list"); // boundary dispatch (issue #1525)
1765
1843
  const limit = parseStrictIntegerQuery(parsed.searchParams.get("limit"), "limit", 50, 1);
1766
1844
  const offset = parseStrictIntegerQuery(parsed.searchParams.get("offset"), "offset", 0, 0);
1767
1845
  const sort = parseMemorySort(parsed.searchParams.get("sort"));
@@ -1769,7 +1847,7 @@ export class EngramAccessHttpServer {
1769
1847
  query: parsed.searchParams.get("q") ?? undefined,
1770
1848
  status: parsed.searchParams.get("status") ?? undefined,
1771
1849
  category: parsed.searchParams.get("category") ?? undefined,
1772
- namespace: parsed.searchParams.get("namespace") ?? undefined,
1850
+ namespace: this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined),
1773
1851
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
1774
1852
  sort,
1775
1853
  limit,
@@ -1803,7 +1881,7 @@ export class EngramAccessHttpServer {
1803
1881
  const memoryMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)$/);
1804
1882
  if (req.method === "GET" && memoryMatch) {
1805
1883
  const memoryId = decodeURIComponent(memoryMatch[1] ?? "");
1806
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
1884
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1807
1885
  // Issue #1582 — thread the transport session key so a `[m:xxxx]` handle in
1808
1886
  // the path resolves against this session's recall history (codex review).
1809
1887
  // resolveRequestIdentity reads it from adapter identity / request headers.
@@ -1830,10 +1908,10 @@ export class EngramAccessHttpServer {
1830
1908
  }
1831
1909
 
1832
1910
  const timelineMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)\/timeline$/);
1833
- void getOperation("memory_timeline"); // boundary dispatch (issue #1525)
1834
1911
  if (req.method === "GET" && timelineMatch) {
1912
+ this.enforceTokenOp("memory_timeline"); // boundary dispatch (issue #1525)
1835
1913
  const memoryId = decodeURIComponent(timelineMatch[1] ?? "");
1836
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
1914
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1837
1915
  const limit = parseStrictIntegerQuery(parsed.searchParams.get("limit"), "limit", 200, 1);
1838
1916
  const response = await this.service.memoryTimeline(memoryId, namespace, limit, this.resolveRequestPrincipal(req));
1839
1917
  this.respondJson(res, response.found ? 200 : 404, response);
@@ -1841,11 +1919,11 @@ export class EngramAccessHttpServer {
1841
1919
  }
1842
1920
 
1843
1921
  if (req.method === "GET" && pathname === "/engram/v1/entities") {
1844
- void getOperation("entity_list"); // boundary dispatch (issue #1525)
1922
+ this.enforceTokenOp("entity_list"); // boundary dispatch (issue #1525)
1845
1923
  const limit = parseStrictIntegerQuery(parsed.searchParams.get("limit"), "limit", 50, 1);
1846
1924
  const offset = parseStrictIntegerQuery(parsed.searchParams.get("offset"), "offset", 0, 0);
1847
1925
  const response = await this.service.entityList({
1848
- namespace: parsed.searchParams.get("namespace") ?? undefined,
1926
+ namespace: this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined),
1849
1927
  query: parsed.searchParams.get("q") ?? undefined,
1850
1928
  limit,
1851
1929
  offset,
@@ -1855,20 +1933,20 @@ export class EngramAccessHttpServer {
1855
1933
  }
1856
1934
 
1857
1935
  const entityMatch = pathname.match(/^\/engram\/v1\/entities\/([^/]+)$/);
1858
- void getOperation("entity_get"); // boundary dispatch (issue #1525)
1859
1936
  if (req.method === "GET" && entityMatch) {
1937
+ this.enforceTokenOp("entity_get"); // boundary dispatch (issue #1525)
1860
1938
  const entityName = decodeURIComponent(entityMatch[1] ?? "");
1861
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
1939
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1862
1940
  const response = await this.service.entityGet(entityName, namespace);
1863
1941
  this.respondJson(res, response.found ? 200 : 404, response);
1864
1942
  return;
1865
1943
  }
1866
1944
 
1867
1945
  if (req.method === "GET" && pathname === "/engram/v1/review-queue") {
1868
- void getOperation("review_queue_list"); // boundary dispatch (issue #1525)
1946
+ this.enforceTokenOp("review_queue_list"); // boundary dispatch (issue #1525)
1869
1947
  const response = await this.service.reviewQueue(
1870
1948
  parsed.searchParams.get("runId") ?? undefined,
1871
- parsed.searchParams.get("namespace") ?? undefined,
1949
+ this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined),
1872
1950
  this.resolveRequestPrincipal(req),
1873
1951
  );
1874
1952
  this.respondJson(res, 200, response);
@@ -1876,23 +1954,25 @@ export class EngramAccessHttpServer {
1876
1954
  }
1877
1955
 
1878
1956
  if (req.method === "GET" && pathname === "/engram/v1/maintenance") {
1879
- void getOperation("maintenance_status"); // boundary dispatch (issue #1525)
1880
- this.respondJson(res, 200, await this.service.maintenance(parsed.searchParams.get("namespace") ?? undefined, this.resolveRequestPrincipal(req)));
1957
+ this.enforceTokenOp("maintenance_status"); // boundary dispatch (issue #1525)
1958
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1959
+ this.respondJson(res, 200, await this.service.maintenance(namespace, this.resolveRequestPrincipal(req)));
1881
1960
  return;
1882
1961
  }
1883
1962
 
1884
1963
  if (req.method === "GET" && pathname === "/engram/v1/quality") {
1885
- void getOperation("quality_status"); // boundary dispatch (issue #1525)
1886
- this.respondJson(res, 200, await this.service.quality(parsed.searchParams.get("namespace") ?? undefined, this.resolveRequestPrincipal(req)));
1964
+ this.enforceTokenOp("quality_status"); // boundary dispatch (issue #1525)
1965
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
1966
+ this.respondJson(res, 200, await this.service.quality(namespace, this.resolveRequestPrincipal(req)));
1887
1967
  return;
1888
1968
  }
1889
1969
 
1890
1970
  if (req.method === "GET" && pathname === "/engram/v1/trust-zones/status") {
1891
- void getOperation("trust_zones_status"); // boundary dispatch (issue #1525)
1971
+ this.enforceTokenOp("trust_zones_status"); // boundary dispatch (issue #1525)
1892
1972
  this.respondJson(
1893
1973
  res,
1894
1974
  200,
1895
- await this.service.trustZoneStatus(parsed.searchParams.get("namespace") ?? undefined, this.resolveRequestPrincipal(req)),
1975
+ await this.service.trustZoneStatus(this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined), this.resolveRequestPrincipal(req)),
1896
1976
  );
1897
1977
  return;
1898
1978
  }
@@ -1901,7 +1981,7 @@ export class EngramAccessHttpServer {
1901
1981
  // scoped via the same resolver used by recall/trust-zones so cross-
1902
1982
  // tenant reads aren't possible (CLAUDE.md rule 42).
1903
1983
  if (req.method === "GET" && pathname === "/engram/v1/procedural/stats") {
1904
- void getOperation("procedural_stats"); // boundary dispatch (issue #1525)
1984
+ this.enforceTokenOp("procedural_stats"); // boundary dispatch (issue #1525)
1905
1985
  const namespaceParam = parsed.searchParams.get("namespace");
1906
1986
  this.respondJson(
1907
1987
  res,
@@ -1922,7 +2002,7 @@ export class EngramAccessHttpServer {
1922
2002
  }
1923
2003
 
1924
2004
  if (req.method === "GET" && pathname === "/engram/v1/trust-zones/records") {
1925
- void getOperation("trust_zones_records"); // boundary dispatch (issue #1525)
2005
+ this.enforceTokenOp("trust_zones_records"); // boundary dispatch (issue #1525)
1926
2006
  const limit = parseStrictIntegerQuery(parsed.searchParams.get("limit"), "limit", 25, 1);
1927
2007
  const offset = parseStrictIntegerQuery(parsed.searchParams.get("offset"), "offset", 0, 0);
1928
2008
  const response = await this.service.trustZoneBrowse({
@@ -1930,7 +2010,7 @@ export class EngramAccessHttpServer {
1930
2010
  zone: parseTrustZoneFilter(parsed.searchParams.get("zone")),
1931
2011
  kind: parseTrustZoneKindFilter(parsed.searchParams.get("kind")),
1932
2012
  sourceClass: parseTrustZoneSourceClassFilter(parsed.searchParams.get("sourceClass")),
1933
- namespace: parsed.searchParams.get("namespace") ?? undefined,
2013
+ namespace: this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined),
1934
2014
  limit,
1935
2015
  offset,
1936
2016
  }, this.resolveRequestPrincipal(req));
@@ -1939,7 +2019,7 @@ export class EngramAccessHttpServer {
1939
2019
  }
1940
2020
 
1941
2021
  if (req.method === "POST" && pathname === "/engram/v1/review-disposition") {
1942
- void getOperation("review_disposition"); // boundary dispatch (issue #1525)
2022
+ this.enforceTokenOp("review_disposition"); // boundary dispatch (issue #1525)
1943
2023
  const body = await this.readValidatedBody(req, "reviewDisposition");
1944
2024
  this.ensureWriteRateLimitAvailable();
1945
2025
  const response = await this.service.reviewDisposition({
@@ -1957,7 +2037,7 @@ export class EngramAccessHttpServer {
1957
2037
  }
1958
2038
 
1959
2039
  if (req.method === "POST" && pathname === "/engram/v1/trust-zones/promote") {
1960
- void getOperation("trust_zones_promote"); // boundary dispatch (issue #1525)
2040
+ this.enforceTokenOp("trust_zones_promote"); // boundary dispatch (issue #1525)
1961
2041
  const body = await this.readValidatedBody(req, "trustZonePromote");
1962
2042
  const dryRun = body.dryRun === true;
1963
2043
  if (!dryRun) {
@@ -1981,7 +2061,7 @@ export class EngramAccessHttpServer {
1981
2061
  }
1982
2062
 
1983
2063
  if (req.method === "POST" && pathname === "/engram/v1/trust-zones/demo-seed") {
1984
- void getOperation("trust_zones_demo_seed"); // boundary dispatch (issue #1525)
2064
+ this.enforceTokenOp("trust_zones_demo_seed"); // boundary dispatch (issue #1525)
1985
2065
  const body = await this.readValidatedBody(req, "trustZoneDemoSeed");
1986
2066
  const dryRun = body.dryRun === true;
1987
2067
  if (!dryRun) {
@@ -2003,7 +2083,7 @@ export class EngramAccessHttpServer {
2003
2083
 
2004
2084
  // Citation usage tracking (issue #379)
2005
2085
  if (req.method === "POST" && pathname === "/v1/citations/observed") {
2006
- void getOperation("citations_observed"); // boundary dispatch (issue #1525)
2086
+ this.enforceTokenOp("citations_observed"); // boundary dispatch (issue #1525)
2007
2087
  const body = await this.readJsonBody(req);
2008
2088
  if (!body || typeof body !== "object" || Array.isArray(body)) {
2009
2089
  throw new HttpError(400, "request body must be a JSON object", "invalid_body");
@@ -2075,19 +2155,15 @@ export class EngramAccessHttpServer {
2075
2155
 
2076
2156
  // ── Contradiction Review (issue #520) ─────────────────────────────────────
2077
2157
  if (req.method === "GET" && pathname === "/engram/v1/review/contradictions") {
2078
- void getOperation("review_list"); // boundary dispatch (issue #1525)
2158
+ this.enforceTokenOp("review_list"); // boundary dispatch (issue #1525)
2079
2159
  const VALID_FILTERS = new Set(["all", "unresolved", "contradicts", "independent", "duplicates", "needs-user"]);
2080
2160
  const rawFilter = parsed.searchParams.get("filter") ?? "unresolved";
2081
2161
  if (!VALID_FILTERS.has(rawFilter)) {
2082
2162
  this.respondJson(res, 400, { error: `Invalid filter '${rawFilter}'. Valid: ${[...VALID_FILTERS].join(", ")}` });
2083
2163
  return;
2084
2164
  }
2085
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
2165
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
2086
2166
  const limit = parseStrictIntegerQuery(parsed.searchParams.get("limit"), "limit", 50, 1);
2087
- const {
2088
- isDefaultReviewNamespace,
2089
- listPairs,
2090
- } = await import("./contradiction/contradiction-review.js");
2091
2167
  const principal = this.resolveRequestPrincipal(req);
2092
2168
  const resolved = await this.service.getReadableStorageForNamespace(namespace, principal);
2093
2169
  const reviewNamespace = this.service.configRef.namespacesEnabled ? resolved.namespace : undefined;
@@ -2105,14 +2181,30 @@ export class EngramAccessHttpServer {
2105
2181
  }
2106
2182
 
2107
2183
  if (req.method === "GET" && pathname.startsWith("/engram/v1/review/contradictions/")) {
2108
- void getOperation("contradiction_detail"); // boundary dispatch (issue #1525)
2184
+ this.enforceTokenOp("contradiction_detail"); // boundary dispatch (issue #1525)
2109
2185
  const pairId = pathname.split("/").pop() ?? "";
2110
- const { readPair } = await import("./contradiction/contradiction-review.js");
2111
2186
  const pair = readPair(this.service.memoryDir, pairId);
2112
2187
  if (!pair) {
2113
2188
  this.respondJson(res, 404, { error: "pair_not_found" });
2114
2189
  return;
2115
2190
  }
2191
+ // Per-token namespace enforcement (issues #1850 round 2 / round 4): the
2192
+ // pair is fetched BY ID, so its namespace comes from the record — NOT a
2193
+ // query param that resolveNamespace() already gates. A namespace-scoped
2194
+ // bearer that knows a pair id must not read contradiction data in a
2195
+ // namespace outside its allow-list. Fail closed (403) before the
2196
+ // principal-storage check so the token's declared scope is the
2197
+ // outermost gate. Legacy pairs carry no namespace (undefined), which
2198
+ // downstream storage maps to the server DEFAULT — route the record
2199
+ // namespace through the SAME effective-namespace chokepoint
2200
+ // (enforceNamespaceAllowList maps undefined → default) so a scoped token
2201
+ // whose allow-list INCLUDES the default can read/resolve a legacy pair,
2202
+ // while one that does NOT is still denied. No-op for unrestricted tokens.
2203
+ enforceNamespaceAllowList(
2204
+ tokenCapabilityStore.getStore(),
2205
+ pair.namespace,
2206
+ this.service.configRef?.defaultNamespace,
2207
+ );
2116
2208
  try {
2117
2209
  await this.service.getReadableStorageForNamespace(pair.namespace, this.resolveRequestPrincipal(req));
2118
2210
  } catch {
@@ -2124,7 +2216,7 @@ export class EngramAccessHttpServer {
2124
2216
  }
2125
2217
 
2126
2218
  if (req.method === "POST" && pathname === "/engram/v1/review/resolve") {
2127
- void getOperation("review_resolve"); // boundary dispatch (issue #1525)
2219
+ this.enforceTokenOp("review_resolve"); // boundary dispatch (issue #1525)
2128
2220
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2129
2221
  const pairId = typeof body.pairId === "string" ? body.pairId : "";
2130
2222
  const verb = typeof body.verb === "string" ? body.verb : "";
@@ -2132,11 +2224,32 @@ export class EngramAccessHttpServer {
2132
2224
  this.respondJson(res, 400, { error: "pairId and verb are required" });
2133
2225
  return;
2134
2226
  }
2135
- const { isValidResolutionVerb, executeResolution } = await import("./contradiction/resolution.js");
2136
2227
  if (!isValidResolutionVerb(verb)) {
2137
2228
  this.respondJson(res, 400, { error: `Invalid verb: ${verb}. Must be one of: keep-a, keep-b, merge, both-valid, needs-more-context` });
2138
2229
  return;
2139
2230
  }
2231
+ // Per-token namespace enforcement (issues #1850 round 2 / round 4): the
2232
+ // resolution target is selected BY pairId, so the affected namespace
2233
+ // comes from the record — NOT a request param that resolveNamespace()
2234
+ // already gates. A namespace-scoped bearer must not mutate a
2235
+ // contradiction pair in a namespace outside its allow-list. Load the
2236
+ // pair to learn its namespace, enforce the token's scope, and fail
2237
+ // closed (403) BEFORE dispatching the (mutating) resolution. Legacy
2238
+ // pairs carry no namespace (undefined), which downstream storage maps to
2239
+ // the server DEFAULT — use the SAME effective-namespace chokepoint
2240
+ // (enforceNamespaceAllowList maps undefined → default) as the detail
2241
+ // route so a scoped token whose allow-list INCLUDES the default can
2242
+ // resolve a legacy pair. A missing pair falls through to
2243
+ // executeResolution's existing not-found result. No-op for unrestricted
2244
+ // tokens.
2245
+ const targetPair = readPair(this.service.memoryDir, pairId);
2246
+ if (targetPair) {
2247
+ enforceNamespaceAllowList(
2248
+ tokenCapabilityStore.getStore(),
2249
+ targetPair.namespace,
2250
+ this.service.configRef?.defaultNamespace,
2251
+ );
2252
+ }
2140
2253
  const principal = this.resolveRequestPrincipal(req);
2141
2254
  const result = await executeResolution(this.service.memoryDir, this.service.storageRef, pairId, verb, {
2142
2255
  mergedMemoryId: typeof body.mergedMemoryId === "string" ? body.mergedMemoryId : undefined,
@@ -2162,7 +2275,7 @@ export class EngramAccessHttpServer {
2162
2275
  // params so the surface stays cacheable; invalid values yield 400 with
2163
2276
  // a descriptive body (CLAUDE.md rule 51 — never silently default).
2164
2277
  if (req.method === "GET" && pathname === "/engram/v1/graph/snapshot") {
2165
- void getOperation("graph_snapshot"); // boundary dispatch (issue #1525)
2278
+ this.enforceTokenOp("graph_snapshot"); // boundary dispatch (issue #1525)
2166
2279
  const limitRaw = parsed.searchParams.get("limit");
2167
2280
  let limit: number | undefined;
2168
2281
  if (limitRaw !== null && limitRaw.length > 0) {
@@ -2255,7 +2368,7 @@ export class EngramAccessHttpServer {
2255
2368
  }
2256
2369
 
2257
2370
  if (req.method === "POST" && pathname === "/engram/v1/contradiction-scan") {
2258
- void getOperation("contradiction_scan_run"); // boundary dispatch (issue #1525)
2371
+ this.enforceTokenOp("contradiction_scan_run"); // boundary dispatch (issue #1525)
2259
2372
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2260
2373
  const { runContradictionScan } = await import("./contradiction/contradiction-scan.js");
2261
2374
  const principal = this.resolveRequestPrincipal(req);
@@ -2268,7 +2381,7 @@ export class EngramAccessHttpServer {
2268
2381
  this.service.getWritableStorageForNamespace(namespace, principal),
2269
2382
  localLlm: this.service.localLlmRef,
2270
2383
  fallbackLlm: this.service.fallbackLlmRef,
2271
- namespace: typeof body.namespace === "string" ? body.namespace : undefined,
2384
+ namespace: this.resolveNamespace(req, typeof body.namespace === "string" ? body.namespace : undefined),
2272
2385
  });
2273
2386
  this.respondJson(res, 200, result);
2274
2387
  return;
@@ -2294,15 +2407,37 @@ export class EngramAccessHttpServer {
2294
2407
  // The stream sends a heartbeat `data: {"type":"heartbeat"}\n\n` every
2295
2408
  // 25 s so load balancers and proxies don't time out idle connections.
2296
2409
  if (req.method === "GET" && pathname === "/engram/v1/graph/events") {
2297
- void getOperation("graph_events"); // boundary dispatch (issue #1525)
2410
+ this.enforceTokenOp("graph_events"); // boundary dispatch (issue #1525)
2298
2411
  await this.handleGraphEventsSSE(req, res);
2299
2412
  return;
2300
2413
  }
2301
2414
 
2302
2415
  // ── Chat endpoints (issue #1583) ────────────────────────────────────────
2303
2416
  if (req.method === "POST" && pathname === "/engram/v1/chat/message") {
2304
- void getOperation("chat_message"); // boundary dispatch (issue #1525)
2417
+ this.enforceTokenOp("chat_message"); // boundary dispatch (issue #1525)
2305
2418
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2419
+ // Issue #1850 round 5 (finding 2): a resumed chat session is an
2420
+ // id-loaded record whose namespace bypassed the token allow-list. Gate
2421
+ // its stored namespace before posting (fail closed; no-op unrestricted).
2422
+ const resumeChatSessionId = typeof body.chatSessionId === "string" ? body.chatSessionId : undefined;
2423
+ if (resumeChatSessionId) {
2424
+ await enforceChatSessionNamespace(this.service, resumeChatSessionId);
2425
+ } else {
2426
+ // NEW chat session (no chatSessionId): processChatMessage will mint a
2427
+ // fresh session under the effective namespace. The HTTP chat handler
2428
+ // does not forward a request namespace, so the new session inherits
2429
+ // the server DEFAULT — route that effective namespace through the
2430
+ // SAME chokepoint as the resume path (enforceNamespaceAllowList maps
2431
+ // undefined → default) so a namespace-scoped token CANNOT start a
2432
+ // chat in a namespace outside its allow-list (its server default may
2433
+ // be unlisted, or the server may carry no configured default). Fail
2434
+ // closed; no-op for unrestricted/legacy tokens (issue #1850 round 7).
2435
+ enforceNamespaceAllowList(
2436
+ tokenCapabilityStore.getStore(),
2437
+ undefined,
2438
+ this.service.configRef?.defaultNamespace,
2439
+ );
2440
+ }
2306
2441
  await handleChatMessage(
2307
2442
  req, res, body,
2308
2443
  { service: this.service, config: this.service.configRef?.chat, memoryDir: this.service.memoryDir },
@@ -2312,7 +2447,11 @@ export class EngramAccessHttpServer {
2312
2447
  }
2313
2448
  const chatEventsMatch = /^\/engram\/v1\/chat\/events\/([^/]+)$/.exec(pathname);
2314
2449
  if (req.method === "GET" && chatEventsMatch) {
2315
- void getOperation("chat_events"); // boundary dispatch (issue #1525)
2450
+ this.enforceTokenOp("chat_events"); // boundary dispatch (issue #1525)
2451
+ // Issue #1850 round 5 (finding 2): the SSE target is an id-loaded chat
2452
+ // session whose namespace bypassed the token allow-list. Gate its stored
2453
+ // namespace before streaming (fail closed; no-op unrestricted).
2454
+ await enforceChatSessionNamespace(this.service, chatEventsMatch[1] ?? "");
2316
2455
  await handleChatEventsSSE(
2317
2456
  req, res, chatEventsMatch[1] ?? "",
2318
2457
  {
@@ -2339,8 +2478,8 @@ export class EngramAccessHttpServer {
2339
2478
  // Read-only; namespace-aware via resolveRequestPrincipal so cross-tenant
2340
2479
  // reads are not possible (CLAUDE.md rule 42).
2341
2480
  if (req.method === "GET" && pathname === "/engram/v1/console/state") {
2342
- void getOperation("console_state"); // boundary dispatch (issue #1525)
2343
- const namespace = parsed.searchParams.get("namespace") ?? undefined;
2481
+ this.enforceTokenOp("console_state"); // boundary dispatch (issue #1525)
2482
+ const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
2344
2483
  const snapshot = await this.service.consoleState(namespace, this.resolveRequestPrincipal(req));
2345
2484
  this.respondJson(res, 200, snapshot);
2346
2485
  return;
@@ -2353,7 +2492,7 @@ export class EngramAccessHttpServer {
2353
2492
  // DELETE /engram/v1/peers/:id?forget=true — destructive full purge (issue #679 completion)
2354
2493
  // GET /engram/v1/peers/:id/profile — get peer profile
2355
2494
  if (req.method === "GET" && pathname === "/engram/v1/peers") {
2356
- void getOperation("peer_list"); // boundary dispatch (issue #1525)
2495
+ this.enforceTokenOp("peer_list"); // boundary dispatch (issue #1525)
2357
2496
  const result = await this.service.peerList();
2358
2497
  this.respondJson(res, 200, result);
2359
2498
  return;
@@ -2361,7 +2500,7 @@ export class EngramAccessHttpServer {
2361
2500
 
2362
2501
  const peerProfileMatch = /^\/engram\/v1\/peers\/([^/]+)\/profile$/.exec(pathname);
2363
2502
  if (peerProfileMatch) {
2364
- void getOperation("peer_profile_get"); // boundary dispatch (issue #1525)
2503
+ this.enforceTokenOp("peer_profile_get"); // boundary dispatch (issue #1525)
2365
2504
  if (req.method !== "GET") {
2366
2505
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2367
2506
  return;
@@ -2381,7 +2520,7 @@ export class EngramAccessHttpServer {
2381
2520
  const peerId = decodePeerIdSegment(peerIdMatch[1] ?? "");
2382
2521
 
2383
2522
  if (req.method === "GET") {
2384
- void getOperation("peer_get"); // boundary dispatch (issue #1525)
2523
+ this.enforceTokenOp("peer_get"); // boundary dispatch (issue #1525)
2385
2524
  const result = await this.service.peerGet(peerId);
2386
2525
  if (!result.found) {
2387
2526
  this.respondJson(res, 404, { error: "peer_not_found", code: "peer_not_found" });
@@ -2392,7 +2531,7 @@ export class EngramAccessHttpServer {
2392
2531
  }
2393
2532
 
2394
2533
  if (req.method === "PUT") {
2395
- void getOperation("peer_set"); // boundary dispatch (issue #1525)
2534
+ this.enforceTokenOp("peer_set"); // boundary dispatch (issue #1525)
2396
2535
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2397
2536
  // Reject malformed types up front rather than silently dropping them
2398
2537
  // to undefined and letting peerSet fall back to defaults
@@ -2421,7 +2560,7 @@ export class EngramAccessHttpServer {
2421
2560
  }
2422
2561
 
2423
2562
  if (req.method === "DELETE") {
2424
- void getOperation("peer_delete"); // boundary dispatch (issue #1525)
2563
+ this.enforceTokenOp("peer_delete"); // boundary dispatch (issue #1525)
2425
2564
  // `?forget=true` triggers the destructive full-purge path (issue #679
2426
2565
  // completion). The caller must also pass `confirm=yes` in the request
2427
2566
  // body; absent confirmation yields 400. Plain DELETE (no ?forget) keeps
@@ -2454,7 +2593,7 @@ export class EngramAccessHttpServer {
2454
2593
  // ── Dreams telemetry (issue #678 PR 3+4) ──────────────────────────────────
2455
2594
 
2456
2595
  if (req.method === "GET" && pathname === "/engram/v1/dreams/status") {
2457
- void getOperation("dreams_status"); // boundary dispatch (issue #1525)
2596
+ this.enforceTokenOp("dreams_status"); // boundary dispatch (issue #1525)
2458
2597
  const { normalizeDreamsStatusWindowHours } = await import("./maintenance/dreams-ledger.js");
2459
2598
  const windowHoursRaw = parsed.searchParams.get("windowHours");
2460
2599
  let windowHours: number;
@@ -2467,7 +2606,7 @@ export class EngramAccessHttpServer {
2467
2606
  return;
2468
2607
  }
2469
2608
  const namespaceParam = parsed.searchParams.get("namespace");
2470
- const namespace = namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined;
2609
+ const namespace = this.resolveNamespace(req, namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined);
2471
2610
  const result = await this.service.dreamsStatus({
2472
2611
  windowHours,
2473
2612
  namespace,
@@ -2478,7 +2617,7 @@ export class EngramAccessHttpServer {
2478
2617
  }
2479
2618
 
2480
2619
  if (req.method === "POST" && pathname === "/engram/v1/dreams/run") {
2481
- void getOperation("dreams_run"); // boundary dispatch (issue #1525)
2620
+ this.enforceTokenOp("dreams_run"); // boundary dispatch (issue #1525)
2482
2621
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2483
2622
  const VALID_PHASES = ["lightSleep", "rem", "deepSleep"] as const;
2484
2623
  const phase = typeof body.phase === "string" ? body.phase : undefined;
@@ -2509,8 +2648,7 @@ export class EngramAccessHttpServer {
2509
2648
  return;
2510
2649
  }
2511
2650
  const dryRun = body.dryRun === true;
2512
- const namespace =
2513
- typeof body.namespace === "string" ? body.namespace : undefined;
2651
+ const namespace = this.resolveNamespace(req, typeof body.namespace === "string" ? body.namespace : undefined);
2514
2652
  if (!dryRun) {
2515
2653
  this.ensureWriteRateLimitAvailable();
2516
2654
  }
@@ -2540,6 +2678,7 @@ export class EngramAccessHttpServer {
2540
2678
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2541
2679
  return;
2542
2680
  }
2681
+ this.requireOperatorToken();
2543
2682
  const principal = this.resolveRequestPrincipal(req);
2544
2683
  let sessionKey: string | undefined;
2545
2684
  let namespaceOverride: string | undefined;
@@ -2563,12 +2702,12 @@ export class EngramAccessHttpServer {
2563
2702
  this.respondJson(res, 200, inspection);
2564
2703
  return;
2565
2704
  }
2566
-
2567
2705
  if (pathname === "/engram/v1/admin/namespaces") {
2568
2706
  if (req.method !== "GET") {
2569
2707
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2570
2708
  return;
2571
2709
  }
2710
+ this.requireOperatorToken();
2572
2711
  const kind = parsed.searchParams.get("kind");
2573
2712
  const principal = parsed.searchParams.get("principal");
2574
2713
  const projectId = parsed.searchParams.get("projectId");
@@ -2584,32 +2723,32 @@ export class EngramAccessHttpServer {
2584
2723
  this.respondJson(res, 200, result);
2585
2724
  return;
2586
2725
  }
2587
-
2588
2726
  if (pathname === "/engram/v1/admin/maintenance-health") {
2589
2727
  if (req.method !== "GET") {
2590
2728
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2591
2729
  return;
2592
2730
  }
2731
+ this.requireOperatorToken();
2593
2732
  const report = await this.service.adminMaintenanceHealth();
2594
2733
  this.respondJson(res, 200, report);
2595
2734
  return;
2596
2735
  }
2597
-
2598
2736
  if (pathname === "/engram/v1/admin/transcript-audit") {
2599
2737
  if (req.method !== "GET") {
2600
2738
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2601
2739
  return;
2602
2740
  }
2741
+ this.requireOperatorToken();
2603
2742
  const report = await this.service.adminTranscriptAudit();
2604
2743
  this.respondJson(res, 200, report);
2605
2744
  return;
2606
2745
  }
2607
-
2608
2746
  if (pathname === "/engram/v1/admin/promote") {
2609
2747
  if (req.method !== "POST") {
2610
2748
  this.respondJson(res, 405, { error: "method_not_allowed", code: "method_not_allowed" });
2611
2749
  return;
2612
2750
  }
2751
+ this.requireOperatorToken();
2613
2752
  const body = await this.readJsonBody(req) as Record<string, unknown>;
2614
2753
  if (typeof body.sourceMemoryId !== "string" || body.sourceMemoryId.trim().length === 0) {
2615
2754
  this.respondJson(res, 400, {
@@ -2727,7 +2866,7 @@ export class EngramAccessHttpServer {
2727
2866
  // default namespace when absent.
2728
2867
  const parsed = new URL(req.url ?? "/", `http://${hostToUrlAuthority(this.host)}`);
2729
2868
  const namespaceParam = parsed.searchParams.get("namespace");
2730
- const namespace = namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined;
2869
+ const namespace = this.resolveNamespace(req, namespaceParam && namespaceParam.length > 0 ? namespaceParam : undefined);
2731
2870
  // Resolve to the per-namespace storage directory so the bus subscription
2732
2871
  // is scoped to the correct tenant (CLAUDE.md rule 42).
2733
2872
  // Pass the request principal so namespace ACL is enforced — without it,
@@ -2935,6 +3074,29 @@ export class EngramAccessHttpServer {
2935
3074
  })();
2936
3075
  const mcpCorrelationId = correlationIdStore.getStore() ?? randomUUID();
2937
3076
  const requestIdentity = this.resolveRequestIdentity(req);
3077
+ // Per-token namespace enforcement on the MCP dispatch path (issue #1850
3078
+ // finding 1): MCP `tools/call` previously never applied the namespace
3079
+ // allow-list, so a namespace-scoped bearer could reach another tenant via
3080
+ // tool args or the adapter `namespaceOverride`. Run the SAME effective-
3081
+ // namespace chokepoint the REST surface runs. The effective namespace is
3082
+ // the explicit tool arg, else the adapter override, else the server
3083
+ // default (op.run with no namespace reaches the default tenant). Only
3084
+ // namespace-scoped tools are gated — namespace-agnostic tools (peer/
3085
+ // wearables) stay ungated, matching the REST surface. Fail closed (403)
3086
+ // BEFORE op.run; no-op for unrestricted tokens.
3087
+ if (request.method === "tools/call" && this.mcpServer.toolAcceptsNamespace(toolName)) {
3088
+ const mcpExplicitNamespace =
3089
+ toolArgs !== null && typeof toolArgs === "object" && !Array.isArray(toolArgs)
3090
+ && typeof (toolArgs as Record<string, unknown>).namespace === "string"
3091
+ && ((toolArgs as Record<string, unknown>).namespace as string).length > 0
3092
+ ? ((toolArgs as Record<string, unknown>).namespace as string)
3093
+ : undefined;
3094
+ enforceNamespaceAllowList(
3095
+ tokenCapabilityStore.getStore(),
3096
+ mcpExplicitNamespace ?? requestIdentity.namespace,
3097
+ this.service.configRef?.defaultNamespace,
3098
+ );
3099
+ }
2938
3100
  const response = await this.mcpServer.handleRequest(request, {
2939
3101
  principalOverride: requestIdentity.principal,
2940
3102
  namespaceOverride: requestIdentity.namespace,
@@ -3280,13 +3442,27 @@ export class EngramAccessHttpServer {
3280
3442
  }
3281
3443
 
3282
3444
  private isAuthorized(req: IncomingMessage, pathname?: string): boolean {
3445
+ return this.resolveAuthorizedEntry(req, pathname) !== null;
3446
+ }
3447
+
3448
+ /**
3449
+ * Resolve the presenting token's entry for an authorized request, or null.
3450
+ * Captures the matched entry — including its capabilities record — so the
3451
+ * op/namespace capability enforcement (issue #1837) uses the SAME entry
3452
+ * that validated. Static operator tokens (`authToken`/`authTokens`) and
3453
+ * the string-token getter carry no capabilities ⇒ unrestricted.
3454
+ */
3455
+ private resolveAuthorizedEntry(
3456
+ req: IncomingMessage,
3457
+ pathname?: string,
3458
+ ): { token: string; connector?: string; capabilities?: TokenCapabilities } | null {
3283
3459
  if (
3284
3460
  !this.authToken &&
3285
3461
  this.authTokens.length === 0 &&
3286
3462
  !this.authTokensGetter &&
3287
3463
  !this.authTokenEntriesGetter
3288
3464
  ) {
3289
- return false;
3465
+ return null;
3290
3466
  }
3291
3467
  // Primary path: Authorization: Bearer <token> header.
3292
3468
  const raw = req.headers.authorization;
@@ -3319,38 +3495,88 @@ export class EngramAccessHttpServer {
3319
3495
  // Malformed URL — don't authenticate
3320
3496
  }
3321
3497
  }
3322
- if (!candidate) return false;
3498
+ if (!candidate) return null;
3323
3499
  const token = candidate;
3324
- // Check primary token
3325
- if (this.authToken && this.timingSafeStringEqual(token, this.authToken)) return true;
3500
+ // Check primary token (static operator token — no capabilities ⇒ unrestricted)
3501
+ if (this.authToken && this.timingSafeStringEqual(token, this.authToken)) {
3502
+ return { token };
3503
+ }
3326
3504
  // Check static multi-connector tokens
3327
3505
  for (const valid of this.authTokens) {
3328
- if (this.timingSafeStringEqual(token, valid)) return true;
3506
+ if (this.timingSafeStringEqual(token, valid)) return { token: valid };
3329
3507
  }
3330
3508
  // Entry-based dynamic tokens are AUTHORITATIVE when configured: the
3331
3509
  // dynamic-token decision ends here (no fall-through to the string
3332
3510
  // getter, which carries no identity and would bypass the policy).
3333
3511
  // Validation and connector identity come from the same snapshot entry,
3334
3512
  // so a scope policy can never observe a token fresher than the
3335
- // identity it scopes (mint/revoke coherence).
3513
+ // identity it scopes (mint/revoke coherence). Capabilities ride on the
3514
+ // same entry so enforcement can never lag the token that validated.
3336
3515
  if (this.authTokenEntriesGetter) {
3337
3516
  for (const entry of this.authTokenEntriesGetter()) {
3338
3517
  if (!this.timingSafeStringEqual(token, entry.token)) continue;
3339
- if (!this.tokenPathPolicy) return true;
3518
+ const matched = { token: entry.token, connector: entry.connector, capabilities: entry.capabilities };
3519
+ if (!this.tokenPathPolicy) return matched;
3340
3520
  // Fail closed: a policy without a connector identity denies.
3341
- if (typeof entry.connector !== "string" || entry.connector.length === 0) return false;
3342
- return this.tokenPathPolicy(entry.connector, pathname);
3521
+ if (typeof entry.connector !== "string" || entry.connector.length === 0) return null;
3522
+ return this.tokenPathPolicy(entry.connector, pathname) ? matched : null;
3343
3523
  }
3344
- return false;
3524
+ return null;
3345
3525
  }
3346
3526
  // String-token getter (no identity, no policy) — only consulted when
3347
3527
  // no entry getter is configured.
3348
3528
  if (this.authTokensGetter) {
3349
3529
  for (const valid of this.authTokensGetter()) {
3350
- if (this.timingSafeStringEqual(token, valid)) return true;
3530
+ if (this.timingSafeStringEqual(token, valid)) return { token: valid };
3351
3531
  }
3352
3532
  }
3353
- return false;
3533
+ return null;
3534
+ }
3535
+
3536
+ /** Cache for per-request capability resolution (avoids re-resolving). */
3537
+ private capabilityCache = new WeakMap<IncomingMessage, TokenCapabilities | null>();
3538
+
3539
+ /**
3540
+ * Resolve the presenting token's capabilities for this request, or
3541
+ * undefined when the token is unrestricted (legacy / static operator /
3542
+ * explicit-unrestricted record). Cached per-request. `null` sentinel
3543
+ * distinguishes "resolved as unrestricted" from "not yet resolved".
3544
+ */
3545
+ private resolveTokenCapabilities(
3546
+ req: IncomingMessage,
3547
+ pathname?: string,
3548
+ ): TokenCapabilities | undefined {
3549
+ if (this.capabilityCache.has(req)) {
3550
+ return this.capabilityCache.get(req) ?? undefined;
3551
+ }
3552
+ const caps = this.resolveAuthorizedEntry(req, pathname)?.capabilities;
3553
+ this.capabilityCache.set(req, caps ?? null);
3554
+ return caps;
3555
+ }
3556
+
3557
+ /**
3558
+ * Enforce the presenting token's ops allow-list for an HTTP route that
3559
+ * dispatches directly to the service (not through the boundary). Reads the
3560
+ * per-request capability context set in `handle`; unrestricted tokens
3561
+ * pass. Throws {@link EngramAccessForbiddenError} (→ 403) when a scoped
3562
+ * token calls a non-permitted op (issue #1837).
3563
+ */
3564
+ private enforceTokenOp(op: OperationName): void {
3565
+ void getOperation(op); // preserve the registration side-effect assertion
3566
+ assertOperationAllowed(tokenCapabilityStore.getStore(), op);
3567
+ }
3568
+
3569
+ /**
3570
+ * Operator/admin routes are unrestricted-only surfaces: a scoped
3571
+ * (least-privileged) token must NOT reach them (issue #1837). Legacy and
3572
+ * explicit-unrestricted tokens pass. Throws EngramAccessForbiddenError (→403).
3573
+ */
3574
+ private requireOperatorToken(): void {
3575
+ if (isCapabilityRestricted(tokenCapabilityStore.getStore())) {
3576
+ throw new EngramAccessForbiddenError(
3577
+ "token is scoped and may not access operator/admin routes",
3578
+ );
3579
+ }
3354
3580
  }
3355
3581
 
3356
3582
  private timingSafeStringEqual(a: string, b: string): boolean {