@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.test.ts
CHANGED
|
@@ -7,8 +7,11 @@ import { gzipSync } from "node:zlib";
|
|
|
7
7
|
|
|
8
8
|
import { EngramAccessHttpServer, type RemnicAdminControls, type RemnicAdminDashboardStatus } from "./access-http.js";
|
|
9
9
|
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
10
|
+
import { tokenCapabilityStore, type TokenCapabilities } from "./access-token-capabilities.js";
|
|
10
11
|
import { parseConfig } from "./config.js";
|
|
11
12
|
import { readPair, writePair } from "./contradiction/contradiction-review.js";
|
|
13
|
+
import { createChatSession } from "./chat/chat-session.js";
|
|
14
|
+
import { DEFAULT_CHAT_CONFIG } from "./chat/chat-config.js";
|
|
12
15
|
import { projectTagProjectId } from "./coding/coding-namespace.js";
|
|
13
16
|
import { OFFLINE_SYNC_MAX_MTIME_MS } from "./offline-sync.js";
|
|
14
17
|
import type { StorageManager } from "./storage.js";
|
|
@@ -1804,3 +1807,781 @@ test("HTTP authTokenEntriesGetter is authoritative: scope policy binds connector
|
|
|
1804
1807
|
await server.stop();
|
|
1805
1808
|
}
|
|
1806
1809
|
});
|
|
1810
|
+
|
|
1811
|
+
test("HTTP scoped-token namespace allow-list is enforced on every namespace route (issue #1837 bypass fix)", async () => {
|
|
1812
|
+
// A scoped token may touch ONLY ns_a. The server default tenant is "default"
|
|
1813
|
+
// (≠ ns_a), so OMITTING ?namespace= must NOT silently fall through to the
|
|
1814
|
+
// default tenant — the EFFECTIVE namespace (explicit OR defaulted) must be a
|
|
1815
|
+
// member of the token's allow-list on every namespace-scoped route. Fail
|
|
1816
|
+
// closed everywhere; the previous behavior let a scoped bearer drop the
|
|
1817
|
+
// param and reach the server default tenant (memory_list / memory_get).
|
|
1818
|
+
const browseCalls: { namespace?: string }[] = [];
|
|
1819
|
+
const getCalls: { memoryId: string; namespace: string | undefined }[] = [];
|
|
1820
|
+
const service = {
|
|
1821
|
+
configRef: parseConfig({
|
|
1822
|
+
memoryDir: "/tmp/remnic-http-scoped-namespace-allow-list",
|
|
1823
|
+
namespacesEnabled: true,
|
|
1824
|
+
defaultNamespace: "default",
|
|
1825
|
+
}),
|
|
1826
|
+
memoryBrowse: async (request: { namespace?: string }) => {
|
|
1827
|
+
browseCalls.push(request);
|
|
1828
|
+
return { total: 0, memories: [] };
|
|
1829
|
+
},
|
|
1830
|
+
memoryGet: async (memoryId: string, namespace: string | undefined) => {
|
|
1831
|
+
getCalls.push({ memoryId, namespace });
|
|
1832
|
+
return { found: true };
|
|
1833
|
+
},
|
|
1834
|
+
peerList: async () => ({ peers: [] }),
|
|
1835
|
+
} as unknown as EngramAccessService;
|
|
1836
|
+
const server = new EngramAccessHttpServer({
|
|
1837
|
+
service,
|
|
1838
|
+
port: 0,
|
|
1839
|
+
// Entry-based tokens with a capabilities record. The scoped token carries
|
|
1840
|
+
// a namespaces allow-list; the operator token is explicit-unrestricted.
|
|
1841
|
+
authTokenEntriesGetter: () => [
|
|
1842
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
|
|
1843
|
+
{ token: "operator", capabilities: { version: 1 } },
|
|
1844
|
+
],
|
|
1845
|
+
adminConsoleEnabled: false,
|
|
1846
|
+
});
|
|
1847
|
+
const status = await server.start();
|
|
1848
|
+
const request = (token: string, urlPath: string) =>
|
|
1849
|
+
fetch(`http://127.0.0.1:${status.port}${urlPath}`, {
|
|
1850
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1851
|
+
});
|
|
1852
|
+
try {
|
|
1853
|
+
// ── memory_list: the named bypass route ──
|
|
1854
|
+
assert.equal(
|
|
1855
|
+
(await request("scoped-ns-a", "/engram/v1/memories")).status,
|
|
1856
|
+
403,
|
|
1857
|
+
"scoped: omitting namespace must be rejected — server default is not in the allow-list",
|
|
1858
|
+
);
|
|
1859
|
+
assert.equal(
|
|
1860
|
+
(await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_b")).status,
|
|
1861
|
+
403,
|
|
1862
|
+
"scoped: an unlisted namespace must be rejected",
|
|
1863
|
+
);
|
|
1864
|
+
const okList = await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_a");
|
|
1865
|
+
assert.equal(okList.status, 200, "scoped: an allowed namespace must succeed");
|
|
1866
|
+
assert.equal(browseCalls.at(-1)?.namespace, "ns_a", "scoped: the allowed namespace is forwarded to the service");
|
|
1867
|
+
|
|
1868
|
+
// ── memory_get: the other named bypass route (dispatches via the op registry) ──
|
|
1869
|
+
assert.equal(
|
|
1870
|
+
(await request("scoped-ns-a", "/engram/v1/memories/m_1")).status,
|
|
1871
|
+
403,
|
|
1872
|
+
"scoped memory_get: omitting namespace must be rejected",
|
|
1873
|
+
);
|
|
1874
|
+
assert.equal(
|
|
1875
|
+
(await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_b")).status,
|
|
1876
|
+
403,
|
|
1877
|
+
"scoped memory_get: an unlisted namespace must be rejected",
|
|
1878
|
+
);
|
|
1879
|
+
const okGet = await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_a");
|
|
1880
|
+
assert.equal(okGet.status, 200, "scoped memory_get: an allowed namespace must succeed");
|
|
1881
|
+
assert.deepEqual(
|
|
1882
|
+
getCalls.at(-1),
|
|
1883
|
+
{ memoryId: "m_1", namespace: "ns_a" },
|
|
1884
|
+
"scoped memory_get: the allowed namespace reaches the service",
|
|
1885
|
+
);
|
|
1886
|
+
|
|
1887
|
+
// ── a non-namespace-scoped route is unaffected by namespace scoping ──
|
|
1888
|
+
assert.equal(
|
|
1889
|
+
(await request("scoped-ns-a", "/engram/v1/peers")).status,
|
|
1890
|
+
200,
|
|
1891
|
+
"a non-namespace-scoped route must be unaffected",
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
// ── unrestricted token: omitting namespace still reaches the default tenant ──
|
|
1895
|
+
assert.equal(
|
|
1896
|
+
(await request("operator", "/engram/v1/memories")).status,
|
|
1897
|
+
200,
|
|
1898
|
+
"unrestricted: omitting namespace must still reach the default tenant",
|
|
1899
|
+
);
|
|
1900
|
+
} finally {
|
|
1901
|
+
await server.stop();
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
test("HTTP scoped-token namespace allow-list gates id-loaded contradiction routes (issue #1850 round 2)", async () => {
|
|
1906
|
+
// The contradiction detail GET and review/resolve routes load the pair BY
|
|
1907
|
+
// ID — its namespace comes from the record, NOT a ?namespace= query param
|
|
1908
|
+
// that resolveNamespace() already gates. A namespace-scoped bearer that
|
|
1909
|
+
// knows a pair id in a namespace outside its allow-list must NOT be able to
|
|
1910
|
+
// read (GET) or mutate (resolve) it. Fail closed (403) everywhere; the pair
|
|
1911
|
+
// in the allowed namespace is reachable.
|
|
1912
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-http-scoped-contradiction-"));
|
|
1913
|
+
const allowedPair = writePair(dir, {
|
|
1914
|
+
namespace: "ns_a",
|
|
1915
|
+
memoryIds: ["a-1", "a-2"],
|
|
1916
|
+
verdict: "contradicts",
|
|
1917
|
+
rationale: "pair in the allowed namespace",
|
|
1918
|
+
confidence: 0.9,
|
|
1919
|
+
detectedAt: new Date().toISOString(),
|
|
1920
|
+
});
|
|
1921
|
+
const deniedPair = writePair(dir, {
|
|
1922
|
+
namespace: "ns_b",
|
|
1923
|
+
memoryIds: ["b-1", "b-2"],
|
|
1924
|
+
verdict: "contradicts",
|
|
1925
|
+
rationale: "pair in a denied namespace",
|
|
1926
|
+
confidence: 0.9,
|
|
1927
|
+
detectedAt: new Date().toISOString(),
|
|
1928
|
+
});
|
|
1929
|
+
const storage = { dir } as unknown as StorageManager;
|
|
1930
|
+
const service = {
|
|
1931
|
+
configRef: parseConfig({
|
|
1932
|
+
memoryDir: dir,
|
|
1933
|
+
namespacesEnabled: true,
|
|
1934
|
+
defaultNamespace: "default",
|
|
1935
|
+
}),
|
|
1936
|
+
memoryDir: dir,
|
|
1937
|
+
storageRef: storage,
|
|
1938
|
+
getReadableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
1939
|
+
namespace: namespace ?? "default",
|
|
1940
|
+
storage,
|
|
1941
|
+
}),
|
|
1942
|
+
getWritableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
1943
|
+
namespace: namespace ?? "default",
|
|
1944
|
+
storage,
|
|
1945
|
+
}),
|
|
1946
|
+
} as unknown as EngramAccessService;
|
|
1947
|
+
const server = new EngramAccessHttpServer({
|
|
1948
|
+
service,
|
|
1949
|
+
port: 0,
|
|
1950
|
+
authTokenEntriesGetter: () => [
|
|
1951
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
|
|
1952
|
+
{ token: "operator", capabilities: { version: 1 } },
|
|
1953
|
+
],
|
|
1954
|
+
adminConsoleEnabled: false,
|
|
1955
|
+
});
|
|
1956
|
+
const status = await server.start();
|
|
1957
|
+
const getDetail = (token: string, pairId: string) =>
|
|
1958
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/review/contradictions/${pairId}`, {
|
|
1959
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1960
|
+
});
|
|
1961
|
+
const resolve = (token: string, pairId: string, verb: string) =>
|
|
1962
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/review/resolve`, {
|
|
1963
|
+
method: "POST",
|
|
1964
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1965
|
+
body: JSON.stringify({ pairId, verb }),
|
|
1966
|
+
});
|
|
1967
|
+
try {
|
|
1968
|
+
// ── GET detail: scoped token, denied namespace → 403 (fail closed, no leak) ──
|
|
1969
|
+
assert.equal(
|
|
1970
|
+
(await getDetail("scoped-ns-a", deniedPair.pairId)).status,
|
|
1971
|
+
403,
|
|
1972
|
+
"scoped GET: a pair in an unlisted namespace must be denied (403), not leaked",
|
|
1973
|
+
);
|
|
1974
|
+
// ── GET detail: scoped token, allowed namespace → 200 ──
|
|
1975
|
+
assert.equal(
|
|
1976
|
+
(await getDetail("scoped-ns-a", allowedPair.pairId)).status,
|
|
1977
|
+
200,
|
|
1978
|
+
"scoped GET: a pair in the allowed namespace must succeed",
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
// ── review/resolve: scoped token, denied namespace → 403 before any mutation ──
|
|
1982
|
+
const deniedResolve = await resolve("scoped-ns-a", deniedPair.pairId, "both-valid");
|
|
1983
|
+
assert.equal(
|
|
1984
|
+
deniedResolve.status,
|
|
1985
|
+
403,
|
|
1986
|
+
"scoped resolve: a pair in an unlisted namespace must be denied (403) before any mutation",
|
|
1987
|
+
);
|
|
1988
|
+
// The denied pair must NOT have been mutated (no resolution leak).
|
|
1989
|
+
assert.notEqual(
|
|
1990
|
+
readPair(dir, deniedPair.pairId)?.resolution,
|
|
1991
|
+
"both-valid",
|
|
1992
|
+
"scoped resolve: the denied pair must remain unresolved",
|
|
1993
|
+
);
|
|
1994
|
+
|
|
1995
|
+
// ── review/resolve: scoped token, allowed namespace → succeeds (not 403) ──
|
|
1996
|
+
const allowedResolve = await resolve("scoped-ns-a", allowedPair.pairId, "both-valid");
|
|
1997
|
+
assert.equal(
|
|
1998
|
+
allowedResolve.status,
|
|
1999
|
+
200,
|
|
2000
|
+
"scoped resolve: a pair in the allowed namespace must succeed",
|
|
2001
|
+
);
|
|
2002
|
+
assert.equal(
|
|
2003
|
+
readPair(dir, allowedPair.pairId)?.resolution,
|
|
2004
|
+
"both-valid",
|
|
2005
|
+
"scoped resolve: the allowed pair is marked resolved",
|
|
2006
|
+
);
|
|
2007
|
+
|
|
2008
|
+
// ── unrestricted operator token: the denied-namespace pair is reachable ──
|
|
2009
|
+
assert.equal(
|
|
2010
|
+
(await getDetail("operator", deniedPair.pairId)).status,
|
|
2011
|
+
200,
|
|
2012
|
+
"unrestricted GET: the denied-namespace pair is reachable for an unrestricted token",
|
|
2013
|
+
);
|
|
2014
|
+
} finally {
|
|
2015
|
+
await server.stop();
|
|
2016
|
+
await rm(dir, { recursive: true, force: true });
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
// ===========================================================================
|
|
2021
|
+
// Issue #1850 round 4 — uniform effective-namespace chokepoint coverage.
|
|
2022
|
+
// One helper (enforceNamespaceAllowList) gates every surface; these tests
|
|
2023
|
+
// prove the gate fires on the surfaces round 3 missed: MCP tools/call, HTTP
|
|
2024
|
+
// body-namespace (coding) routes, and the legacy undefined-namespace pair.
|
|
2025
|
+
// ===========================================================================
|
|
2026
|
+
|
|
2027
|
+
test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 finding 1)", async () => {
|
|
2028
|
+
// MCP `tools/call` previously never applied the namespace allow-list, so a
|
|
2029
|
+
// scoped bearer could reach another tenant via tool args. capsule_list is a
|
|
2030
|
+
// namespace-scoped MCP tool (accepts a `namespace` argument), so the gate
|
|
2031
|
+
// must fire on it. The 403 is thrown BEFORE op.run, so the denied case never
|
|
2032
|
+
// reaches the service.
|
|
2033
|
+
const capsuleCalls: Array<Record<string, unknown>> = [];
|
|
2034
|
+
const service = {
|
|
2035
|
+
configRef: parseConfig({ memoryDir: "/tmp/remnic-mcp-ns", namespacesEnabled: true, defaultNamespace: "default" }),
|
|
2036
|
+
capsuleList: async (args: Record<string, unknown>) => {
|
|
2037
|
+
capsuleCalls.push(args);
|
|
2038
|
+
return { capsules: [] };
|
|
2039
|
+
},
|
|
2040
|
+
} as unknown as EngramAccessService;
|
|
2041
|
+
const server = new EngramAccessHttpServer({
|
|
2042
|
+
service,
|
|
2043
|
+
port: 0,
|
|
2044
|
+
authTokenEntriesGetter: () => [
|
|
2045
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
|
|
2046
|
+
{ token: "operator", capabilities: { version: 1 } },
|
|
2047
|
+
],
|
|
2048
|
+
adminConsoleEnabled: false,
|
|
2049
|
+
});
|
|
2050
|
+
const status = await server.start();
|
|
2051
|
+
const mcpCall = (token: string, namespace: string) =>
|
|
2052
|
+
fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
2053
|
+
method: "POST",
|
|
2054
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
2055
|
+
body: JSON.stringify({
|
|
2056
|
+
jsonrpc: "2.0",
|
|
2057
|
+
id: 1,
|
|
2058
|
+
method: "tools/call",
|
|
2059
|
+
params: { name: "engram.capsule_list", arguments: { namespace } },
|
|
2060
|
+
}),
|
|
2061
|
+
});
|
|
2062
|
+
try {
|
|
2063
|
+
// scoped: an unlisted namespace is rejected with 403 BEFORE op.run.
|
|
2064
|
+
assert.equal(
|
|
2065
|
+
(await mcpCall("scoped-ns-a", "ns_b")).status,
|
|
2066
|
+
403,
|
|
2067
|
+
"scoped MCP: an unlisted namespace must be denied (403) before op.run",
|
|
2068
|
+
);
|
|
2069
|
+
assert.equal(capsuleCalls.length, 0, "scoped MCP: the service must NOT be reached for a denied namespace");
|
|
2070
|
+
// scoped: the allowed namespace proceeds past the gate (200, not 403).
|
|
2071
|
+
const allowed = await mcpCall("scoped-ns-a", "ns_a");
|
|
2072
|
+
assert.notEqual(allowed.status, 403, "scoped MCP: the allowed namespace must proceed past the gate");
|
|
2073
|
+
assert.equal(capsuleCalls.at(-1)?.namespace, "ns_a", "scoped MCP: the allowed namespace reaches the service");
|
|
2074
|
+
// unrestricted: an unlisted namespace is a no-op (proceeds, not 403).
|
|
2075
|
+
const unrestricted = await mcpCall("operator", "ns_b");
|
|
2076
|
+
assert.notEqual(unrestricted.status, 403, "unrestricted MCP: namespace scoping must be a no-op");
|
|
2077
|
+
} finally {
|
|
2078
|
+
await server.stop();
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
test("HTTP coding POST routes gate the body namespace through the allow-list (issue #1850 finding 2)", async () => {
|
|
2083
|
+
// coding_decision passes `body` straight to op.run; a scoped bearer setting
|
|
2084
|
+
// body.namespace to another tenant must be rejected. The gate fires on the
|
|
2085
|
+
// body namespace field via the same resolveNamespace chokepoint as query
|
|
2086
|
+
// routes. 403 before op.run; the allowed namespace reaches the service.
|
|
2087
|
+
const decisionCalls: Array<Record<string, unknown>> = [];
|
|
2088
|
+
const service = {
|
|
2089
|
+
configRef: parseConfig({ memoryDir: "/tmp/remnic-coding-ns", namespacesEnabled: true, defaultNamespace: "default" }),
|
|
2090
|
+
codingDecision: async (input: Record<string, unknown>) => {
|
|
2091
|
+
decisionCalls.push(input);
|
|
2092
|
+
return { subcommand: "list", records: [], count: 0 };
|
|
2093
|
+
},
|
|
2094
|
+
} as unknown as EngramAccessService;
|
|
2095
|
+
const server = new EngramAccessHttpServer({
|
|
2096
|
+
service,
|
|
2097
|
+
port: 0,
|
|
2098
|
+
authTokenEntriesGetter: () => [
|
|
2099
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
|
|
2100
|
+
{ token: "operator", capabilities: { version: 1 } },
|
|
2101
|
+
],
|
|
2102
|
+
adminConsoleEnabled: false,
|
|
2103
|
+
});
|
|
2104
|
+
const status = await server.start();
|
|
2105
|
+
const postDecision = (token: string, namespace: string | undefined) =>
|
|
2106
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/coding/decisions`, {
|
|
2107
|
+
method: "POST",
|
|
2108
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
2109
|
+
body: JSON.stringify({ subcommand: "list", ...(namespace ? { namespace } : {}) }),
|
|
2110
|
+
});
|
|
2111
|
+
try {
|
|
2112
|
+
// scoped: body.namespace to an unlisted tenant → 403 before op.run.
|
|
2113
|
+
assert.equal(
|
|
2114
|
+
(await postDecision("scoped-ns-a", "ns_b")).status,
|
|
2115
|
+
403,
|
|
2116
|
+
"scoped coding: body.namespace to an unlisted namespace must be denied (403)",
|
|
2117
|
+
);
|
|
2118
|
+
assert.equal(decisionCalls.length, 0, "scoped coding: the service must NOT be reached for a denied namespace");
|
|
2119
|
+
// scoped: the allowed body.namespace reaches the service.
|
|
2120
|
+
const allowed = await postDecision("scoped-ns-a", "ns_a");
|
|
2121
|
+
assert.notEqual(allowed.status, 403, "scoped coding: the allowed namespace must proceed past the gate");
|
|
2122
|
+
assert.equal(decisionCalls.at(-1)?.namespace, "ns_a", "scoped coding: the allowed namespace reaches the service");
|
|
2123
|
+
// unrestricted: any body.namespace is a no-op.
|
|
2124
|
+
assert.notEqual(
|
|
2125
|
+
(await postDecision("operator", "ns_b")).status,
|
|
2126
|
+
403,
|
|
2127
|
+
"unrestricted coding: namespace scoping must be a no-op",
|
|
2128
|
+
);
|
|
2129
|
+
} finally {
|
|
2130
|
+
await server.stop();
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
test("Legacy undefined-namespace contradiction pair: default-allow-list token reads+resolves; non-default denied (issue #1850 findings 3+4)", async () => {
|
|
2135
|
+
// A legacy pair carries namespace:undefined, which downstream storage maps to
|
|
2136
|
+
// the server DEFAULT. Round 3's assertNamespaceAllowed(caps, undefined)
|
|
2137
|
+
// wrongly denied a scoped token whose allow-list INCLUDES the default. The
|
|
2138
|
+
// effective-namespace chokepoint maps undefined → default, so:
|
|
2139
|
+
// - namespaces:['default'] (== server default) ⇒ ALLOWED read + resolve;
|
|
2140
|
+
// - namespaces:['ns_a'] (not default) ⇒ DENIED.
|
|
2141
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-http-scoped-legacy-pair-"));
|
|
2142
|
+
const legacyPair = writePair(dir, {
|
|
2143
|
+
memoryIds: ["legacy-x", "legacy-y"],
|
|
2144
|
+
verdict: "contradicts",
|
|
2145
|
+
rationale: "legacy pair with no namespace",
|
|
2146
|
+
confidence: 0.9,
|
|
2147
|
+
detectedAt: new Date().toISOString(),
|
|
2148
|
+
});
|
|
2149
|
+
// Sanity: the fixture really is a legacy unscoped pair.
|
|
2150
|
+
assert.equal(readPair(dir, legacyPair.pairId)?.namespace, undefined);
|
|
2151
|
+
const storage = { dir } as unknown as StorageManager;
|
|
2152
|
+
const baseService = {
|
|
2153
|
+
configRef: parseConfig({ memoryDir: dir, namespacesEnabled: true, defaultNamespace: "default" }),
|
|
2154
|
+
memoryDir: dir,
|
|
2155
|
+
storageRef: storage,
|
|
2156
|
+
getReadableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
2157
|
+
namespace: namespace ?? "default",
|
|
2158
|
+
storage,
|
|
2159
|
+
}),
|
|
2160
|
+
getWritableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
2161
|
+
namespace: namespace ?? "default",
|
|
2162
|
+
storage,
|
|
2163
|
+
}),
|
|
2164
|
+
};
|
|
2165
|
+
const mkServer = (scopedToken: string, allowList: string[]) => {
|
|
2166
|
+
const service = { ...baseService } as unknown as EngramAccessService;
|
|
2167
|
+
return new EngramAccessHttpServer({
|
|
2168
|
+
service,
|
|
2169
|
+
port: 0,
|
|
2170
|
+
authTokenEntriesGetter: () => [{ token: scopedToken, capabilities: { version: 1, namespaces: allowList } }],
|
|
2171
|
+
adminConsoleEnabled: false,
|
|
2172
|
+
});
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
// ── default-allow-list token: legacy pair is reachable + resolvable ──
|
|
2176
|
+
const defaultServer = mkServer("allow-default", ["default"]);
|
|
2177
|
+
const defaultStatus = await defaultServer.start();
|
|
2178
|
+
try {
|
|
2179
|
+
const detail = await fetch(
|
|
2180
|
+
`http://127.0.0.1:${defaultStatus.port}/engram/v1/review/contradictions/${legacyPair.pairId}`,
|
|
2181
|
+
{ headers: { authorization: "Bearer allow-default" } },
|
|
2182
|
+
);
|
|
2183
|
+
assert.equal(detail.status, 200, "default-allow token: legacy undefined pair must be readable (undefined→default)");
|
|
2184
|
+
const resolve = await fetch(`http://127.0.0.1:${defaultStatus.port}/engram/v1/review/resolve`, {
|
|
2185
|
+
method: "POST",
|
|
2186
|
+
headers: { authorization: "Bearer allow-default", "content-type": "application/json" },
|
|
2187
|
+
body: JSON.stringify({ pairId: legacyPair.pairId, verb: "both-valid" }),
|
|
2188
|
+
});
|
|
2189
|
+
assert.equal(resolve.status, 200, "default-allow token: legacy undefined pair must be resolvable");
|
|
2190
|
+
assert.equal(
|
|
2191
|
+
readPair(dir, legacyPair.pairId)?.resolution,
|
|
2192
|
+
"both-valid",
|
|
2193
|
+
"default-allow token: the legacy pair is marked resolved",
|
|
2194
|
+
);
|
|
2195
|
+
} finally {
|
|
2196
|
+
await defaultServer.stop();
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// A fresh legacy pair for the deny case — independent of the pair the
|
|
2200
|
+
// default-allow token already resolved above, so "must remain unresolved"
|
|
2201
|
+
// is a clean assertion.
|
|
2202
|
+
const deniedLegacyPair = writePair(dir, {
|
|
2203
|
+
memoryIds: ["legacy-deny-x", "legacy-deny-y"],
|
|
2204
|
+
verdict: "contradicts",
|
|
2205
|
+
rationale: "legacy pair for the deny case",
|
|
2206
|
+
confidence: 0.9,
|
|
2207
|
+
detectedAt: new Date().toISOString(),
|
|
2208
|
+
});
|
|
2209
|
+
assert.equal(readPair(dir, deniedLegacyPair.pairId)?.namespace, undefined);
|
|
2210
|
+
|
|
2211
|
+
// ── non-default-allow-list token: legacy pair is DENIED (undefined→default not listed) ──
|
|
2212
|
+
const deniedServer = mkServer("allow-ns-a", ["ns_a"]);
|
|
2213
|
+
const deniedStatus = await deniedServer.start();
|
|
2214
|
+
try {
|
|
2215
|
+
const detail = await fetch(
|
|
2216
|
+
`http://127.0.0.1:${deniedStatus.port}/engram/v1/review/contradictions/${deniedLegacyPair.pairId}`,
|
|
2217
|
+
{ headers: { authorization: "Bearer allow-ns-a" } },
|
|
2218
|
+
);
|
|
2219
|
+
assert.equal(
|
|
2220
|
+
detail.status,
|
|
2221
|
+
403,
|
|
2222
|
+
"non-default token: legacy undefined pair must be denied (undefined→default not in allow-list)",
|
|
2223
|
+
);
|
|
2224
|
+
const resolve = await fetch(`http://127.0.0.1:${deniedStatus.port}/engram/v1/review/resolve`, {
|
|
2225
|
+
method: "POST",
|
|
2226
|
+
headers: { authorization: "Bearer allow-ns-a", "content-type": "application/json" },
|
|
2227
|
+
body: JSON.stringify({ pairId: deniedLegacyPair.pairId, verb: "both-valid" }),
|
|
2228
|
+
});
|
|
2229
|
+
assert.equal(resolve.status, 403, "non-default token: legacy undefined pair must not be resolvable");
|
|
2230
|
+
assert.notEqual(
|
|
2231
|
+
readPair(dir, deniedLegacyPair.pairId)?.resolution,
|
|
2232
|
+
"both-valid",
|
|
2233
|
+
"non-default token: the legacy pair must remain unresolved",
|
|
2234
|
+
);
|
|
2235
|
+
} finally {
|
|
2236
|
+
await deniedServer.stop();
|
|
2237
|
+
await rm(dir, { recursive: true, force: true });
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2242
|
+
// Issue #1850 round 5: comprehensive access-surface coverage
|
|
2243
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
2244
|
+
|
|
2245
|
+
test("HTTP adapters route is op-gated: deny-all → 403, unrestricted → 200 (issue #1850 finding 1)", async () => {
|
|
2246
|
+
// The adapters route serves adapter metadata after auth but previously had
|
|
2247
|
+
// NO enforceTokenOp, so a deny-all / narrow-ops token reached it. It now
|
|
2248
|
+
// dispatches through the adapters_status op; a scoped token is rejected.
|
|
2249
|
+
const service = {
|
|
2250
|
+
configRef: parseConfig({ memoryDir: "/tmp/remnic-http-adapters-opgate", defaultNamespace: "default" }),
|
|
2251
|
+
} as unknown as EngramAccessService;
|
|
2252
|
+
const server = new EngramAccessHttpServer({
|
|
2253
|
+
service,
|
|
2254
|
+
port: 0,
|
|
2255
|
+
authTokenEntriesGetter: () => [
|
|
2256
|
+
{ token: "denyall", capabilities: { version: 1, ops: [] } },
|
|
2257
|
+
{ token: "narrow", capabilities: { version: 1, ops: ["memory_get"] } },
|
|
2258
|
+
{ token: "operator", capabilities: { version: 1 } },
|
|
2259
|
+
],
|
|
2260
|
+
adminConsoleEnabled: false,
|
|
2261
|
+
});
|
|
2262
|
+
const status = await server.start();
|
|
2263
|
+
const request = (token: string, urlPath: string) =>
|
|
2264
|
+
fetch(`http://127.0.0.1:${status.port}${urlPath}`, {
|
|
2265
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2266
|
+
});
|
|
2267
|
+
try {
|
|
2268
|
+
assert.equal(
|
|
2269
|
+
(await request("denyall", "/engram/v1/adapters")).status,
|
|
2270
|
+
403,
|
|
2271
|
+
"deny-all token must be rejected by the op gate",
|
|
2272
|
+
);
|
|
2273
|
+
assert.equal(
|
|
2274
|
+
(await request("narrow", "/engram/v1/adapters")).status,
|
|
2275
|
+
403,
|
|
2276
|
+
"narrow-ops token without adapters_status must be rejected",
|
|
2277
|
+
);
|
|
2278
|
+
const ok = await request("operator", "/engram/v1/adapters");
|
|
2279
|
+
assert.equal(ok.status, 200, "unrestricted token must reach adapter metadata");
|
|
2280
|
+
const body = (await ok.json()) as { adaptersEnabled?: boolean };
|
|
2281
|
+
assert.equal(typeof body.adaptersEnabled, "boolean", "adapter metadata body shape preserved");
|
|
2282
|
+
} finally {
|
|
2283
|
+
await server.stop();
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
test("HTTP chat routes gate the resumed session's STORED namespace (issue #1850 finding 2)", async () => {
|
|
2288
|
+
// A chat session is an id-loaded record: its namespace comes from the stored
|
|
2289
|
+
// session header, NOT a ?namespace= query param. A namespace-scoped bearer
|
|
2290
|
+
// that knows a chatSessionId must not post to / stream a session in a
|
|
2291
|
+
// namespace outside its allow-list. The gate runs before delegation.
|
|
2292
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-chat-ns-gate-"));
|
|
2293
|
+
// Session bound to ns_a (no principal ⇒ accessible to anyone per
|
|
2294
|
+
// sessionBelongsToPrincipal, so the namespace gate is the ONLY gate in play).
|
|
2295
|
+
const session = await createChatSession(dir, { namespace: "ns_a" });
|
|
2296
|
+
const service = {
|
|
2297
|
+
configRef: parseConfig({ memoryDir: dir, namespacesEnabled: true, defaultNamespace: "default" }),
|
|
2298
|
+
memoryDir: dir,
|
|
2299
|
+
} as unknown as EngramAccessService;
|
|
2300
|
+
const server = new EngramAccessHttpServer({
|
|
2301
|
+
service,
|
|
2302
|
+
port: 0,
|
|
2303
|
+
authTokenEntriesGetter: () => [
|
|
2304
|
+
{ token: "scoped-ns-b", capabilities: { version: 1, namespaces: ["ns_b"] } },
|
|
2305
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, namespaces: ["ns_a"] } },
|
|
2306
|
+
],
|
|
2307
|
+
adminConsoleEnabled: false,
|
|
2308
|
+
});
|
|
2309
|
+
const status = await server.start();
|
|
2310
|
+
const authed = (token: string) => ({ headers: { authorization: `Bearer ${token}` } });
|
|
2311
|
+
try {
|
|
2312
|
+
// ── POST chat/message resuming a CROSS-namespace session → 403 ──
|
|
2313
|
+
const cross = await fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
|
|
2314
|
+
method: "POST",
|
|
2315
|
+
...authed("scoped-ns-b"),
|
|
2316
|
+
headers: { ...authed("scoped-ns-b").headers, "content-type": "application/json" },
|
|
2317
|
+
body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
|
|
2318
|
+
});
|
|
2319
|
+
assert.equal(cross.status, 403, "scoped-ns-b must NOT post to a session bound to ns_a");
|
|
2320
|
+
|
|
2321
|
+
// ── POST chat/message resuming a SAME-namespace session → passes the gate ──
|
|
2322
|
+
// (Chat is unconfigured here so the handler returns chat_disabled 404; the
|
|
2323
|
+
// point is it is NOT 403 — the namespace gate let it through.)
|
|
2324
|
+
const same = await fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
|
|
2325
|
+
method: "POST",
|
|
2326
|
+
headers: { authorization: "Bearer scoped-ns-a", "content-type": "application/json" },
|
|
2327
|
+
body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
|
|
2328
|
+
});
|
|
2329
|
+
assert.notEqual(same.status, 403, "scoped-ns-a must pass the namespace gate for its own session");
|
|
2330
|
+
|
|
2331
|
+
// ── GET chat/events for a CROSS-namespace session → 403 ──
|
|
2332
|
+
const crossEvents = await fetch(
|
|
2333
|
+
`http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
|
|
2334
|
+
authed("scoped-ns-b"),
|
|
2335
|
+
);
|
|
2336
|
+
assert.equal(crossEvents.status, 403, "scoped-ns-b must NOT stream a session bound to ns_a");
|
|
2337
|
+
|
|
2338
|
+
// ── GET chat/events for a SAME-namespace session → passes the gate ──
|
|
2339
|
+
const sameEvents = await fetch(
|
|
2340
|
+
`http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
|
|
2341
|
+
authed("scoped-ns-a"),
|
|
2342
|
+
);
|
|
2343
|
+
assert.notEqual(sameEvents.status, 403, "scoped-ns-a must pass the namespace gate for its own session");
|
|
2344
|
+
} finally {
|
|
2345
|
+
await server.stop();
|
|
2346
|
+
await rm(dir, { recursive: true, force: true });
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
test("HTTP per-request token capabilities never bleed across concurrent async requests (issue #1850 r7 ALS run fix)", async () => {
|
|
2351
|
+
// The capability store must be bound via run() (NOT enterWith) so each
|
|
2352
|
+
// request's async scope observes its OWN resolved capabilities across every
|
|
2353
|
+
// await. Interleave concurrent requests with DIFFERENT scopes against an
|
|
2354
|
+
// op+namespace-gated route whose service read yields, then assert:
|
|
2355
|
+
// (1) a deny-all op token is ALWAYS rejected — never fail-opens to 200
|
|
2356
|
+
// because its caps leaked to undefined/unrestricted mid-handler;
|
|
2357
|
+
// (2) the store read AFTER an async yield inside the service still carries
|
|
2358
|
+
// the REQUESTING token's scope on every call — zero cross-request bleed.
|
|
2359
|
+
const observed: Array<TokenCapabilities | undefined> = [];
|
|
2360
|
+
const service = {
|
|
2361
|
+
configRef: parseConfig({
|
|
2362
|
+
memoryDir: "/tmp/remnic-http-als-concurrency",
|
|
2363
|
+
namespacesEnabled: true,
|
|
2364
|
+
defaultNamespace: "default",
|
|
2365
|
+
}),
|
|
2366
|
+
memoryBrowse: async () => {
|
|
2367
|
+
// Yield so concurrent requests overlap at this await — the exact window
|
|
2368
|
+
// where an enterWith-based store would read another request's caps (or
|
|
2369
|
+
// undefined). run() keeps each request in its own scope across the yield.
|
|
2370
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 8));
|
|
2371
|
+
observed.push(tokenCapabilityStore.getStore());
|
|
2372
|
+
return { total: 0, memories: [] };
|
|
2373
|
+
},
|
|
2374
|
+
memoryGet: async () => ({ found: true }),
|
|
2375
|
+
peerList: async () => ({ peers: [] }),
|
|
2376
|
+
} as unknown as EngramAccessService;
|
|
2377
|
+
const server = new EngramAccessHttpServer({
|
|
2378
|
+
service,
|
|
2379
|
+
port: 0,
|
|
2380
|
+
authTokenEntriesGetter: () => [
|
|
2381
|
+
{ token: "deny-all", capabilities: { version: 1, ops: [] } },
|
|
2382
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_a"] } },
|
|
2383
|
+
{ token: "scoped-ns-b", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_b"] } },
|
|
2384
|
+
],
|
|
2385
|
+
adminConsoleEnabled: false,
|
|
2386
|
+
});
|
|
2387
|
+
const status = await server.start();
|
|
2388
|
+
const get = (token: string, namespace: string) =>
|
|
2389
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`, {
|
|
2390
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2391
|
+
});
|
|
2392
|
+
try {
|
|
2393
|
+
const REPEATS = 15;
|
|
2394
|
+
const batch: Promise<Response>[] = [];
|
|
2395
|
+
for (let i = 0; i < REPEATS; i++) {
|
|
2396
|
+
batch.push(get("deny-all", "ns_a")); // op-gate must reject (ops: [])
|
|
2397
|
+
batch.push(get("scoped-ns-a", "ns_a")); // passes op + ns gate, reaches service
|
|
2398
|
+
batch.push(get("scoped-ns-b", "ns_b")); // passes op + ns gate, reaches service
|
|
2399
|
+
}
|
|
2400
|
+
const responses = await Promise.all(batch);
|
|
2401
|
+
|
|
2402
|
+
// (1) Op-gate holds under concurrency: every deny-all request is 403, never
|
|
2403
|
+
// fail-open 200 (which would mean its caps bled to unrestricted).
|
|
2404
|
+
const denyAllStatuses = responses.filter((_, i) => i % 3 === 0).map((r) => r.status);
|
|
2405
|
+
assert.ok(
|
|
2406
|
+
denyAllStatuses.every((s) => s === 403),
|
|
2407
|
+
`deny-all token must always be 403, got: ${JSON.stringify(denyAllStatuses)}`,
|
|
2408
|
+
);
|
|
2409
|
+
|
|
2410
|
+
// scoped tokens reach the service (200) — op + namespace gates both pass.
|
|
2411
|
+
const nsAStatuses = responses.filter((_, i) => i % 3 === 1).map((r) => r.status);
|
|
2412
|
+
const nsBStatuses = responses.filter((_, i) => i % 3 === 2).map((r) => r.status);
|
|
2413
|
+
assert.ok(nsAStatuses.every((s) => s === 200), `scoped-ns-a should all be 200: ${JSON.stringify(nsAStatuses)}`);
|
|
2414
|
+
assert.ok(nsBStatuses.every((s) => s === 200), `scoped-ns-b should all be 200: ${JSON.stringify(nsBStatuses)}`);
|
|
2415
|
+
|
|
2416
|
+
// (2) No cross-request bleed: the store read AFTER the async yield carried
|
|
2417
|
+
// the REQUESTING token's namespace on every service call. memoryBrowse
|
|
2418
|
+
// ran once per scoped request (2 * REPEATS).
|
|
2419
|
+
assert.equal(observed.length, 2 * REPEATS, "memoryBrowse called once per scoped request");
|
|
2420
|
+
const nsAObserved = observed.filter((c) => c?.namespaces?.includes("ns_a")).length;
|
|
2421
|
+
const nsBObserved = observed.filter((c) => c?.namespaces?.includes("ns_b")).length;
|
|
2422
|
+
assert.equal(nsAObserved, REPEATS, "every ns_a request observed its own scope — no bleed");
|
|
2423
|
+
assert.equal(nsBObserved, REPEATS, "every ns_b request observed its own scope — no bleed");
|
|
2424
|
+
} finally {
|
|
2425
|
+
await server.stop();
|
|
2426
|
+
}
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
test("HTTP allow-capability is never inherited by a concurrent deny token across the async yield (issue #1850 r8 ALS non-inheritance)", async () => {
|
|
2430
|
+
// The r7 run() fix guarantees each request observes its OWN caps. The sharper
|
|
2431
|
+
// property: a token whose caps ALLOW the op+namespace (→ 200) interleaved
|
|
2432
|
+
// with deny tokens (→ 403) must NEVER let a deny request inherit the allow
|
|
2433
|
+
// token's capabilities — i.e. in the SAME batch where allow requests returned
|
|
2434
|
+
// 200, NO deny request ever returned 200. Distinct (op, namespace) per token
|
|
2435
|
+
// so the result can only come from each request's own resolved caps.
|
|
2436
|
+
const observed: Array<TokenCapabilities | undefined> = [];
|
|
2437
|
+
const service = {
|
|
2438
|
+
configRef: parseConfig({
|
|
2439
|
+
memoryDir: "/tmp/remnic-http-als-noninherit",
|
|
2440
|
+
namespacesEnabled: true,
|
|
2441
|
+
defaultNamespace: "default",
|
|
2442
|
+
}),
|
|
2443
|
+
memoryBrowse: async () => {
|
|
2444
|
+
// Real wall-clock yield — the LEGITIMATE timer exception: this test
|
|
2445
|
+
// exercises ALS async-scope isolation across CONCURRENT requests, so the
|
|
2446
|
+
// await must produce a genuine overlap window where an enterWith-based
|
|
2447
|
+
// store would read another request's caps. Deterministic fake timers
|
|
2448
|
+
// cannot force that cross-request overlap (mirrors the r7 sibling test).
|
|
2449
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 8));
|
|
2450
|
+
observed.push(tokenCapabilityStore.getStore());
|
|
2451
|
+
return { total: 0, memories: [] };
|
|
2452
|
+
},
|
|
2453
|
+
memoryGet: async () => ({ found: true }),
|
|
2454
|
+
peerList: async () => ({ peers: [] }),
|
|
2455
|
+
} as unknown as EngramAccessService;
|
|
2456
|
+
const server = new EngramAccessHttpServer({
|
|
2457
|
+
service,
|
|
2458
|
+
port: 0,
|
|
2459
|
+
authTokenEntriesGetter: () => [
|
|
2460
|
+
// ALLOW: op + namespace both permitted → 200.
|
|
2461
|
+
{ token: "allow", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_allow"] } },
|
|
2462
|
+
// DENY (op-level): empty ops → 403 at the op-gate.
|
|
2463
|
+
{ token: "deny-all", capabilities: { version: 1, ops: [] } },
|
|
2464
|
+
// DENY (namespace-level): op permitted but scoped to a DIFFERENT
|
|
2465
|
+
// namespace ("ns_other") than the request ("ns_allow") → 403 at the
|
|
2466
|
+
// namespace-gate. The ONLY way this becomes 200 is by inheriting the
|
|
2467
|
+
// allow token's ns_allow cap — exactly the bleed run() prevents.
|
|
2468
|
+
{ token: "deny-ns", capabilities: { version: 1, ops: ["memory_list"], namespaces: ["ns_other"] } },
|
|
2469
|
+
],
|
|
2470
|
+
adminConsoleEnabled: false,
|
|
2471
|
+
});
|
|
2472
|
+
const status = await server.start();
|
|
2473
|
+
const get = (token: string, namespace: string) =>
|
|
2474
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`, {
|
|
2475
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2476
|
+
});
|
|
2477
|
+
try {
|
|
2478
|
+
const REPEATS = 15;
|
|
2479
|
+
const batch: Promise<Response>[] = [];
|
|
2480
|
+
for (let i = 0; i < REPEATS; i++) {
|
|
2481
|
+
batch.push(get("allow", "ns_allow")); // 3i → 200
|
|
2482
|
+
batch.push(get("deny-all", "ns_allow")); // 3i+1 → 403 (op-gate)
|
|
2483
|
+
batch.push(get("deny-ns", "ns_allow")); // 3i+2 → 403 (namespace-gate)
|
|
2484
|
+
}
|
|
2485
|
+
const responses = await Promise.all(batch);
|
|
2486
|
+
const allowStatuses = responses.filter((_, i) => i % 3 === 0).map((r) => r.status);
|
|
2487
|
+
const denyAllStatuses = responses.filter((_, i) => i % 3 === 1).map((r) => r.status);
|
|
2488
|
+
const denyNsStatuses = responses.filter((_, i) => i % 3 === 2).map((r) => r.status);
|
|
2489
|
+
|
|
2490
|
+
// ALLOW requests succeed (200) — the caps DID resolve correctly and the
|
|
2491
|
+
// service read happened under the allow scope.
|
|
2492
|
+
assert.ok(allowStatuses.every((s) => s === 200), `allow token should all be 200: ${JSON.stringify(allowStatuses)}`);
|
|
2493
|
+
|
|
2494
|
+
// NON-INHERITANCE: in the SAME interleaved batch where allow got 200, every
|
|
2495
|
+
// deny request stayed 403 — neither deny token ever inherited the allow
|
|
2496
|
+
// token's capabilities (which would have been 200). Covers BOTH op-level
|
|
2497
|
+
// and namespace-level denial.
|
|
2498
|
+
assert.ok(
|
|
2499
|
+
denyAllStatuses.every((s) => s === 403),
|
|
2500
|
+
`deny-all token must never be 200 (never inherits allow caps): ${JSON.stringify(denyAllStatuses)}`,
|
|
2501
|
+
);
|
|
2502
|
+
assert.ok(
|
|
2503
|
+
denyNsStatuses.every((s) => s === 403),
|
|
2504
|
+
`deny-ns token must never be 200 (never inherits allow namespace): ${JSON.stringify(denyNsStatuses)}`,
|
|
2505
|
+
);
|
|
2506
|
+
// Both deny arrays are ALL 403 ⟹ neither contains a 200: in the same
|
|
2507
|
+
// interleaved batch where the allow token returned 200, no deny request
|
|
2508
|
+
// ever inherited the allow token's capabilities.
|
|
2509
|
+
|
|
2510
|
+
// Only allow requests reached the service; the store read after the async
|
|
2511
|
+
// yield carried the allow token's namespace every time — zero deny bleed.
|
|
2512
|
+
assert.equal(observed.length, REPEATS, "only allow requests reached the service");
|
|
2513
|
+
assert.ok(
|
|
2514
|
+
observed.every((c) => c?.namespaces?.includes("ns_allow")),
|
|
2515
|
+
"every service read carried the allow token's namespace — no deny bleed",
|
|
2516
|
+
);
|
|
2517
|
+
} finally {
|
|
2518
|
+
await server.stop();
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
test("HTTP chat/message gates the NEW session's namespace — scoped token cannot start a chat in a disallowed namespace (issue #1850 r7)", async () => {
|
|
2523
|
+
// A NEW chat turn (no chatSessionId) previously never hit the effective-
|
|
2524
|
+
// namespace chokepoint, so a namespace-scoped token could start a chat in the
|
|
2525
|
+
// server DEFAULT namespace even when "default" was outside its allow-list.
|
|
2526
|
+
// The new-session path must resolve the effective namespace (the server
|
|
2527
|
+
// default, since the HTTP chat handler forwards no namespace) and fail closed
|
|
2528
|
+
// against the token allow-list — same as the resume path.
|
|
2529
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-chat-new-ns-"));
|
|
2530
|
+
try {
|
|
2531
|
+
const service = {
|
|
2532
|
+
configRef: parseConfig({
|
|
2533
|
+
memoryDir: dir,
|
|
2534
|
+
namespacesEnabled: true,
|
|
2535
|
+
defaultNamespace: "default",
|
|
2536
|
+
chat: { ...DEFAULT_CHAT_CONFIG, enabled: true },
|
|
2537
|
+
}),
|
|
2538
|
+
memoryDir: dir,
|
|
2539
|
+
fallbackLlmRef: { chatCompletion: async () => ({ content: "stub reply" }) },
|
|
2540
|
+
localLlmRef: null,
|
|
2541
|
+
memoryGet: () => Promise.resolve(null),
|
|
2542
|
+
memoryTimeline: () => Promise.resolve([]),
|
|
2543
|
+
memorySearch: () => Promise.resolve({ results: [], count: 0 }),
|
|
2544
|
+
recallExplain: () => Promise.resolve(null),
|
|
2545
|
+
entityGet: () => Promise.resolve(null),
|
|
2546
|
+
memoryProfile: () => Promise.resolve({}),
|
|
2547
|
+
memoryEntitiesList: () => Promise.resolve({ items: [] }),
|
|
2548
|
+
memoryQuestions: () => Promise.resolve({ items: [] }),
|
|
2549
|
+
reviewQueue: () => Promise.resolve({ items: [] }),
|
|
2550
|
+
} as unknown as EngramAccessService;
|
|
2551
|
+
const server = new EngramAccessHttpServer({
|
|
2552
|
+
service,
|
|
2553
|
+
port: 0,
|
|
2554
|
+
authTokenEntriesGetter: () => [
|
|
2555
|
+
// scoped to ns_a (NOT the server default) — must be denied a new chat.
|
|
2556
|
+
{ token: "scoped-ns-a", capabilities: { version: 1, ops: ["chat_message"], namespaces: ["ns_a"] } },
|
|
2557
|
+
// scoped to the server default — may start a new chat there.
|
|
2558
|
+
{ token: "scoped-default", capabilities: { version: 1, ops: ["chat_message"], namespaces: ["default"] } },
|
|
2559
|
+
],
|
|
2560
|
+
adminConsoleEnabled: false,
|
|
2561
|
+
});
|
|
2562
|
+
const status = await server.start();
|
|
2563
|
+
const post = (token: string) =>
|
|
2564
|
+
fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
|
|
2565
|
+
method: "POST",
|
|
2566
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
2567
|
+
body: JSON.stringify({ message: "hi" }), // no chatSessionId → NEW session
|
|
2568
|
+
});
|
|
2569
|
+
try {
|
|
2570
|
+
// DISALLOWED: token scoped to ns_a; the new session would land in the
|
|
2571
|
+
// server default ("default"), which is not in ["ns_a"] → 403.
|
|
2572
|
+
const denied = await post("scoped-ns-a");
|
|
2573
|
+
assert.equal(denied.status, 403, "scoped-to-ns_a must not start a new chat in the default namespace");
|
|
2574
|
+
|
|
2575
|
+
// ALLOWED: token scoped to the server default → gate passes → chat runs
|
|
2576
|
+
// and returns 200 with a reply.
|
|
2577
|
+
const allowed = await post("scoped-default");
|
|
2578
|
+
assert.equal(allowed.status, 200, "scoped-to-default may start a new chat in the default namespace");
|
|
2579
|
+
const allowedBody = (await allowed.json()) as { reply?: string };
|
|
2580
|
+
assert.ok(allowedBody.reply && allowedBody.reply.length > 0, "allowed new chat returns a reply");
|
|
2581
|
+
} finally {
|
|
2582
|
+
await server.stop();
|
|
2583
|
+
}
|
|
2584
|
+
} finally {
|
|
2585
|
+
await rm(dir, { recursive: true, force: true });
|
|
2586
|
+
}
|
|
2587
|
+
});
|