@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.
- package/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +28 -28
- package/dist/access-boundary.d.ts +27 -8
- package/dist/access-boundary.js +33 -29
- package/dist/access-cli.js +83 -81
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +52 -6
- package/dist/access-http.js +40 -38
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +28 -28
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +28 -28
- package/dist/access-mcp.d.ts +16 -6
- package/dist/access-mcp.js +37 -35
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +28 -28
- package/dist/access-operations-batch.js +33 -32
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +28 -28
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +174 -0
- package/dist/access-token-capabilities.js +33 -0
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +9 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
- package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
- package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
- package/dist/chunk-4GNEDXDI.js.map +1 -0
- package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
- package/dist/chunk-4OKLXWDZ.js.map +1 -0
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
- package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
- package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
- package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
- package/dist/chunk-B3ABVQ36.js.map +1 -0
- package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
- package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/chunk-D7FOKIY5.js +173 -0
- package/dist/chunk-D7FOKIY5.js.map +1 -0
- package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
- package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
- package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
- package/dist/chunk-IC2TCZJ2.js +54 -0
- package/dist/chunk-IC2TCZJ2.js.map +1 -0
- package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
- package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
- package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
- package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
- package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
- package/dist/chunk-LADFWYHY.js.map +1 -0
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
- package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
- package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
- package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
- package/dist/chunk-PCLIWBGD.js.map +1 -0
- package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
- package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
- package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
- package/dist/chunk-UFUKHGZD.js.map +1 -0
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
- package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
- package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
- package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +61 -59
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +9 -9
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +9 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +271 -17
- package/dist/index.js +163 -99
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/mcp-read-only-tools.d.ts +16 -0
- package/dist/mcp-read-only-tools.js +8 -0
- package/dist/mcp-read-only-tools.js.map +1 -0
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +19 -19
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +25 -25
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +85 -83
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/document-scanner.js +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -3
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +9 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage-C1maCNbc.d.ts +1591 -0
- package/dist/storage.d.ts +5 -1239
- package/dist/storage.js +8 -8
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
- package/dist/tokens.d.ts +17 -2
- package/dist/tokens.js +3 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-boundary.test.ts +62 -0
- package/src/access-boundary.ts +165 -126
- package/src/access-http.test.ts +781 -0
- package/src/access-http.ts +334 -108
- package/src/access-mcp.test.ts +264 -0
- package/src/access-mcp.ts +45 -70
- package/src/access-operations-batch.ts +35 -12
- package/src/access-surface-catalog.test.ts +22 -14
- package/src/access-surface-catalog.ts +6 -3
- package/src/access-token-capabilities.test.ts +844 -0
- package/src/access-token-capabilities.ts +391 -0
- package/src/chat/chat-mcp.test.ts +232 -0
- package/src/chat/chat-session.ts +34 -0
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +14 -0
- package/src/mcp-read-only-tools.ts +61 -0
- package/src/storage.ts +16 -0
- package/src/tokens.ts +32 -3
- package/src/wearables/cli.test.ts +102 -0
- package/src/wearables/cli.ts +65 -0
- package/src/wearables/config.test.ts +32 -0
- package/src/wearables/config.ts +52 -0
- package/src/wearables/day-store.test.ts +246 -0
- package/src/wearables/day-store.ts +272 -3
- package/src/wearables/fusion/cluster.ts +230 -0
- package/src/wearables/fusion/fuse.ts +218 -0
- package/src/wearables/fusion/fusion.test.ts +2575 -0
- package/src/wearables/fusion/index.ts +50 -0
- package/src/wearables/fusion/reconcile.ts +831 -0
- package/src/wearables/fusion/reconstruct.ts +251 -0
- package/src/wearables/fusion/store.ts +480 -0
- package/src/wearables/fusion/types.ts +228 -0
- package/src/wearables/index.ts +41 -0
- package/src/wearables/service.test.ts +1030 -2
- package/src/wearables/service.ts +258 -16
- package/src/wearables/speakers.test.ts +128 -0
- package/src/wearables/speakers.ts +58 -4
- package/src/wearables/storage-io.test.ts +146 -0
- package/src/wearables/types.ts +31 -0
- package/dist/chunk-2FOD2YY3.js.map +0 -1
- package/dist/chunk-7QWMTOIF.js.map +0 -1
- package/dist/chunk-DR6PEJU3.js.map +0 -1
- package/dist/chunk-E2SPGGUI.js.map +0 -1
- package/dist/chunk-FZGGZAAU.js.map +0 -1
- package/dist/chunk-ILFJ7AVS.js.map +0 -1
- package/dist/chunk-M7GGMGMZ.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
- /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
- /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
- /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
- /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
- /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
- /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
package/src/access-http.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3498
|
+
if (!candidate) return null;
|
|
3323
3499
|
const token = candidate;
|
|
3324
|
-
// Check primary token
|
|
3325
|
-
if (this.authToken && this.timingSafeStringEqual(token, this.authToken))
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3530
|
+
if (this.timingSafeStringEqual(token, valid)) return { token: valid };
|
|
3351
3531
|
}
|
|
3352
3532
|
}
|
|
3353
|
-
return
|
|
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 {
|