@remnic/core 9.54.4 → 9.54.6
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 +24 -23
- package/dist/access-admin-ops-surface.js +55 -50
- package/dist/access-authorization-probe.d.ts +25 -22
- package/dist/access-authorization-probe.js +58 -51
- package/dist/access-boundary.d.ts +26 -23
- package/dist/access-boundary.js +59 -52
- package/dist/access-cli.js +115 -107
- package/dist/access-cli.js.map +1 -1
- package/dist/access-coding-context-resolution.d.ts +1 -1
- package/dist/access-extraction-force-flush.d.ts +19 -18
- package/dist/access-extraction-force-flush.js +55 -50
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +22 -21
- package/dist/access-http-lifecycle-flush.d.ts +22 -21
- package/dist/access-http-offline-stream.d.ts +19 -18
- package/dist/access-http-query.js +55 -50
- package/dist/access-http.d.ts +55 -36
- package/dist/access-http.js +76 -65
- package/dist/access-identity-continuity-surface.d.ts +9 -9
- package/dist/access-identity-continuity-surface.js +55 -50
- package/dist/access-lcm-surface.d.ts +21 -20
- package/dist/access-lcm-surface.js +55 -50
- package/dist/access-mcp.d.ts +22 -21
- package/dist/access-mcp.js +63 -57
- package/dist/access-memory-search-fanout.d.ts +22 -2
- package/dist/access-memory-search-fanout.js +1 -1
- package/dist/{access-namespace-preflight-C7n4tgWC.d.ts → access-namespace-preflight-CCNn_6NE.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +55 -50
- package/dist/access-observe-write-surface.d.ts +22 -21
- package/dist/access-observe-write-surface.js +55 -50
- package/dist/access-offline-manifest.d.ts +10 -9
- package/dist/access-offline-manifest.js +12 -10
- package/dist/access-operations-batch.js +57 -52
- package/dist/access-operations.d.ts +23 -22
- package/dist/access-operations.js +61 -55
- package/dist/access-recall-concurrency.d.ts +21 -20
- package/dist/access-recall-concurrency.js +55 -50
- package/dist/access-recall-response.d.ts +19 -18
- package/dist/access-recall-response.js +55 -50
- package/dist/access-recall-surface.d.ts +19 -18
- package/dist/access-recall-surface.js +55 -50
- package/dist/access-schema.d.ts +2 -2
- package/dist/access-schema.js +5 -4
- package/dist/{access-service-CXvbxLK-.d.ts → access-service-DkSdXK4S.d.ts} +1314 -83
- package/dist/access-service-helpers.d.ts +22 -21
- package/dist/access-service.d.ts +31 -30
- package/dist/access-service.js +55 -50
- package/dist/access-surface-catalog.d.ts +22 -21
- package/dist/access-surface-catalog.js +209 -2
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +3 -3
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +14 -1
- package/dist/active-memory-bridge.js +6 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/admin-console/public/what-helps-me/app.js +1594 -0
- package/dist/admin-console/public/what-helps-me/index.html +339 -0
- package/dist/admin-console/public/what-helps-me/model.js +685 -0
- package/dist/admin-console/public/what-helps-me/what-helps-me.css +1296 -0
- package/dist/ambient-provenance.d.ts +1 -1
- package/dist/artifact-search.d.ts +1 -1
- package/dist/{auto-sync-TDHLZPVP.js → auto-sync-NR3GPCOA.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +9 -9
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +33 -12
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/buffer.js +3 -3
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-BOOxl-I2.d.ts → catalog-DGFf8mD9.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +34 -13
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-ZYLC7NFO.js → chunk-26ZXBTYN.js} +4 -4
- package/dist/chunk-26ZXBTYN.js.map +1 -0
- package/dist/{chunk-LLJMAULE.js → chunk-2FKEIPIT.js} +5 -5
- package/dist/{chunk-3CSX3ETE.js → chunk-2LWQSV6A.js} +23 -11
- package/dist/chunk-2LWQSV6A.js.map +1 -0
- package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
- package/dist/chunk-37DZZFFM.js +502 -0
- package/dist/chunk-37DZZFFM.js.map +1 -0
- package/dist/chunk-3HSRWJTC.js +9 -0
- package/dist/chunk-3HSRWJTC.js.map +1 -0
- package/dist/{chunk-XUAMCLY4.js → chunk-3TIZPSJC.js} +17 -13
- package/dist/chunk-3TIZPSJC.js.map +1 -0
- package/dist/{chunk-PDDTI7IC.js → chunk-43SEBY2M.js} +1 -1
- package/dist/{chunk-HWRD23DW.js → chunk-46SZBVDS.js} +3 -3
- package/dist/{chunk-6HPHEFXT.js → chunk-5FD7JMTX.js} +2 -2
- package/dist/{chunk-7BZOBA5J.js → chunk-5NB726GV.js} +70 -16
- package/dist/chunk-5NB726GV.js.map +1 -0
- package/dist/chunk-62OKGIJA.js +39 -0
- package/dist/chunk-62OKGIJA.js.map +1 -0
- package/dist/{chunk-IBKR2TFR.js → chunk-6CYHUWCO.js} +2 -2
- package/dist/{chunk-YN6ASZUG.js → chunk-6QPI2ITJ.js} +5 -5
- package/dist/{chunk-UU3G6GUP.js → chunk-76JJSXK7.js} +1 -1
- package/dist/chunk-76JJSXK7.js.map +1 -0
- package/dist/{chunk-PX2LSHYM.js → chunk-7BD6O7ZV.js} +189 -4
- package/dist/chunk-7BD6O7ZV.js.map +1 -0
- package/dist/chunk-7TBXNYLO.js +346 -0
- package/dist/chunk-7TBXNYLO.js.map +1 -0
- package/dist/{chunk-NH5IR2WM.js → chunk-BRR5EUTF.js} +6 -2
- package/dist/chunk-BRR5EUTF.js.map +1 -0
- package/dist/{chunk-NIS256NW.js → chunk-CIAI24TQ.js} +3 -3
- package/dist/{chunk-T2JHZWH6.js → chunk-D3LBIUWV.js} +11703 -7573
- package/dist/chunk-D3LBIUWV.js.map +1 -0
- package/dist/{chunk-7FGL6CGA.js → chunk-DVYRD4HY.js} +2 -2
- package/dist/{chunk-G7Z6EENO.js → chunk-EKTP7MNS.js} +4 -4
- package/dist/{chunk-S75M5ZRK.js → chunk-F2JRDPON.js} +16 -1
- package/dist/chunk-F2JRDPON.js.map +1 -0
- package/dist/{chunk-RZ4XGKOJ.js → chunk-FFRTNF3L.js} +8 -8
- package/dist/{chunk-VKXWK44Q.js → chunk-FONWBCVY.js} +3 -3
- package/dist/{chunk-DZD576ZU.js → chunk-FPLM2TKD.js} +2 -2
- package/dist/{chunk-ZPJ4QSGP.js → chunk-FTPONF6B.js} +2 -2
- package/dist/{chunk-LJS7KUPV.js → chunk-FWX7W5PM.js} +2 -2
- package/dist/{chunk-UTRO5UJU.js → chunk-G6DFSS6A.js} +5 -1
- package/dist/chunk-G6DFSS6A.js.map +1 -0
- package/dist/{chunk-6LAPLZOS.js → chunk-GJ776BC2.js} +8 -3
- package/dist/chunk-GJ776BC2.js.map +1 -0
- package/dist/{chunk-G4T7L557.js → chunk-GN3WRFJY.js} +6 -6
- package/dist/{chunk-QT2FRYI4.js → chunk-H7NAZ3UB.js} +2 -2
- package/dist/{chunk-APPFNGX6.js → chunk-HBUFQRIM.js} +2 -2
- package/dist/{chunk-UU7TRFNE.js → chunk-HGJ6NJ4E.js} +2 -2
- package/dist/{chunk-WB7JSQCP.js → chunk-HIEWDJ2O.js} +5 -2
- package/dist/{chunk-WB7JSQCP.js.map → chunk-HIEWDJ2O.js.map} +1 -1
- package/dist/{chunk-UES4BHA2.js → chunk-I3MSGFGD.js} +17 -6
- package/dist/chunk-I3MSGFGD.js.map +1 -0
- package/dist/{chunk-C4ZNGVK3.js → chunk-IKP5OYQQ.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-RDJRFDJY.js → chunk-IQ4NVWEF.js} +2 -2
- package/dist/{chunk-CGVHPCP2.js → chunk-JYG4OLWH.js} +2 -2
- package/dist/{chunk-WOPA3SGI.js → chunk-K26NWEOC.js} +8 -8
- package/dist/{chunk-K4QS7LXU.js → chunk-K2UGPTYY.js} +485 -44
- package/dist/chunk-K2UGPTYY.js.map +1 -0
- package/dist/{chunk-L6CQ7WIW.js → chunk-KFBOPMF6.js} +11 -11
- package/dist/chunk-KFBOPMF6.js.map +1 -0
- package/dist/{chunk-G7A5HLUS.js → chunk-KJH6W2I7.js} +2 -2
- package/dist/{chunk-IXEBUYO4.js → chunk-KM55SMLG.js} +11 -2
- package/dist/{chunk-IXEBUYO4.js.map → chunk-KM55SMLG.js.map} +1 -1
- package/dist/{chunk-HOIRB77A.js → chunk-L6Z5OAS4.js} +2 -2
- package/dist/chunk-LC2BDILZ.js +57 -0
- package/dist/chunk-LC2BDILZ.js.map +1 -0
- package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
- package/dist/{chunk-6R4G7N4S.js → chunk-M4YHAODQ.js} +388 -748
- package/dist/chunk-M4YHAODQ.js.map +1 -0
- package/dist/{chunk-3JLPGJ43.js → chunk-MIH3CJPN.js} +5 -5
- package/dist/{chunk-QHFV6VNU.js → chunk-MK5VEWCR.js} +2 -2
- package/dist/chunk-NISZQW72.js +140 -0
- package/dist/chunk-NISZQW72.js.map +1 -0
- package/dist/{chunk-GUKBW4HL.js → chunk-NQMSEB5D.js} +6 -6
- package/dist/{chunk-G2LUMAUF.js → chunk-OCIP6YMD.js} +12 -5
- package/dist/chunk-OCIP6YMD.js.map +1 -0
- package/dist/{chunk-47P3RYZP.js → chunk-PG4QBPVG.js} +21 -11
- package/dist/chunk-PG4QBPVG.js.map +1 -0
- package/dist/{chunk-WRJVB3C2.js → chunk-PO7NXBG2.js} +2 -2
- package/dist/{chunk-QWGE2PUH.js → chunk-PT54WISF.js} +190 -56
- package/dist/chunk-PT54WISF.js.map +1 -0
- package/dist/{chunk-AZM4CCAC.js → chunk-QZLDRQ4I.js} +8 -8
- package/dist/{chunk-THPPCISA.js → chunk-RBII7637.js} +5 -5
- package/dist/{chunk-JDZPNCMA.js → chunk-RUNORG2P.js} +6 -3
- package/dist/chunk-RUNORG2P.js.map +1 -0
- package/dist/{chunk-JV5YUBIZ.js → chunk-RZQ6W2W4.js} +50 -44
- package/dist/chunk-RZQ6W2W4.js.map +1 -0
- package/dist/{chunk-RUMNY5DT.js → chunk-SMNYAMJD.js} +6 -2
- package/dist/chunk-SMNYAMJD.js.map +1 -0
- package/dist/{chunk-MKQBXQKU.js → chunk-TB6CBZ5V.js} +22 -15
- package/dist/chunk-TB6CBZ5V.js.map +1 -0
- package/dist/{chunk-VMMQDEN5.js → chunk-TO7ZTWJW.js} +2 -2
- package/dist/{chunk-K2TDK7GG.js → chunk-TWTUGCTY.js} +2 -2
- package/dist/{chunk-HLYR2TQL.js → chunk-UCTS3DDH.js} +2 -2
- package/dist/{chunk-JYIJNNN3.js → chunk-UG6VXIOK.js} +2 -2
- package/dist/{chunk-WM65LU67.js → chunk-UKYS3NU5.js} +2 -2
- package/dist/{chunk-WDBX6VER.js → chunk-UPLZNJKN.js} +5 -5
- package/dist/{chunk-C23BYTJV.js → chunk-UW5NPZJ2.js} +40 -12
- package/dist/chunk-UW5NPZJ2.js.map +1 -0
- package/dist/{chunk-2GFK22OZ.js → chunk-V6ERBR7U.js} +2 -2
- package/dist/{chunk-CYF6WVE7.js → chunk-W5ZKPI34.js} +139 -158
- package/dist/chunk-W5ZKPI34.js.map +1 -0
- package/dist/chunk-XX4HPFFM.js +370 -0
- package/dist/chunk-XX4HPFFM.js.map +1 -0
- package/dist/{chunk-ZMYDYNI4.js → chunk-XZHE2DEN.js} +4 -4
- package/dist/chunk-YXQEKCZM.js +223 -0
- package/dist/chunk-YXQEKCZM.js.map +1 -0
- package/dist/{chunk-GYPQL6XN.js → chunk-Z4N7SVVF.js} +35 -47
- package/dist/chunk-Z4N7SVVF.js.map +1 -0
- package/dist/{chunk-MAXVH44E.js → chunk-Z54EM5YZ.js} +7 -7
- package/dist/{chunk-YGLY6QFV.js → chunk-ZRZVLB7O.js} +2 -2
- package/dist/{chunk-4IIFL7GC.js → chunk-ZUKIUXHQ.js} +10 -10
- package/dist/{cli-DVUFIa5E.d.ts → cli-Bze8S0Qs.d.ts} +5 -5
- package/dist/cli.d.ts +22 -21
- package/dist/cli.js +90 -80
- package/dist/coding/pre-action-gate.d.ts +1 -1
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +33 -12
- 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 +4 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +33 -12
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +32 -12
- 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 +3 -1
- package/dist/contradiction-scan-JDTBXRGW.js +20 -0
- package/dist/converge-config.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +2 -2
- package/dist/convergence-refresh.js +35 -14
- 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/corpus-watermark.d.ts +1 -1
- package/dist/corpus-watermark.js +25 -24
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/dependency-propagation-config.d.ts +1 -1
- package/dist/{dependency-propagation-delivery-XzF76C9n.d.ts → dependency-propagation-delivery-CzEfB31P.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 +33 -12
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +9 -9
- package/dist/explicit-capture.js +7 -4
- package/dist/external-wiki-access.d.ts +22 -21
- package/dist/external-wiki-access.js +56 -51
- package/dist/external-wiki-collection-registration.d.ts +1 -1
- package/dist/external-wiki-collection.d.ts +1 -1
- package/dist/external-wiki-mcp-tools.d.ts +22 -21
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-faithfulness.js +2 -2
- 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-judge.js +2 -2
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-normalization.d.ts +1 -1
- package/dist/extraction-prompt.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +35 -19
- package/dist/fallback-llm.d.ts +14 -1
- package/dist/fallback-llm.js +1 -1
- package/dist/{first-start-migration-626C5554.js → first-start-migration-CRZCZAJN.js} +11 -3
- package/dist/first-start-migration-CRZCZAJN.js.map +1 -0
- 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-path-reconstruction.d.ts +1 -1
- package/dist/graph-path-scoring.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +314 -308
- package/dist/index.js +304 -133
- package/dist/inline-script.d.ts +3 -0
- package/dist/inline-script.js +8 -0
- package/dist/inline-script.js.map +1 -0
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +6 -6
- 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 +2 -1
- package/dist/local-llm.js +2 -2
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +33 -12
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +33 -12
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +34 -13
- package/dist/{maintenance-BJzNIKBE.d.ts → maintenance-C8aRWfhq.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +22 -21
- package/dist/mcp-read-only-tools.js +1 -1
- 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-mutations.js +2 -2
- package/dist/memory-projection-store.d.ts +6 -2
- package/dist/memory-projection-store.js +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-snapshot.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 +24 -23
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +33 -12
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-file-io.d.ts +20 -0
- package/dist/offline-sync-file-io.js +18 -0
- package/dist/offline-sync-file-io.js.map +1 -0
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/offline-sync-impression-drain.js +5 -5
- package/dist/offline-sync.d.ts +8 -6
- package/dist/offline-sync.js +2 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +26 -25
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +39 -36
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +38 -18
- package/dist/{orchestrator-BiXgBJBQ.d.ts → orchestrator-D85CzDDY.d.ts} +11 -8
- package/dist/orchestrator.d.ts +9 -9
- package/dist/orchestrator.js +117 -109
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-sz_dcuLg.d.ts → pipeline-DPcUifCO.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/public-http-D-SyWNj6.d.ts +25 -0
- package/dist/{purge-533FZLS4.js → purge-GB7PZHT2.js} +2 -2
- package/dist/{qmd-eAWObK4M.d.ts → qmd-dFCHp1r0.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-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +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-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/cursor.d.ts +2 -1
- package/dist/reconcile/manifest.d.ts +2 -1
- package/dist/reconcile/manifest.js +6 -5
- package/dist/reconcile/plan.d.ts +2 -1
- package/dist/reconcile/plan.js +3 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -3
- 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/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +38 -38
- package/dist/{scope-profiles-BBQsXuyj.d.ts → scope-profiles-DICK7GtN.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +12 -12
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-HD8RkzVA.d.ts → semantic-consolidation-BP7NAX0N.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +34 -13
- package/dist/semantic-rule-promotion.js +33 -12
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +33 -12
- package/dist/{service-DIrtHHEg.d.ts → service-DT-pJyda.d.ts} +660 -660
- 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/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +31 -15
- package/dist/{storage-DqCzXi56.d.ts → storage-qOnaJ1Lw.d.ts} +49 -13
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +33 -12
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +5 -5
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/support-passport/index.d.ts +518 -0
- package/dist/support-passport/index.js +357 -0
- package/dist/support-passport/index.js.map +1 -0
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- 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 +2 -2
- package/dist/tier-routing.js +7 -1
- package/dist/{tier-stats-2JCZYEF7.js → tier-stats-EXYSKSVN.js} +8 -2
- package/dist/{tier-stats-2JCZYEF7.js.map → tier-stats-EXYSKSVN.js.map} +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +36 -36
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-d_j4vC6N.d.ts → types-ed-SvS2t.d.ts} +8 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +2 -2
- package/dist/verified-recall.js +33 -12
- package/dist/write-envelope.d.ts +1 -1
- package/dist/write-rate-limiter.d.ts +11 -5
- package/dist/write-rate-limiter.js +1 -1
- package/package.json +12 -2
- package/scripts/build-with-heap.mjs +11 -1
- package/src/access-authorization-probe.ts +20 -2
- package/src/access-boundary.ts +32 -0
- package/src/access-http.test.ts +1971 -643
- package/src/access-http.ts +81 -74
- package/src/access-mcp.ts +3 -1
- package/src/access-memory-search-fanout.ts +24 -1
- package/src/access-offline-manifest.ts +5 -0
- package/src/access-operations.ts +2 -0
- package/src/access-service-namespace.test.ts +14 -0
- package/src/access-service-observe-scope.test.ts +2 -0
- package/src/access-service-offline-file-content.test.ts +211 -0
- package/src/access-service.ts +628 -884
- package/src/access-surface-catalog.test.ts +40 -10
- package/src/access-surface-catalog.ts +15 -0
- package/src/active-memory-bridge.test.ts +151 -0
- package/src/active-memory-bridge.ts +95 -21
- package/src/briefing.test.ts +38 -0
- package/src/briefing.ts +3 -3
- package/src/cli.ts +6 -1
- package/src/config.ts +18 -9
- package/src/connectors/codex-materialize-runner.ts +2 -0
- package/src/contradiction/contradiction-scan.ts +2 -0
- package/src/contradiction/contradiction.test.ts +42 -0
- package/src/correction/correction-access-wiring.ts +8 -2
- package/src/correction/correction-storage-integration.test.ts +7 -0
- package/src/explicit-capture.test.ts +24 -0
- package/src/explicit-capture.ts +2 -0
- package/src/fallback-llm.test.ts +54 -0
- package/src/fallback-llm.ts +54 -9
- package/src/index.ts +3 -0
- package/src/inline-script.ts +6 -0
- package/src/local-llm-headers-timeout.test.ts +42 -0
- package/src/local-llm.ts +8 -8
- package/src/maintenance/first-start-migration.ts +2 -1
- package/src/maintenance/memory-governance.test.ts +25 -0
- package/src/maintenance/memory-governance.ts +4 -1
- package/src/maintenance/pattern-reinforcement.test.ts +20 -0
- package/src/maintenance/pattern-reinforcement.ts +2 -1
- package/src/maintenance/rebuild-memory-projection.ts +8 -8
- package/src/mcp-read-only-tools.ts +3 -0
- package/src/memory-projection-store.ts +25 -27
- package/src/offline-sync-file-io.ts +68 -0
- package/src/offline-sync.ts +43 -57
- package/src/orchestration/consolidation-run.ts +5 -4
- package/src/orchestration/contradiction-linking-coordinator.ts +7 -0
- package/src/orchestration/contradiction-localization.integration.test.ts +115 -0
- package/src/orchestration/lifecycle-policy-coordinator.ts +2 -1
- package/src/orchestration/qmd-result-resolver.test.ts +215 -14
- package/src/orchestration/qmd-result-resolver.ts +142 -82
- package/src/orchestration/recall-search-pipeline.test.ts +41 -0
- package/src/orchestration/recall-search-pipeline.ts +12 -0
- package/src/orchestration/semantic-consolidation-coordinator.ts +2 -1
- package/src/orchestration/tier-migration-coordinator.ts +23 -56
- package/src/orchestration/workspace-ops.test.ts +37 -0
- package/src/orchestration/workspace-ops.ts +5 -29
- package/src/orchestrator.ts +7 -8
- package/src/relay/mission-access.test.ts +11 -11
- package/src/relay/mission-ui.test.ts +3 -1
- package/src/storage/supersession-audit.ts +7 -1
- package/src/storage/supersession-side-effects.ts +17 -5
- package/src/storage/tombstone-blocked-capture-sync.ts +31 -0
- package/src/storage-contract/failure-semantics.test.ts +102 -0
- package/src/storage.ts +18 -19
- package/src/support-passport/access-http-base.ts +143 -0
- package/src/support-passport/access-http.ts +172 -0
- package/src/support-passport/access-operations.ts +272 -0
- package/src/support-passport/access-service-base.test.ts +193 -0
- package/src/support-passport/access-service-base.ts +176 -0
- package/src/support-passport/access-surface.test.ts +283 -0
- package/src/support-passport/access-surface.ts +286 -0
- package/src/support-passport/card-projection.test.ts +137 -0
- package/src/support-passport/card-projection.ts +348 -0
- package/src/support-passport/card-replacement.ts +126 -0
- package/src/support-passport/card-service-commit-forwarding.test.ts +160 -0
- package/src/support-passport/card-service.test.ts +2964 -0
- package/src/support-passport/card-service.ts +1184 -0
- package/src/support-passport/card-state.ts +158 -0
- package/src/support-passport/config-model-routing.test.ts +37 -0
- package/src/support-passport/config.test.ts +34 -0
- package/src/support-passport/config.ts +41 -0
- package/src/support-passport/contracts.test.ts +156 -0
- package/src/support-passport/contracts.ts +149 -0
- package/src/support-passport/errors.ts +30 -0
- package/src/support-passport/generated-batch.test.ts +185 -0
- package/src/support-passport/generated-batch.ts +338 -0
- package/src/support-passport/grant-contracts.ts +183 -0
- package/src/support-passport/grant-generated-batch-scope.test.ts +201 -0
- package/src/support-passport/grant-service-cache.test.ts +148 -0
- package/src/support-passport/grant-service-cancellation.test.ts +83 -0
- package/src/support-passport/grant-service-marker-cache.test.ts +62 -0
- package/src/support-passport/grant-service.test.ts +3496 -0
- package/src/support-passport/grant-service.ts +513 -0
- package/src/support-passport/grant-store-commit-hook.test.ts +123 -0
- package/src/support-passport/grant-store-order.test.ts +60 -0
- package/src/support-passport/grant-store-path-safety.test.ts +30 -0
- package/src/support-passport/grant-store-recovery.test.ts +448 -0
- package/src/support-passport/grant-store.ts +906 -0
- package/src/support-passport/index.ts +17 -0
- package/src/support-passport/mcp-tools.test.ts +222 -0
- package/src/support-passport/mcp-tools.ts +187 -0
- package/src/support-passport/model-adapter.test.ts +840 -0
- package/src/support-passport/model-adapter.ts +602 -0
- package/src/support-passport/model-audit-pinned-write.test.ts +59 -0
- package/src/support-passport/model-audit.ts +152 -0
- package/src/support-passport/model-bridge.test.ts +456 -0
- package/src/support-passport/model-bridge.ts +439 -0
- package/src/support-passport/model-contracts.ts +124 -0
- package/src/support-passport/model-service.test.ts +1731 -0
- package/src/support-passport/model-service.ts +399 -0
- package/src/support-passport/offline-sync-guard.ts +230 -0
- package/src/support-passport/owner-http.test.ts +671 -0
- package/src/support-passport/owner-lock.ts +70 -0
- package/src/support-passport/private-file.test.ts +99 -0
- package/src/support-passport/private-file.ts +498 -0
- package/src/support-passport/public-http.test.ts +476 -0
- package/src/support-passport/public-http.ts +601 -0
- package/src/support-passport/ui.test.ts +181 -0
- package/src/tier-routing.ts +28 -17
- package/src/training-export/converter.test.ts +24 -0
- package/src/training-export/converter.ts +2 -0
- package/src/types.ts +8 -1
- package/src/wearables/service.test.ts +13 -0
- package/src/wearables/service.ts +2 -0
- package/src/write-rate-limiter.test.ts +30 -6
- package/src/write-rate-limiter.ts +29 -18
- package/dist/chunk-3CSX3ETE.js.map +0 -1
- package/dist/chunk-47P3RYZP.js.map +0 -1
- package/dist/chunk-6LAPLZOS.js.map +0 -1
- package/dist/chunk-6R4G7N4S.js.map +0 -1
- package/dist/chunk-7BZOBA5J.js.map +0 -1
- package/dist/chunk-C23BYTJV.js.map +0 -1
- package/dist/chunk-CYF6WVE7.js.map +0 -1
- package/dist/chunk-G2LUMAUF.js.map +0 -1
- package/dist/chunk-GYPQL6XN.js.map +0 -1
- package/dist/chunk-JDZPNCMA.js.map +0 -1
- package/dist/chunk-JV5YUBIZ.js.map +0 -1
- package/dist/chunk-K4QS7LXU.js.map +0 -1
- package/dist/chunk-L6CQ7WIW.js.map +0 -1
- package/dist/chunk-MKQBXQKU.js.map +0 -1
- package/dist/chunk-NH5IR2WM.js.map +0 -1
- package/dist/chunk-PX2LSHYM.js.map +0 -1
- package/dist/chunk-QWGE2PUH.js.map +0 -1
- package/dist/chunk-RUMNY5DT.js.map +0 -1
- package/dist/chunk-S75M5ZRK.js.map +0 -1
- package/dist/chunk-T2JHZWH6.js.map +0 -1
- package/dist/chunk-UES4BHA2.js.map +0 -1
- package/dist/chunk-UTRO5UJU.js.map +0 -1
- package/dist/chunk-UU3G6GUP.js.map +0 -1
- package/dist/chunk-XUAMCLY4.js.map +0 -1
- package/dist/chunk-ZYLC7NFO.js.map +0 -1
- package/dist/contradiction-scan-HYLIRMJ2.js +0 -14
- package/dist/first-start-migration-626C5554.js.map +0 -1
- /package/dist/{auto-sync-TDHLZPVP.js.map → auto-sync-NR3GPCOA.js.map} +0 -0
- /package/dist/{chunk-LLJMAULE.js.map → chunk-2FKEIPIT.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-PDDTI7IC.js.map → chunk-43SEBY2M.js.map} +0 -0
- /package/dist/{chunk-HWRD23DW.js.map → chunk-46SZBVDS.js.map} +0 -0
- /package/dist/{chunk-6HPHEFXT.js.map → chunk-5FD7JMTX.js.map} +0 -0
- /package/dist/{chunk-IBKR2TFR.js.map → chunk-6CYHUWCO.js.map} +0 -0
- /package/dist/{chunk-YN6ASZUG.js.map → chunk-6QPI2ITJ.js.map} +0 -0
- /package/dist/{chunk-NIS256NW.js.map → chunk-CIAI24TQ.js.map} +0 -0
- /package/dist/{chunk-7FGL6CGA.js.map → chunk-DVYRD4HY.js.map} +0 -0
- /package/dist/{chunk-G7Z6EENO.js.map → chunk-EKTP7MNS.js.map} +0 -0
- /package/dist/{chunk-RZ4XGKOJ.js.map → chunk-FFRTNF3L.js.map} +0 -0
- /package/dist/{chunk-VKXWK44Q.js.map → chunk-FONWBCVY.js.map} +0 -0
- /package/dist/{chunk-DZD576ZU.js.map → chunk-FPLM2TKD.js.map} +0 -0
- /package/dist/{chunk-ZPJ4QSGP.js.map → chunk-FTPONF6B.js.map} +0 -0
- /package/dist/{chunk-LJS7KUPV.js.map → chunk-FWX7W5PM.js.map} +0 -0
- /package/dist/{chunk-G4T7L557.js.map → chunk-GN3WRFJY.js.map} +0 -0
- /package/dist/{chunk-QT2FRYI4.js.map → chunk-H7NAZ3UB.js.map} +0 -0
- /package/dist/{chunk-APPFNGX6.js.map → chunk-HBUFQRIM.js.map} +0 -0
- /package/dist/{chunk-UU7TRFNE.js.map → chunk-HGJ6NJ4E.js.map} +0 -0
- /package/dist/{chunk-C4ZNGVK3.js.map → chunk-IKP5OYQQ.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-RDJRFDJY.js.map → chunk-IQ4NVWEF.js.map} +0 -0
- /package/dist/{chunk-CGVHPCP2.js.map → chunk-JYG4OLWH.js.map} +0 -0
- /package/dist/{chunk-WOPA3SGI.js.map → chunk-K26NWEOC.js.map} +0 -0
- /package/dist/{chunk-G7A5HLUS.js.map → chunk-KJH6W2I7.js.map} +0 -0
- /package/dist/{chunk-HOIRB77A.js.map → chunk-L6Z5OAS4.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-3JLPGJ43.js.map → chunk-MIH3CJPN.js.map} +0 -0
- /package/dist/{chunk-QHFV6VNU.js.map → chunk-MK5VEWCR.js.map} +0 -0
- /package/dist/{chunk-GUKBW4HL.js.map → chunk-NQMSEB5D.js.map} +0 -0
- /package/dist/{chunk-WRJVB3C2.js.map → chunk-PO7NXBG2.js.map} +0 -0
- /package/dist/{chunk-AZM4CCAC.js.map → chunk-QZLDRQ4I.js.map} +0 -0
- /package/dist/{chunk-THPPCISA.js.map → chunk-RBII7637.js.map} +0 -0
- /package/dist/{chunk-VMMQDEN5.js.map → chunk-TO7ZTWJW.js.map} +0 -0
- /package/dist/{chunk-K2TDK7GG.js.map → chunk-TWTUGCTY.js.map} +0 -0
- /package/dist/{chunk-HLYR2TQL.js.map → chunk-UCTS3DDH.js.map} +0 -0
- /package/dist/{chunk-JYIJNNN3.js.map → chunk-UG6VXIOK.js.map} +0 -0
- /package/dist/{chunk-WM65LU67.js.map → chunk-UKYS3NU5.js.map} +0 -0
- /package/dist/{chunk-WDBX6VER.js.map → chunk-UPLZNJKN.js.map} +0 -0
- /package/dist/{chunk-2GFK22OZ.js.map → chunk-V6ERBR7U.js.map} +0 -0
- /package/dist/{chunk-ZMYDYNI4.js.map → chunk-XZHE2DEN.js.map} +0 -0
- /package/dist/{chunk-MAXVH44E.js.map → chunk-Z54EM5YZ.js.map} +0 -0
- /package/dist/{chunk-YGLY6QFV.js.map → chunk-ZRZVLB7O.js.map} +0 -0
- /package/dist/{chunk-4IIFL7GC.js.map → chunk-ZUKIUXHQ.js.map} +0 -0
- /package/dist/{contradiction-scan-HYLIRMJ2.js.map → contradiction-scan-JDTBXRGW.js.map} +0 -0
- /package/dist/{purge-533FZLS4.js.map → purge-GB7PZHT2.js.map} +0 -0
package/src/access-http.test.ts
CHANGED
|
@@ -5,9 +5,20 @@ import path from "node:path";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { gzipSync } from "node:zlib";
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {
|
|
9
|
+
EngramAccessHttpServer,
|
|
10
|
+
type RemnicAdminControls,
|
|
11
|
+
type RemnicAdminDashboardStatus,
|
|
12
|
+
} from "./access-http.js";
|
|
13
|
+
import {
|
|
14
|
+
EngramAccessInputError,
|
|
15
|
+
type EngramAccessRecallRequest,
|
|
16
|
+
type EngramAccessService,
|
|
17
|
+
} from "./access-service.js";
|
|
18
|
+
import {
|
|
19
|
+
tokenCapabilityStore,
|
|
20
|
+
type TokenCapabilities,
|
|
21
|
+
} from "./access-token-capabilities.js";
|
|
11
22
|
import { parseConfig } from "./config.js";
|
|
12
23
|
import { readPair, writePair } from "./contradiction/contradiction-review.js";
|
|
13
24
|
import { createChatSession } from "./chat/chat-session.js";
|
|
@@ -15,6 +26,7 @@ import { DEFAULT_CHAT_CONFIG } from "./chat/chat-config.js";
|
|
|
15
26
|
import { projectTagProjectId } from "./coding/coding-namespace.js";
|
|
16
27
|
import { OFFLINE_SYNC_MAX_MTIME_MS } from "./offline-sync.js";
|
|
17
28
|
import type { StorageManager } from "./storage.js";
|
|
29
|
+
import { SupportPassportError } from "./support-passport/errors.js";
|
|
18
30
|
|
|
19
31
|
test("HTTP server rejects invalid constructor ports", () => {
|
|
20
32
|
const service = {} as EngramAccessService;
|
|
@@ -78,13 +90,18 @@ test("HTTP liveness is authenticated and does not run detailed health diagnostic
|
|
|
78
90
|
|
|
79
91
|
const status = await server.start();
|
|
80
92
|
try {
|
|
81
|
-
const denied = await fetch(
|
|
93
|
+
const denied = await fetch(
|
|
94
|
+
`http://127.0.0.1:${status.port}/engram/v1/live`,
|
|
95
|
+
);
|
|
82
96
|
assert.equal(denied.status, 401);
|
|
83
97
|
|
|
84
|
-
const response = await fetch(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
const response = await fetch(
|
|
99
|
+
`http://127.0.0.1:${status.port}/engram/v1/live`,
|
|
100
|
+
{
|
|
101
|
+
headers: { authorization: "Bearer test-token" },
|
|
102
|
+
signal: AbortSignal.timeout(500),
|
|
103
|
+
},
|
|
104
|
+
);
|
|
88
105
|
assert.equal(response.status, 200);
|
|
89
106
|
assert.deepEqual(await response.json(), { ok: true, ready: true });
|
|
90
107
|
assert.equal(healthCalls, 0);
|
|
@@ -110,11 +127,17 @@ test("HTTP detailed health route still runs service diagnostics", async () => {
|
|
|
110
127
|
|
|
111
128
|
const status = await server.start();
|
|
112
129
|
try {
|
|
113
|
-
const response = await fetch(
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
const response = await fetch(
|
|
131
|
+
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
132
|
+
{
|
|
133
|
+
headers: { authorization: "Bearer test-token" },
|
|
134
|
+
},
|
|
135
|
+
);
|
|
116
136
|
assert.equal(response.status, 200);
|
|
117
|
-
assert.deepEqual(await response.json(), {
|
|
137
|
+
assert.deepEqual(await response.json(), {
|
|
138
|
+
ok: true,
|
|
139
|
+
diagnostics: "complete",
|
|
140
|
+
});
|
|
118
141
|
assert.equal(healthCalls, 1);
|
|
119
142
|
} finally {
|
|
120
143
|
await server.stop();
|
|
@@ -138,13 +161,25 @@ test("HTTP memory browse rejects malformed pagination and sort query values", as
|
|
|
138
161
|
|
|
139
162
|
const status = await server.start();
|
|
140
163
|
try {
|
|
141
|
-
for (const query of [
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
for (const query of [
|
|
165
|
+
"limit=10abc",
|
|
166
|
+
"offset=1.5",
|
|
167
|
+
"limit=0",
|
|
168
|
+
"sort=udpated_desc",
|
|
169
|
+
]) {
|
|
170
|
+
const response = await fetch(
|
|
171
|
+
`http://127.0.0.1:${status.port}/engram/v1/memories?${query}`,
|
|
172
|
+
{
|
|
173
|
+
headers: { authorization: "Bearer test-token" },
|
|
174
|
+
},
|
|
175
|
+
);
|
|
145
176
|
assert.equal(response.status, 400, `${query} should be rejected`);
|
|
146
177
|
}
|
|
147
|
-
assert.equal(
|
|
178
|
+
assert.equal(
|
|
179
|
+
calls.length,
|
|
180
|
+
0,
|
|
181
|
+
"invalid queries must fail before calling memoryBrowse",
|
|
182
|
+
);
|
|
148
183
|
|
|
149
184
|
const response = await fetch(
|
|
150
185
|
`http://127.0.0.1:${status.port}/engram/v1/memories?limit=10&offset=1&sort=updated_desc`,
|
|
@@ -169,7 +204,11 @@ test("HTTP memory browse rejects malformed pagination and sort query values", as
|
|
|
169
204
|
});
|
|
170
205
|
|
|
171
206
|
test("HTTP batch LCM flush isolates namespace failures and charges one write quota", async () => {
|
|
172
|
-
const calls: Array<{
|
|
207
|
+
const calls: Array<{
|
|
208
|
+
namespace?: string;
|
|
209
|
+
sessionKey: string;
|
|
210
|
+
authenticatedPrincipal?: string;
|
|
211
|
+
}> = [];
|
|
173
212
|
const service = {
|
|
174
213
|
lcmCompactionFlush: async (request: {
|
|
175
214
|
namespace?: string;
|
|
@@ -177,7 +216,8 @@ test("HTTP batch LCM flush isolates namespace failures and charges one write quo
|
|
|
177
216
|
authenticatedPrincipal?: string;
|
|
178
217
|
}) => {
|
|
179
218
|
calls.push(request);
|
|
180
|
-
if (request.namespace === "team-b")
|
|
219
|
+
if (request.namespace === "team-b")
|
|
220
|
+
throw new Error("team-b flush failed");
|
|
181
221
|
return { enabled: true, flushed: true };
|
|
182
222
|
},
|
|
183
223
|
} as unknown as EngramAccessService;
|
|
@@ -191,17 +231,20 @@ test("HTTP batch LCM flush isolates namespace failures and charges one write quo
|
|
|
191
231
|
|
|
192
232
|
const status = await server.start();
|
|
193
233
|
try {
|
|
194
|
-
const response = await fetch(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
const response = await fetch(
|
|
235
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/flush`,
|
|
236
|
+
{
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: {
|
|
239
|
+
authorization: "Bearer test-token",
|
|
240
|
+
"content-type": "application/json",
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
sessionKey: "batch-session",
|
|
244
|
+
namespaces: ["team-a", "team-b"],
|
|
245
|
+
}),
|
|
199
246
|
},
|
|
200
|
-
|
|
201
|
-
sessionKey: "batch-session",
|
|
202
|
-
namespaces: ["team-a", "team-b"],
|
|
203
|
-
}),
|
|
204
|
-
});
|
|
247
|
+
);
|
|
205
248
|
assert.equal(response.status, 200);
|
|
206
249
|
assert.deepEqual(await response.json(), {
|
|
207
250
|
enabled: false,
|
|
@@ -230,32 +273,42 @@ test("HTTP batch LCM flush isolates namespace failures and charges one write quo
|
|
|
230
273
|
},
|
|
231
274
|
]);
|
|
232
275
|
|
|
233
|
-
const conflicting = await fetch(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
const conflicting = await fetch(
|
|
277
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/flush`,
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
authorization: "Bearer test-token",
|
|
282
|
+
"content-type": "application/json",
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
sessionKey: "batch-session",
|
|
286
|
+
namespace: "team-a",
|
|
287
|
+
namespaces: ["team-b"],
|
|
288
|
+
}),
|
|
238
289
|
},
|
|
239
|
-
|
|
240
|
-
sessionKey: "batch-session",
|
|
241
|
-
namespace: "team-a",
|
|
242
|
-
namespaces: ["team-b"],
|
|
243
|
-
}),
|
|
244
|
-
});
|
|
290
|
+
);
|
|
245
291
|
assert.equal(conflicting.status, 400);
|
|
246
|
-
assert.equal(
|
|
292
|
+
assert.equal(
|
|
293
|
+
calls.length,
|
|
294
|
+
2,
|
|
295
|
+
"conflicting namespace forms must fail before dispatch",
|
|
296
|
+
);
|
|
247
297
|
|
|
248
|
-
const rateLimited = await fetch(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
298
|
+
const rateLimited = await fetch(
|
|
299
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/flush`,
|
|
300
|
+
{
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
authorization: "Bearer test-token",
|
|
304
|
+
"content-type": "application/json",
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
sessionKey: "batch-session",
|
|
308
|
+
namespaces: ["team-a"],
|
|
309
|
+
}),
|
|
253
310
|
},
|
|
254
|
-
|
|
255
|
-
sessionKey: "batch-session",
|
|
256
|
-
namespaces: ["team-a"],
|
|
257
|
-
}),
|
|
258
|
-
});
|
|
311
|
+
);
|
|
259
312
|
assert.equal(rateLimited.status, 429);
|
|
260
313
|
assert.equal(calls.length, 2, "a batch consumes one write quota unit");
|
|
261
314
|
} finally {
|
|
@@ -281,19 +334,22 @@ test("HTTP LCM compaction record uses the lifecycle route and write quota", asyn
|
|
|
281
334
|
|
|
282
335
|
const status = await server.start();
|
|
283
336
|
try {
|
|
284
|
-
const response = await fetch(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
337
|
+
const response = await fetch(
|
|
338
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/record`,
|
|
339
|
+
{
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: {
|
|
342
|
+
authorization: "Bearer test-token",
|
|
343
|
+
"content-type": "application/json",
|
|
344
|
+
},
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
sessionKey: "record-session",
|
|
347
|
+
namespace: "team-a",
|
|
348
|
+
tokensBefore: 100,
|
|
349
|
+
tokensAfter: 40,
|
|
350
|
+
}),
|
|
289
351
|
},
|
|
290
|
-
|
|
291
|
-
sessionKey: "record-session",
|
|
292
|
-
namespace: "team-a",
|
|
293
|
-
tokensBefore: 100,
|
|
294
|
-
tokensAfter: 40,
|
|
295
|
-
}),
|
|
296
|
-
});
|
|
352
|
+
);
|
|
297
353
|
assert.equal(response.status, 200);
|
|
298
354
|
assert.deepEqual(await response.json(), {
|
|
299
355
|
enabled: true,
|
|
@@ -310,18 +366,21 @@ test("HTTP LCM compaction record uses the lifecycle route and write quota", asyn
|
|
|
310
366
|
},
|
|
311
367
|
]);
|
|
312
368
|
|
|
313
|
-
const rateLimited = await fetch(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
369
|
+
const rateLimited = await fetch(
|
|
370
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/record`,
|
|
371
|
+
{
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
authorization: "Bearer test-token",
|
|
375
|
+
"content-type": "application/json",
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
sessionKey: "record-session",
|
|
379
|
+
tokensBefore: 120,
|
|
380
|
+
tokensAfter: 60,
|
|
381
|
+
}),
|
|
318
382
|
},
|
|
319
|
-
|
|
320
|
-
sessionKey: "record-session",
|
|
321
|
-
tokensBefore: 120,
|
|
322
|
-
tokensAfter: 60,
|
|
323
|
-
}),
|
|
324
|
-
});
|
|
383
|
+
);
|
|
325
384
|
assert.equal(rateLimited.status, 429);
|
|
326
385
|
assert.equal(calls.length, 1);
|
|
327
386
|
} finally {
|
|
@@ -330,9 +389,16 @@ test("HTTP LCM compaction record uses the lifecycle route and write quota", asyn
|
|
|
330
389
|
});
|
|
331
390
|
|
|
332
391
|
test("HTTP batch LCM flush deduplicates aliases after effective namespace resolution", async () => {
|
|
333
|
-
const calls: Array<{
|
|
392
|
+
const calls: Array<{
|
|
393
|
+
namespace?: string;
|
|
394
|
+
sessionKey: string;
|
|
395
|
+
authenticatedPrincipal?: string;
|
|
396
|
+
}> = [];
|
|
334
397
|
const service = {
|
|
335
|
-
configRef: parseConfig({
|
|
398
|
+
configRef: parseConfig({
|
|
399
|
+
namespacesEnabled: true,
|
|
400
|
+
defaultNamespace: "default",
|
|
401
|
+
}),
|
|
336
402
|
lcmCompactionFlush: async (request: {
|
|
337
403
|
namespace?: string;
|
|
338
404
|
sessionKey: string;
|
|
@@ -351,17 +417,20 @@ test("HTTP batch LCM flush deduplicates aliases after effective namespace resolu
|
|
|
351
417
|
|
|
352
418
|
const status = await server.start();
|
|
353
419
|
try {
|
|
354
|
-
const response = await fetch(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
420
|
+
const response = await fetch(
|
|
421
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/flush`,
|
|
422
|
+
{
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers: {
|
|
425
|
+
authorization: "Bearer test-token",
|
|
426
|
+
"content-type": "application/json",
|
|
427
|
+
},
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
sessionKey: "effective-namespace-session",
|
|
430
|
+
namespaces: ["", "default"],
|
|
431
|
+
}),
|
|
359
432
|
},
|
|
360
|
-
|
|
361
|
-
sessionKey: "effective-namespace-session",
|
|
362
|
-
namespaces: ["", "default"],
|
|
363
|
-
}),
|
|
364
|
-
});
|
|
433
|
+
);
|
|
365
434
|
assert.equal(response.status, 200);
|
|
366
435
|
assert.deepEqual(await response.json(), {
|
|
367
436
|
enabled: true,
|
|
@@ -393,7 +462,6 @@ test("HTTP batch LCM flush deduplicates aliases after effective namespace resolu
|
|
|
393
462
|
}
|
|
394
463
|
});
|
|
395
464
|
|
|
396
|
-
|
|
397
465
|
test("HTTP batch LCM flush isolates per-namespace authorization failures", async () => {
|
|
398
466
|
const calls: string[] = [];
|
|
399
467
|
const service = {
|
|
@@ -421,17 +489,20 @@ test("HTTP batch LCM flush isolates per-namespace authorization failures", async
|
|
|
421
489
|
|
|
422
490
|
const status = await server.start();
|
|
423
491
|
try {
|
|
424
|
-
const response = await fetch(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
492
|
+
const response = await fetch(
|
|
493
|
+
`http://127.0.0.1:${status.port}/engram/v1/lcm/compaction/flush`,
|
|
494
|
+
{
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: {
|
|
497
|
+
authorization: "Bearer scoped-token",
|
|
498
|
+
"content-type": "application/json",
|
|
499
|
+
},
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
sessionKey: "authorized-batch-session",
|
|
502
|
+
namespaces: ["team-a", "team-b"],
|
|
503
|
+
}),
|
|
429
504
|
},
|
|
430
|
-
|
|
431
|
-
sessionKey: "authorized-batch-session",
|
|
432
|
-
namespaces: ["team-a", "team-b"],
|
|
433
|
-
}),
|
|
434
|
-
});
|
|
505
|
+
);
|
|
435
506
|
assert.equal(response.status, 200);
|
|
436
507
|
assert.deepEqual(await response.json(), {
|
|
437
508
|
enabled: false,
|
|
@@ -473,7 +544,7 @@ test("HTTP admin console assets are public but API routes require bearer authent
|
|
|
473
544
|
assert.match(app.headers.get("content-type") ?? "", /javascript/);
|
|
474
545
|
|
|
475
546
|
const api = await fetch(`http://127.0.0.1:${status.port}/engram/v1/health`);
|
|
476
|
-
const body = await api.json() as { code?: string };
|
|
547
|
+
const body = (await api.json()) as { code?: string };
|
|
477
548
|
assert.equal(api.status, 401);
|
|
478
549
|
assert.equal(body.code, "unauthorized");
|
|
479
550
|
assert.equal(api.headers.get("www-authenticate"), "Bearer");
|
|
@@ -493,7 +564,9 @@ test("HTTP admin console can prefill the primary bearer token when explicitly en
|
|
|
493
564
|
|
|
494
565
|
const status = await server.start();
|
|
495
566
|
try {
|
|
496
|
-
const unauthenticatedShell = await fetch(
|
|
567
|
+
const unauthenticatedShell = await fetch(
|
|
568
|
+
`http://127.0.0.1:${status.port}/engram/ui/`,
|
|
569
|
+
);
|
|
497
570
|
assert.equal(unauthenticatedShell.status, 200);
|
|
498
571
|
assert.doesNotMatch(await unauthenticatedShell.text(), /test-token/);
|
|
499
572
|
|
|
@@ -504,10 +577,11 @@ test("HTTP admin console can prefill the primary bearer token when explicitly en
|
|
|
504
577
|
assert.equal(shell.headers.get("cache-control"), "private, no-store");
|
|
505
578
|
assert.equal(shell.headers.get("vary"), "authorization");
|
|
506
579
|
const shellText = await shell.text();
|
|
507
|
-
assert.match(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
);
|
|
580
|
+
assert.match(shellText, /Object\.defineProperty\(window,key/);
|
|
581
|
+
assert.match(shellText, /window\.addEventListener\("pagehide",clear,\{once:true\}\)/);
|
|
582
|
+
assert.match(shellText, /script\.textContent="";script\.remove\(\)/);
|
|
583
|
+
assert.match(shellText, /\}\)\("test-token",document\.currentScript\)/);
|
|
584
|
+
assert.doesNotMatch(shellText, /window\.__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__="test-token"/);
|
|
511
585
|
|
|
512
586
|
const app = await fetch(`http://127.0.0.1:${status.port}/engram/ui/app.js`);
|
|
513
587
|
assert.equal(app.status, 200);
|
|
@@ -517,6 +591,59 @@ test("HTTP admin console can prefill the primary bearer token when explicitly en
|
|
|
517
591
|
}
|
|
518
592
|
});
|
|
519
593
|
|
|
594
|
+
test("HTTP admin console safely serializes a prefill token into its inline bootstrap", async () => {
|
|
595
|
+
const token = 'safe</script><script>window.injected=true</script>';
|
|
596
|
+
const service = {} as EngramAccessService;
|
|
597
|
+
const server = new EngramAccessHttpServer({
|
|
598
|
+
service,
|
|
599
|
+
port: 0,
|
|
600
|
+
authToken: token,
|
|
601
|
+
adminConsolePrefillToken: true,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const status = await server.start();
|
|
605
|
+
try {
|
|
606
|
+
const shell = await fetch(`http://127.0.0.1:${status.port}/engram/ui/`, {
|
|
607
|
+
headers: { authorization: `Bearer ${token}` },
|
|
608
|
+
});
|
|
609
|
+
assert.equal(shell.status, 200);
|
|
610
|
+
const shellText = await shell.text();
|
|
611
|
+
assert.doesNotMatch(shellText, /<script>window\.injected=true<\/script>/);
|
|
612
|
+
assert.match(shellText, /safe\\u003c\/script>\\u003cscript>window\.injected=true\\u003c\/script>/);
|
|
613
|
+
} finally {
|
|
614
|
+
await server.stop();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("HTTP admin console does not disclose the primary token to a dynamic token", async () => {
|
|
619
|
+
const service = {} as EngramAccessService;
|
|
620
|
+
const server = new EngramAccessHttpServer({
|
|
621
|
+
service,
|
|
622
|
+
port: 0,
|
|
623
|
+
authToken: "primary-token",
|
|
624
|
+
authTokenEntriesGetter: () => [
|
|
625
|
+
{
|
|
626
|
+
token: "scoped-token",
|
|
627
|
+
capabilities: { version: 1, ops: ["support_passport_cards_list"] },
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
adminConsolePrefillToken: true,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const status = await server.start();
|
|
634
|
+
try {
|
|
635
|
+
const shell = await fetch(`http://127.0.0.1:${status.port}/engram/ui/`, {
|
|
636
|
+
headers: { authorization: "Bearer scoped-token" },
|
|
637
|
+
});
|
|
638
|
+
assert.equal(shell.status, 200);
|
|
639
|
+
const body = await shell.text();
|
|
640
|
+
assert.doesNotMatch(body, /primary-token/);
|
|
641
|
+
assert.doesNotMatch(body, /__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__/);
|
|
642
|
+
} finally {
|
|
643
|
+
await server.stop();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
520
647
|
test("HTTP admin dashboard endpoints require bearer authentication and apply config patches", async () => {
|
|
521
648
|
const service = {} as EngramAccessService;
|
|
522
649
|
const patches: unknown[] = [];
|
|
@@ -562,27 +689,40 @@ test("HTTP admin dashboard endpoints require bearer authentication and apply con
|
|
|
562
689
|
|
|
563
690
|
const status = await server.start();
|
|
564
691
|
try {
|
|
565
|
-
const unauthorized = await fetch(
|
|
692
|
+
const unauthorized = await fetch(
|
|
693
|
+
`http://127.0.0.1:${status.port}/engram/v1/admin/dashboard`,
|
|
694
|
+
);
|
|
566
695
|
assert.equal(unauthorized.status, 401);
|
|
567
696
|
|
|
568
|
-
const dashboardResponse = await fetch(
|
|
569
|
-
|
|
570
|
-
|
|
697
|
+
const dashboardResponse = await fetch(
|
|
698
|
+
`http://127.0.0.1:${status.port}/engram/v1/admin/dashboard`,
|
|
699
|
+
{
|
|
700
|
+
headers: { authorization: "Bearer test-token" },
|
|
701
|
+
},
|
|
702
|
+
);
|
|
571
703
|
assert.equal(dashboardResponse.status, 200);
|
|
572
704
|
assert.deepEqual(await dashboardResponse.json(), dashboard);
|
|
573
705
|
|
|
574
|
-
const patchResponse = await fetch(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
706
|
+
const patchResponse = await fetch(
|
|
707
|
+
`http://127.0.0.1:${status.port}/remnic/v1/admin/config`,
|
|
708
|
+
{
|
|
709
|
+
method: "PATCH",
|
|
710
|
+
headers: {
|
|
711
|
+
authorization: "Bearer test-token",
|
|
712
|
+
"content-type": "application/json",
|
|
713
|
+
},
|
|
714
|
+
body: JSON.stringify({
|
|
715
|
+
modelSource: "gateway",
|
|
716
|
+
gatewayAgentId: "sage-router",
|
|
717
|
+
}),
|
|
579
718
|
},
|
|
580
|
-
|
|
581
|
-
});
|
|
719
|
+
);
|
|
582
720
|
assert.equal(patchResponse.status, 200);
|
|
583
|
-
const body = await patchResponse.json() as RemnicAdminDashboardStatus;
|
|
721
|
+
const body = (await patchResponse.json()) as RemnicAdminDashboardStatus;
|
|
584
722
|
assert.equal(body.config.restartRequired, true);
|
|
585
|
-
assert.deepEqual(patches, [
|
|
723
|
+
assert.deepEqual(patches, [
|
|
724
|
+
{ modelSource: "gateway", gatewayAgentId: "sage-router" },
|
|
725
|
+
]);
|
|
586
726
|
} finally {
|
|
587
727
|
await server.stop();
|
|
588
728
|
}
|
|
@@ -604,17 +744,20 @@ test("HTTP coding-context endpoint accepts projectTag shorthand", async () => {
|
|
|
604
744
|
|
|
605
745
|
const status = await server.start();
|
|
606
746
|
try {
|
|
607
|
-
const response = await fetch(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
747
|
+
const response = await fetch(
|
|
748
|
+
`http://127.0.0.1:${status.port}/engram/v1/coding-context`,
|
|
749
|
+
{
|
|
750
|
+
method: "POST",
|
|
751
|
+
headers: {
|
|
752
|
+
authorization: "Bearer test-token",
|
|
753
|
+
"content-type": "application/json",
|
|
754
|
+
},
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
sessionKey: "s1",
|
|
757
|
+
projectTag: "Acme/Webshop",
|
|
758
|
+
}),
|
|
612
759
|
},
|
|
613
|
-
|
|
614
|
-
sessionKey: "s1",
|
|
615
|
-
projectTag: "Acme/Webshop",
|
|
616
|
-
}),
|
|
617
|
-
});
|
|
760
|
+
);
|
|
618
761
|
|
|
619
762
|
const projectId = projectTagProjectId("Acme/Webshop");
|
|
620
763
|
assert.equal(response.status, 200);
|
|
@@ -636,7 +779,10 @@ test("HTTP coding-context endpoint accepts projectTag shorthand", async () => {
|
|
|
636
779
|
});
|
|
637
780
|
|
|
638
781
|
test("HTTP contradiction scan uses writable namespace resolver", async () => {
|
|
639
|
-
const resolverCalls: Array<{
|
|
782
|
+
const resolverCalls: Array<{
|
|
783
|
+
namespace: string | undefined;
|
|
784
|
+
principal: string | undefined;
|
|
785
|
+
}> = [];
|
|
640
786
|
const storage = {
|
|
641
787
|
readAllMemories: async () => [],
|
|
642
788
|
} as unknown as StorageManager;
|
|
@@ -656,9 +802,14 @@ test("HTTP contradiction scan uses writable namespace resolver", async () => {
|
|
|
656
802
|
localLlmRef: null,
|
|
657
803
|
fallbackLlmRef: null,
|
|
658
804
|
getReadableStorageForNamespace: async () => {
|
|
659
|
-
throw new Error(
|
|
805
|
+
throw new Error(
|
|
806
|
+
"readable resolver must not authorize contradiction scan writes",
|
|
807
|
+
);
|
|
660
808
|
},
|
|
661
|
-
getWritableStorageForNamespace: async (
|
|
809
|
+
getWritableStorageForNamespace: async (
|
|
810
|
+
namespace: string | undefined,
|
|
811
|
+
principal: string | undefined,
|
|
812
|
+
) => {
|
|
662
813
|
resolverCalls.push({ namespace, principal });
|
|
663
814
|
return { namespace: namespace ?? "default", storage };
|
|
664
815
|
},
|
|
@@ -673,19 +824,24 @@ test("HTTP contradiction scan uses writable namespace resolver", async () => {
|
|
|
673
824
|
|
|
674
825
|
const status = await server.start();
|
|
675
826
|
try {
|
|
676
|
-
const response = await fetch(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
827
|
+
const response = await fetch(
|
|
828
|
+
`http://127.0.0.1:${status.port}/engram/v1/contradiction-scan`,
|
|
829
|
+
{
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: {
|
|
832
|
+
authorization: "Bearer test-token",
|
|
833
|
+
"content-type": "application/json",
|
|
834
|
+
},
|
|
835
|
+
body: JSON.stringify({ namespace: "team" }),
|
|
681
836
|
},
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
const body = await response.json() as { scanned?: number };
|
|
837
|
+
);
|
|
838
|
+
const body = (await response.json()) as { scanned?: number };
|
|
685
839
|
|
|
686
840
|
assert.equal(response.status, 200);
|
|
687
841
|
assert.equal(body.scanned, 0);
|
|
688
|
-
assert.deepEqual(resolverCalls, [
|
|
842
|
+
assert.deepEqual(resolverCalls, [
|
|
843
|
+
{ namespace: "team", principal: "writer" },
|
|
844
|
+
]);
|
|
689
845
|
} finally {
|
|
690
846
|
await server.stop();
|
|
691
847
|
}
|
|
@@ -693,7 +849,10 @@ test("HTTP contradiction scan uses writable namespace resolver", async () => {
|
|
|
693
849
|
|
|
694
850
|
test("HTTP review list uses readable namespace resolver", async () => {
|
|
695
851
|
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-http-review-list-"));
|
|
696
|
-
const resolverCalls: Array<{
|
|
852
|
+
const resolverCalls: Array<{
|
|
853
|
+
namespace: string | undefined;
|
|
854
|
+
principal: string | undefined;
|
|
855
|
+
}> = [];
|
|
697
856
|
const service = {
|
|
698
857
|
configRef: parseConfig({
|
|
699
858
|
memoryDir: dir,
|
|
@@ -701,9 +860,14 @@ test("HTTP review list uses readable namespace resolver", async () => {
|
|
|
701
860
|
defaultNamespace: "default",
|
|
702
861
|
}),
|
|
703
862
|
memoryDir: dir,
|
|
704
|
-
getReadableStorageForNamespace: async (
|
|
863
|
+
getReadableStorageForNamespace: async (
|
|
864
|
+
namespace: string | undefined,
|
|
865
|
+
principal: string | undefined,
|
|
866
|
+
) => {
|
|
705
867
|
resolverCalls.push({ namespace, principal });
|
|
706
|
-
throw new EngramAccessInputError(
|
|
868
|
+
throw new EngramAccessInputError(
|
|
869
|
+
`namespace is not readable: ${namespace}`,
|
|
870
|
+
);
|
|
707
871
|
},
|
|
708
872
|
} as unknown as EngramAccessService;
|
|
709
873
|
const server = new EngramAccessHttpServer({
|
|
@@ -716,14 +880,19 @@ test("HTTP review list uses readable namespace resolver", async () => {
|
|
|
716
880
|
|
|
717
881
|
const status = await server.start();
|
|
718
882
|
try {
|
|
719
|
-
const response = await fetch(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
883
|
+
const response = await fetch(
|
|
884
|
+
`http://127.0.0.1:${status.port}/engram/v1/review/contradictions?namespace=team`,
|
|
885
|
+
{
|
|
886
|
+
headers: { authorization: "Bearer test-token" },
|
|
887
|
+
},
|
|
888
|
+
);
|
|
889
|
+
const body = (await response.json()) as { error?: string };
|
|
723
890
|
|
|
724
891
|
assert.equal(response.status, 400);
|
|
725
892
|
assert.match(body.error ?? "", /namespace is not readable: team/);
|
|
726
|
-
assert.deepEqual(resolverCalls, [
|
|
893
|
+
assert.deepEqual(resolverCalls, [
|
|
894
|
+
{ namespace: "team", principal: "reader" },
|
|
895
|
+
]);
|
|
727
896
|
} finally {
|
|
728
897
|
await server.stop();
|
|
729
898
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -731,7 +900,9 @@ test("HTTP review list uses readable namespace resolver", async () => {
|
|
|
731
900
|
});
|
|
732
901
|
|
|
733
902
|
test("HTTP default review list includes legacy unscoped pairs without mutating storage", async () => {
|
|
734
|
-
const dir = await mkdtemp(
|
|
903
|
+
const dir = await mkdtemp(
|
|
904
|
+
path.join(os.tmpdir(), "remnic-http-review-list-default-"),
|
|
905
|
+
);
|
|
735
906
|
const legacy = writePair(dir, {
|
|
736
907
|
memoryIds: ["legacy-a", "legacy-b"],
|
|
737
908
|
verdict: "contradicts",
|
|
@@ -739,7 +910,10 @@ test("HTTP default review list includes legacy unscoped pairs without mutating s
|
|
|
739
910
|
confidence: 0.9,
|
|
740
911
|
detectedAt: new Date().toISOString(),
|
|
741
912
|
});
|
|
742
|
-
const resolverCalls: Array<{
|
|
913
|
+
const resolverCalls: Array<{
|
|
914
|
+
namespace: string | undefined;
|
|
915
|
+
principal: string | undefined;
|
|
916
|
+
}> = [];
|
|
743
917
|
const storage = {
|
|
744
918
|
readAllMemories: async () => [],
|
|
745
919
|
} as unknown as StorageManager;
|
|
@@ -750,7 +924,10 @@ test("HTTP default review list includes legacy unscoped pairs without mutating s
|
|
|
750
924
|
defaultNamespace: "default",
|
|
751
925
|
}),
|
|
752
926
|
memoryDir: dir,
|
|
753
|
-
getReadableStorageForNamespace: async (
|
|
927
|
+
getReadableStorageForNamespace: async (
|
|
928
|
+
namespace: string | undefined,
|
|
929
|
+
principal: string | undefined,
|
|
930
|
+
) => {
|
|
754
931
|
resolverCalls.push({ namespace, principal });
|
|
755
932
|
return { namespace: namespace ?? "default", storage };
|
|
756
933
|
},
|
|
@@ -765,10 +942,13 @@ test("HTTP default review list includes legacy unscoped pairs without mutating s
|
|
|
765
942
|
|
|
766
943
|
const status = await server.start();
|
|
767
944
|
try {
|
|
768
|
-
const response = await fetch(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
945
|
+
const response = await fetch(
|
|
946
|
+
`http://127.0.0.1:${status.port}/engram/v1/review/contradictions`,
|
|
947
|
+
{
|
|
948
|
+
headers: { authorization: "Bearer test-token" },
|
|
949
|
+
},
|
|
950
|
+
);
|
|
951
|
+
const body = (await response.json()) as {
|
|
772
952
|
total?: number;
|
|
773
953
|
pairs?: Array<{ pairId?: string; namespace?: string }>;
|
|
774
954
|
};
|
|
@@ -777,7 +957,9 @@ test("HTTP default review list includes legacy unscoped pairs without mutating s
|
|
|
777
957
|
assert.equal(body.pairs?.[0]?.pairId, legacy.pairId);
|
|
778
958
|
assert.equal(body.pairs?.[0]?.namespace, undefined);
|
|
779
959
|
assert.equal(readPair(dir, legacy.pairId)?.namespace, undefined);
|
|
780
|
-
assert.deepEqual(resolverCalls, [
|
|
960
|
+
assert.deepEqual(resolverCalls, [
|
|
961
|
+
{ namespace: undefined, principal: "reader" },
|
|
962
|
+
]);
|
|
781
963
|
} finally {
|
|
782
964
|
await server.stop();
|
|
783
965
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -794,7 +976,10 @@ test("HTTP review show hides namespace denial as pair_not_found", async () => {
|
|
|
794
976
|
confidence: 0.9,
|
|
795
977
|
detectedAt: new Date().toISOString(),
|
|
796
978
|
});
|
|
797
|
-
const resolverCalls: Array<{
|
|
979
|
+
const resolverCalls: Array<{
|
|
980
|
+
namespace: string | undefined;
|
|
981
|
+
principal: string | undefined;
|
|
982
|
+
}> = [];
|
|
798
983
|
const service = {
|
|
799
984
|
configRef: parseConfig({
|
|
800
985
|
memoryDir: dir,
|
|
@@ -802,9 +987,14 @@ test("HTTP review show hides namespace denial as pair_not_found", async () => {
|
|
|
802
987
|
defaultNamespace: "default",
|
|
803
988
|
}),
|
|
804
989
|
memoryDir: dir,
|
|
805
|
-
getReadableStorageForNamespace: async (
|
|
990
|
+
getReadableStorageForNamespace: async (
|
|
991
|
+
namespace: string | undefined,
|
|
992
|
+
principal: string | undefined,
|
|
993
|
+
) => {
|
|
806
994
|
resolverCalls.push({ namespace, principal });
|
|
807
|
-
throw new EngramAccessInputError(
|
|
995
|
+
throw new EngramAccessInputError(
|
|
996
|
+
`namespace is not readable: ${namespace}`,
|
|
997
|
+
);
|
|
808
998
|
},
|
|
809
999
|
} as unknown as EngramAccessService;
|
|
810
1000
|
const server = new EngramAccessHttpServer({
|
|
@@ -817,14 +1007,19 @@ test("HTTP review show hides namespace denial as pair_not_found", async () => {
|
|
|
817
1007
|
|
|
818
1008
|
const status = await server.start();
|
|
819
1009
|
try {
|
|
820
|
-
const response = await fetch(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1010
|
+
const response = await fetch(
|
|
1011
|
+
`http://127.0.0.1:${status.port}/engram/v1/review/contradictions/${pair.pairId}`,
|
|
1012
|
+
{
|
|
1013
|
+
headers: { authorization: "Bearer test-token" },
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
const body = (await response.json()) as { error?: string };
|
|
824
1017
|
|
|
825
1018
|
assert.equal(response.status, 404);
|
|
826
1019
|
assert.equal(body.error, "pair_not_found");
|
|
827
|
-
assert.deepEqual(resolverCalls, [
|
|
1020
|
+
assert.deepEqual(resolverCalls, [
|
|
1021
|
+
{ namespace: "team", principal: "reader" },
|
|
1022
|
+
]);
|
|
828
1023
|
} finally {
|
|
829
1024
|
await server.stop();
|
|
830
1025
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -877,7 +1072,7 @@ test("HTTP offline snapshot forwards namespace and transfer options", async () =
|
|
|
877
1072
|
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/snapshot?namespace=team&include_transcripts=false&content=false`,
|
|
878
1073
|
{ headers: { authorization: "Bearer test-token" } },
|
|
879
1074
|
);
|
|
880
|
-
const body = await response.json() as {
|
|
1075
|
+
const body = (await response.json()) as {
|
|
881
1076
|
namespace?: string;
|
|
882
1077
|
includeTranscripts?: boolean;
|
|
883
1078
|
files?: unknown[];
|
|
@@ -888,13 +1083,17 @@ test("HTTP offline snapshot forwards namespace and transfer options", async () =
|
|
|
888
1083
|
assert.equal(body.namespace, "team");
|
|
889
1084
|
assert.equal(body.includeTranscripts, false);
|
|
890
1085
|
assert.deepEqual(body.files, []);
|
|
891
|
-
assert.deepEqual(body.deletions, [
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1086
|
+
assert.deepEqual(body.deletions, [
|
|
1087
|
+
{ path: "facts/deleted.md", mtimeMs: 1235 },
|
|
1088
|
+
]);
|
|
1089
|
+
assert.deepEqual(calls, [
|
|
1090
|
+
{
|
|
1091
|
+
namespace: "team",
|
|
1092
|
+
principal: "reader",
|
|
1093
|
+
includeTranscripts: false,
|
|
1094
|
+
includeContent: false,
|
|
1095
|
+
},
|
|
1096
|
+
]);
|
|
898
1097
|
} finally {
|
|
899
1098
|
await server.stop();
|
|
900
1099
|
}
|
|
@@ -947,18 +1146,22 @@ test("HTTP offline snapshot accepts gzipped fast-base bodies", async () => {
|
|
|
947
1146
|
|
|
948
1147
|
const status = await server.start();
|
|
949
1148
|
try {
|
|
950
|
-
const body = gzipSync(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1149
|
+
const body = gzipSync(
|
|
1150
|
+
JSON.stringify({
|
|
1151
|
+
namespace: "team",
|
|
1152
|
+
includeTranscripts: false,
|
|
1153
|
+
includeContent: false,
|
|
1154
|
+
baseCapturedAt: "2026-05-20T00:00:00.000Z",
|
|
1155
|
+
baseFiles: [
|
|
1156
|
+
{
|
|
1157
|
+
path: "facts/a.md",
|
|
1158
|
+
sha256: "a".repeat(64),
|
|
1159
|
+
bytes: 12,
|
|
1160
|
+
mtimeMs: 1234,
|
|
1161
|
+
},
|
|
1162
|
+
],
|
|
1163
|
+
}),
|
|
1164
|
+
);
|
|
962
1165
|
const response = await fetch(
|
|
963
1166
|
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/snapshot`,
|
|
964
1167
|
{
|
|
@@ -971,7 +1174,7 @@ test("HTTP offline snapshot accepts gzipped fast-base bodies", async () => {
|
|
|
971
1174
|
body,
|
|
972
1175
|
},
|
|
973
1176
|
);
|
|
974
|
-
const responseBody = await response.json() as { namespace?: string };
|
|
1177
|
+
const responseBody = (await response.json()) as { namespace?: string };
|
|
975
1178
|
|
|
976
1179
|
assert.equal(response.status, 200);
|
|
977
1180
|
assert.equal(responseBody.namespace, "team");
|
|
@@ -983,12 +1186,14 @@ test("HTTP offline snapshot accepts gzipped fast-base bodies", async () => {
|
|
|
983
1186
|
principal: "reader",
|
|
984
1187
|
includeTranscripts: false,
|
|
985
1188
|
includeContent: false,
|
|
986
|
-
baseFiles: [
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1189
|
+
baseFiles: [
|
|
1190
|
+
{
|
|
1191
|
+
path: "facts/a.md",
|
|
1192
|
+
sha256: "a".repeat(64),
|
|
1193
|
+
bytes: 12,
|
|
1194
|
+
mtimeMs: 1234,
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
992
1197
|
baseCapturedAt: new Date("2026-05-20T00:00:00.000Z"),
|
|
993
1198
|
});
|
|
994
1199
|
} finally {
|
|
@@ -1055,18 +1260,25 @@ test("HTTP offline snapshot stream emits metadata records as NDJSON", async () =
|
|
|
1055
1260
|
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
1056
1261
|
|
|
1057
1262
|
assert.equal(response.status, 200);
|
|
1058
|
-
assert.equal(
|
|
1263
|
+
assert.equal(
|
|
1264
|
+
response.headers.get("content-type"),
|
|
1265
|
+
"application/x-ndjson; charset=utf-8",
|
|
1266
|
+
);
|
|
1059
1267
|
assert.equal(lines[0]?.type, "snapshot");
|
|
1060
1268
|
assert.equal(lines[0]?.namespace, "team");
|
|
1061
|
-
assert.deepEqual(lines[0]?.deletions, [
|
|
1269
|
+
assert.deepEqual(lines[0]?.deletions, [
|
|
1270
|
+
{ path: "facts/deleted.md", mtimeMs: 1235 },
|
|
1271
|
+
]);
|
|
1062
1272
|
assert.equal(lines[1]?.type, "file");
|
|
1063
1273
|
assert.deepEqual((lines[1]?.file as { path?: string }).path, "facts/a.md");
|
|
1064
|
-
assert.deepEqual(calls, [
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1274
|
+
assert.deepEqual(calls, [
|
|
1275
|
+
{
|
|
1276
|
+
namespace: "team",
|
|
1277
|
+
principal: "reader",
|
|
1278
|
+
includeTranscripts: false,
|
|
1279
|
+
includeContent: false,
|
|
1280
|
+
},
|
|
1281
|
+
]);
|
|
1070
1282
|
} finally {
|
|
1071
1283
|
await server.stop();
|
|
1072
1284
|
}
|
|
@@ -1102,36 +1314,66 @@ test("HTTP offline manifest stream preserves snapshot-stream auth and emits body
|
|
|
1102
1314
|
port: 0,
|
|
1103
1315
|
principal: "reader",
|
|
1104
1316
|
authTokenEntriesGetter: () => [
|
|
1105
|
-
{
|
|
1317
|
+
{
|
|
1318
|
+
token: "wrong-op",
|
|
1319
|
+
capabilities: { version: 1, ops: ["offline_sync_snapshot"] },
|
|
1320
|
+
},
|
|
1106
1321
|
{
|
|
1107
1322
|
token: "wrong-namespace",
|
|
1108
|
-
capabilities: {
|
|
1323
|
+
capabilities: {
|
|
1324
|
+
version: 1,
|
|
1325
|
+
ops: ["offline_sync_snapshot_stream"],
|
|
1326
|
+
namespaces: ["other"],
|
|
1327
|
+
},
|
|
1109
1328
|
},
|
|
1110
1329
|
{
|
|
1111
1330
|
token: "reader",
|
|
1112
|
-
capabilities: {
|
|
1331
|
+
capabilities: {
|
|
1332
|
+
version: 1,
|
|
1333
|
+
ops: ["offline_sync_snapshot_stream"],
|
|
1334
|
+
namespaces: ["team"],
|
|
1335
|
+
},
|
|
1113
1336
|
},
|
|
1114
1337
|
],
|
|
1115
1338
|
adminConsoleEnabled: false,
|
|
1116
1339
|
});
|
|
1117
1340
|
const status = await server.start();
|
|
1118
|
-
const request = (
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1341
|
+
const request = (
|
|
1342
|
+
token: string,
|
|
1343
|
+
query = "namespace=team&include_transcripts=false",
|
|
1344
|
+
) =>
|
|
1345
|
+
fetch(
|
|
1346
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/manifest-stream?${query}`,
|
|
1347
|
+
{
|
|
1348
|
+
headers: { authorization: `Bearer ${token}` },
|
|
1349
|
+
},
|
|
1350
|
+
);
|
|
1122
1351
|
try {
|
|
1123
1352
|
assert.equal((await request("wrong-op")).status, 403);
|
|
1124
1353
|
assert.equal((await request("wrong-namespace")).status, 403);
|
|
1125
1354
|
assert.equal(calls.length, 0);
|
|
1126
|
-
assert.equal(
|
|
1127
|
-
|
|
1355
|
+
assert.equal(
|
|
1356
|
+
(await request("reader", "namespace=team&include_transcripts=invalid"))
|
|
1357
|
+
.status,
|
|
1358
|
+
400,
|
|
1359
|
+
);
|
|
1360
|
+
assert.equal(
|
|
1361
|
+
(await request("reader", "namespace=team&content=true")).status,
|
|
1362
|
+
400,
|
|
1363
|
+
);
|
|
1128
1364
|
assert.equal(calls.length, 0);
|
|
1129
1365
|
|
|
1130
1366
|
const response = await request("reader");
|
|
1131
1367
|
const text = await response.text();
|
|
1132
|
-
const lines = text
|
|
1368
|
+
const lines = text
|
|
1369
|
+
.trim()
|
|
1370
|
+
.split("\n")
|
|
1371
|
+
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
1133
1372
|
assert.equal(response.status, 200);
|
|
1134
|
-
assert.equal(
|
|
1373
|
+
assert.equal(
|
|
1374
|
+
response.headers.get("content-type"),
|
|
1375
|
+
"application/x-ndjson; charset=utf-8",
|
|
1376
|
+
);
|
|
1135
1377
|
assert.equal(lines[0]?.type, "manifest");
|
|
1136
1378
|
assert.equal(lines[1]?.type, "file");
|
|
1137
1379
|
assert.deepEqual(lines[1]?.file, {
|
|
@@ -1147,15 +1389,18 @@ test("HTTP offline manifest stream preserves snapshot-stream auth and emits body
|
|
|
1147
1389
|
},
|
|
1148
1390
|
});
|
|
1149
1391
|
assert.doesNotMatch(text, /contentBase64|rawContent|memory body/);
|
|
1150
|
-
assert.deepEqual(
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1392
|
+
assert.deepEqual(
|
|
1393
|
+
{
|
|
1394
|
+
namespace: calls[0]?.namespace,
|
|
1395
|
+
principal: calls[0]?.principal,
|
|
1396
|
+
includeTranscripts: calls[0]?.includeTranscripts,
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
namespace: "team",
|
|
1400
|
+
principal: "reader",
|
|
1401
|
+
includeTranscripts: false,
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1159
1404
|
} finally {
|
|
1160
1405
|
await server.stop();
|
|
1161
1406
|
}
|
|
@@ -1185,7 +1430,6 @@ test("HTTP offline sync capabilities advertise manifest streaming", async () =>
|
|
|
1185
1430
|
}
|
|
1186
1431
|
});
|
|
1187
1432
|
|
|
1188
|
-
|
|
1189
1433
|
test("HTTP offline files forwards namespace and requested paths", async () => {
|
|
1190
1434
|
const calls: Array<{
|
|
1191
1435
|
namespace: string | undefined;
|
|
@@ -1242,18 +1486,24 @@ test("HTTP offline files forwards namespace and requested paths", async () => {
|
|
|
1242
1486
|
}),
|
|
1243
1487
|
},
|
|
1244
1488
|
);
|
|
1245
|
-
const body = await response.json() as {
|
|
1489
|
+
const body = (await response.json()) as {
|
|
1490
|
+
namespace?: string;
|
|
1491
|
+
includeTranscripts?: boolean;
|
|
1492
|
+
files?: unknown[];
|
|
1493
|
+
};
|
|
1246
1494
|
|
|
1247
1495
|
assert.equal(response.status, 200);
|
|
1248
1496
|
assert.equal(body.namespace, "team");
|
|
1249
1497
|
assert.equal(body.includeTranscripts, false);
|
|
1250
1498
|
assert.deepEqual(body.files, []);
|
|
1251
|
-
assert.deepEqual(calls, [
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1499
|
+
assert.deepEqual(calls, [
|
|
1500
|
+
{
|
|
1501
|
+
namespace: "team",
|
|
1502
|
+
principal: "reader",
|
|
1503
|
+
includeTranscripts: false,
|
|
1504
|
+
paths: ["facts/a.md"],
|
|
1505
|
+
},
|
|
1506
|
+
]);
|
|
1257
1507
|
} finally {
|
|
1258
1508
|
await server.stop();
|
|
1259
1509
|
}
|
|
@@ -1328,22 +1578,30 @@ test("HTTP offline file-content forwards range options and returns binary metada
|
|
|
1328
1578
|
|
|
1329
1579
|
assert.equal(response.status, 200);
|
|
1330
1580
|
assert.equal(body.toString("utf-8"), "hello");
|
|
1331
|
-
assert.equal(
|
|
1581
|
+
assert.equal(
|
|
1582
|
+
response.headers.get("content-type"),
|
|
1583
|
+
"application/octet-stream",
|
|
1584
|
+
);
|
|
1332
1585
|
assert.equal(response.headers.get("x-remnic-namespace"), "team");
|
|
1333
|
-
assert.equal(
|
|
1586
|
+
assert.equal(
|
|
1587
|
+
response.headers.get("x-remnic-file-path"),
|
|
1588
|
+
"artifacts%2Flarge.txt",
|
|
1589
|
+
);
|
|
1334
1590
|
assert.equal(response.headers.get("x-remnic-file-sha256"), "a".repeat(64));
|
|
1335
1591
|
assert.equal(response.headers.get("x-remnic-file-bytes"), "12");
|
|
1336
1592
|
assert.equal(response.headers.get("x-remnic-file-mtime-ms"), "1234");
|
|
1337
1593
|
assert.equal(response.headers.get("x-remnic-chunk-offset"), "8");
|
|
1338
1594
|
assert.equal(response.headers.get("x-remnic-chunk-bytes"), "5");
|
|
1339
|
-
assert.deepEqual(calls, [
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1595
|
+
assert.deepEqual(calls, [
|
|
1596
|
+
{
|
|
1597
|
+
namespace: "team",
|
|
1598
|
+
principal: "reader",
|
|
1599
|
+
includeTranscripts: false,
|
|
1600
|
+
path: "artifacts/large.txt",
|
|
1601
|
+
offset: 8,
|
|
1602
|
+
length: 5,
|
|
1603
|
+
},
|
|
1604
|
+
]);
|
|
1347
1605
|
} finally {
|
|
1348
1606
|
await server.stop();
|
|
1349
1607
|
}
|
|
@@ -1433,25 +1691,31 @@ test("HTTP offline apply-file-content forwards binary chunks and metadata", asyn
|
|
|
1433
1691
|
body: Buffer.from("hello"),
|
|
1434
1692
|
},
|
|
1435
1693
|
);
|
|
1436
|
-
const body = await response.json() as {
|
|
1694
|
+
const body = (await response.json()) as {
|
|
1695
|
+
namespace?: string;
|
|
1696
|
+
applied?: boolean;
|
|
1697
|
+
chunkBytes?: number;
|
|
1698
|
+
};
|
|
1437
1699
|
|
|
1438
1700
|
assert.equal(response.status, 200);
|
|
1439
1701
|
assert.equal(body.namespace, "team");
|
|
1440
1702
|
assert.equal(body.applied, true);
|
|
1441
1703
|
assert.equal(body.chunkBytes, 5);
|
|
1442
|
-
assert.deepEqual(calls, [
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1704
|
+
assert.deepEqual(calls, [
|
|
1705
|
+
{
|
|
1706
|
+
namespace: "team",
|
|
1707
|
+
principal: "writer",
|
|
1708
|
+
includeTranscripts: false,
|
|
1709
|
+
sourceId: "laptop:test",
|
|
1710
|
+
path: "state/lcm.sqlite",
|
|
1711
|
+
sha256: "b".repeat(64),
|
|
1712
|
+
bytes: 5,
|
|
1713
|
+
mtimeMs: 1234,
|
|
1714
|
+
offset: 8,
|
|
1715
|
+
baseSha256: "a".repeat(64),
|
|
1716
|
+
content: "hello",
|
|
1717
|
+
},
|
|
1718
|
+
]);
|
|
1455
1719
|
} finally {
|
|
1456
1720
|
await server.stop();
|
|
1457
1721
|
}
|
|
@@ -1541,7 +1805,12 @@ test("HTTP offline snapshot accepts baseline metadata for fast sync", async () =
|
|
|
1541
1805
|
includeTranscripts?: boolean;
|
|
1542
1806
|
includeContent?: boolean;
|
|
1543
1807
|
baseCapturedAt?: Date;
|
|
1544
|
-
baseFiles?: Array<{
|
|
1808
|
+
baseFiles?: Array<{
|
|
1809
|
+
path: string;
|
|
1810
|
+
sha256: string;
|
|
1811
|
+
bytes: number;
|
|
1812
|
+
mtimeMs: number;
|
|
1813
|
+
}>;
|
|
1545
1814
|
}) => {
|
|
1546
1815
|
calls.push({
|
|
1547
1816
|
namespace: options.namespace,
|
|
@@ -1585,28 +1854,35 @@ test("HTTP offline snapshot accepts baseline metadata for fast sync", async () =
|
|
|
1585
1854
|
includeTranscripts: false,
|
|
1586
1855
|
includeContent: false,
|
|
1587
1856
|
baseCapturedAt: "2026-05-31T17:30:08.350Z",
|
|
1588
|
-
baseFiles: [
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1857
|
+
baseFiles: [
|
|
1858
|
+
{
|
|
1859
|
+
path: "facts/a.md",
|
|
1860
|
+
sha256: "a".repeat(64),
|
|
1861
|
+
bytes: 12,
|
|
1862
|
+
mtimeMs: 1234,
|
|
1863
|
+
},
|
|
1864
|
+
],
|
|
1594
1865
|
}),
|
|
1595
1866
|
},
|
|
1596
1867
|
);
|
|
1597
|
-
const body = await response.json() as {
|
|
1868
|
+
const body = (await response.json()) as {
|
|
1869
|
+
namespace?: string;
|
|
1870
|
+
files?: unknown[];
|
|
1871
|
+
};
|
|
1598
1872
|
|
|
1599
1873
|
assert.equal(response.status, 200);
|
|
1600
1874
|
assert.equal(body.namespace, "team");
|
|
1601
1875
|
assert.equal(body.files?.length, 1);
|
|
1602
|
-
assert.deepEqual(calls, [
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1876
|
+
assert.deepEqual(calls, [
|
|
1877
|
+
{
|
|
1878
|
+
namespace: "team",
|
|
1879
|
+
principal: "reader",
|
|
1880
|
+
includeTranscripts: false,
|
|
1881
|
+
includeContent: false,
|
|
1882
|
+
baseCapturedAt: "2026-05-31T17:30:08.350Z",
|
|
1883
|
+
baseFileCount: 1,
|
|
1884
|
+
},
|
|
1885
|
+
]);
|
|
1610
1886
|
} finally {
|
|
1611
1887
|
await server.stop();
|
|
1612
1888
|
}
|
|
@@ -1639,16 +1915,21 @@ test("HTTP offline snapshot rejects unsafe baseline paths as validation errors",
|
|
|
1639
1915
|
"content-type": "application/json",
|
|
1640
1916
|
},
|
|
1641
1917
|
body: JSON.stringify({
|
|
1642
|
-
baseFiles: [
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1918
|
+
baseFiles: [
|
|
1919
|
+
{
|
|
1920
|
+
path: "../outside.md",
|
|
1921
|
+
sha256: "a".repeat(64),
|
|
1922
|
+
bytes: 12,
|
|
1923
|
+
mtimeMs: 1234,
|
|
1924
|
+
},
|
|
1925
|
+
],
|
|
1648
1926
|
}),
|
|
1649
1927
|
},
|
|
1650
1928
|
);
|
|
1651
|
-
const body = await response.json() as {
|
|
1929
|
+
const body = (await response.json()) as {
|
|
1930
|
+
code?: string;
|
|
1931
|
+
details?: Array<{ field?: string; message?: string }>;
|
|
1932
|
+
};
|
|
1652
1933
|
|
|
1653
1934
|
assert.equal(response.status, 400);
|
|
1654
1935
|
assert.equal(body.code, "validation_error");
|
|
@@ -1687,16 +1968,21 @@ test("HTTP offline snapshot rejects out-of-range baseline mtimes as validation e
|
|
|
1687
1968
|
"content-type": "application/json",
|
|
1688
1969
|
},
|
|
1689
1970
|
body: JSON.stringify({
|
|
1690
|
-
baseFiles: [
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1971
|
+
baseFiles: [
|
|
1972
|
+
{
|
|
1973
|
+
path: "facts/a.md",
|
|
1974
|
+
sha256: "a".repeat(64),
|
|
1975
|
+
bytes: 12,
|
|
1976
|
+
mtimeMs: OFFLINE_SYNC_MAX_MTIME_MS + 1,
|
|
1977
|
+
},
|
|
1978
|
+
],
|
|
1696
1979
|
}),
|
|
1697
1980
|
},
|
|
1698
1981
|
);
|
|
1699
|
-
const body = await response.json() as {
|
|
1982
|
+
const body = (await response.json()) as {
|
|
1983
|
+
code?: string;
|
|
1984
|
+
details?: Array<{ field?: string; message?: string }>;
|
|
1985
|
+
};
|
|
1700
1986
|
|
|
1701
1987
|
assert.equal(response.status, 400);
|
|
1702
1988
|
assert.equal(body.code, "validation_error");
|
|
@@ -1730,10 +2016,13 @@ test("HTTP offline snapshot rejects invalid boolean query values", async () => {
|
|
|
1730
2016
|
`http://127.0.0.1:${status.port}/engram/v1/offline-sync/snapshot?include_transcripts=maybe`,
|
|
1731
2017
|
{ headers: { authorization: "Bearer test-token" } },
|
|
1732
2018
|
);
|
|
1733
|
-
const body = await response.json() as { error?: string; code?: string };
|
|
2019
|
+
const body = (await response.json()) as { error?: string; code?: string };
|
|
1734
2020
|
|
|
1735
2021
|
assert.equal(response.status, 400);
|
|
1736
|
-
assert.match(
|
|
2022
|
+
assert.match(
|
|
2023
|
+
body.error ?? "",
|
|
2024
|
+
/include_transcripts must be one of: true, false/,
|
|
2025
|
+
);
|
|
1737
2026
|
assert.equal(body.code, "input_error");
|
|
1738
2027
|
assert.equal(calls, 0);
|
|
1739
2028
|
} finally {
|
|
@@ -1786,24 +2075,32 @@ test("HTTP offline apply validates and forwards changesets", async () => {
|
|
|
1786
2075
|
|
|
1787
2076
|
const status = await server.start();
|
|
1788
2077
|
try {
|
|
1789
|
-
const response = await fetch(
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2078
|
+
const response = await fetch(
|
|
2079
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply`,
|
|
2080
|
+
{
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: {
|
|
2083
|
+
authorization: "Bearer test-token",
|
|
2084
|
+
"content-type": "application/json",
|
|
2085
|
+
},
|
|
2086
|
+
body: JSON.stringify({ namespace: "team", changeset }),
|
|
1794
2087
|
},
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2088
|
+
);
|
|
2089
|
+
const body = (await response.json()) as {
|
|
2090
|
+
namespace?: string;
|
|
2091
|
+
appliedUpserts?: number;
|
|
2092
|
+
};
|
|
1798
2093
|
|
|
1799
2094
|
assert.equal(response.status, 200);
|
|
1800
2095
|
assert.equal(body.namespace, "team");
|
|
1801
2096
|
assert.equal(body.appliedUpserts, 0);
|
|
1802
|
-
assert.deepEqual(calls, [
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2097
|
+
assert.deepEqual(calls, [
|
|
2098
|
+
{
|
|
2099
|
+
namespace: "team",
|
|
2100
|
+
principal: "writer",
|
|
2101
|
+
changeset,
|
|
2102
|
+
},
|
|
2103
|
+
]);
|
|
1807
2104
|
} finally {
|
|
1808
2105
|
await server.stop();
|
|
1809
2106
|
}
|
|
@@ -1818,17 +2115,19 @@ test("HTTP offline apply accepts bulk changesets above the generic JSON body lim
|
|
|
1818
2115
|
createdAt: new Date("2026-05-21T00:00:00Z").toISOString(),
|
|
1819
2116
|
sourceId: "laptop:test",
|
|
1820
2117
|
includeTranscripts: true,
|
|
1821
|
-
changes: [
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
file: {
|
|
2118
|
+
changes: [
|
|
2119
|
+
{
|
|
2120
|
+
type: "upsert",
|
|
1825
2121
|
path: "facts/large.md",
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2122
|
+
file: {
|
|
2123
|
+
path: "facts/large.md",
|
|
2124
|
+
sha256: "b".repeat(64),
|
|
2125
|
+
bytes: 256 * 1024,
|
|
2126
|
+
mtimeMs: 1,
|
|
2127
|
+
contentBase64: largeContent,
|
|
2128
|
+
},
|
|
1830
2129
|
},
|
|
1831
|
-
|
|
2130
|
+
],
|
|
1832
2131
|
};
|
|
1833
2132
|
const service = {
|
|
1834
2133
|
offlineSyncApply: async () => {
|
|
@@ -1854,15 +2153,18 @@ test("HTTP offline apply accepts bulk changesets above the generic JSON body lim
|
|
|
1854
2153
|
|
|
1855
2154
|
const status = await server.start();
|
|
1856
2155
|
try {
|
|
1857
|
-
const response = await fetch(
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2156
|
+
const response = await fetch(
|
|
2157
|
+
`http://127.0.0.1:${status.port}/remnic/v1/offline-sync/apply`,
|
|
2158
|
+
{
|
|
2159
|
+
method: "POST",
|
|
2160
|
+
headers: {
|
|
2161
|
+
authorization: "Bearer test-token",
|
|
2162
|
+
"content-type": "application/json",
|
|
2163
|
+
},
|
|
2164
|
+
body: JSON.stringify({ namespace: "team", changeset }),
|
|
1862
2165
|
},
|
|
1863
|
-
|
|
1864
|
-
}
|
|
1865
|
-
const body = await response.json() as { appliedUpserts?: number };
|
|
2166
|
+
);
|
|
2167
|
+
const body = (await response.json()) as { appliedUpserts?: number };
|
|
1866
2168
|
|
|
1867
2169
|
assert.equal(response.status, 200);
|
|
1868
2170
|
assert.equal(body.appliedUpserts, 1);
|
|
@@ -1890,30 +2192,42 @@ test("HTTP offline apply requires a changeset", async () => {
|
|
|
1890
2192
|
|
|
1891
2193
|
const status = await server.start();
|
|
1892
2194
|
try {
|
|
1893
|
-
const response = await fetch(
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2195
|
+
const response = await fetch(
|
|
2196
|
+
`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`,
|
|
2197
|
+
{
|
|
2198
|
+
method: "POST",
|
|
2199
|
+
headers: {
|
|
2200
|
+
authorization: "Bearer test-token",
|
|
2201
|
+
"content-type": "application/json",
|
|
2202
|
+
},
|
|
2203
|
+
body: JSON.stringify({ namespace: "team" }),
|
|
1898
2204
|
},
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2205
|
+
);
|
|
2206
|
+
const body = (await response.json()) as {
|
|
2207
|
+
code?: string;
|
|
2208
|
+
details?: Array<{ field?: string; message?: string }>;
|
|
2209
|
+
};
|
|
1902
2210
|
|
|
1903
2211
|
assert.equal(response.status, 400);
|
|
1904
2212
|
assert.equal(body.code, "validation_error");
|
|
1905
2213
|
assert.equal(body.details?.[0]?.field, "changeset");
|
|
1906
2214
|
assert.equal(calls, 0);
|
|
1907
2215
|
|
|
1908
|
-
const nullResponse = await fetch(
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2216
|
+
const nullResponse = await fetch(
|
|
2217
|
+
`http://127.0.0.1:${status.port}/engram/v1/offline-sync/apply`,
|
|
2218
|
+
{
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: {
|
|
2221
|
+
authorization: "Bearer test-token",
|
|
2222
|
+
"content-type": "application/json",
|
|
2223
|
+
},
|
|
2224
|
+
body: JSON.stringify({ namespace: "team", changeset: null }),
|
|
1913
2225
|
},
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2226
|
+
);
|
|
2227
|
+
const nullBody = (await nullResponse.json()) as {
|
|
2228
|
+
code?: string;
|
|
2229
|
+
details?: Array<{ field?: string; message?: string }>;
|
|
2230
|
+
};
|
|
1917
2231
|
|
|
1918
2232
|
assert.equal(nullResponse.status, 400);
|
|
1919
2233
|
assert.equal(nullBody.code, "validation_error");
|
|
@@ -1927,7 +2241,12 @@ test("HTTP offline apply requires a changeset", async () => {
|
|
|
1927
2241
|
|
|
1928
2242
|
test("HTTP server rejects invalid resourceMetadataUrl at construction", () => {
|
|
1929
2243
|
const service = {} as EngramAccessService;
|
|
1930
|
-
for (const bad of [
|
|
2244
|
+
for (const bad of [
|
|
2245
|
+
"not a url",
|
|
2246
|
+
"ftp://example.com/oauth",
|
|
2247
|
+
"//relative/path",
|
|
2248
|
+
"",
|
|
2249
|
+
]) {
|
|
1931
2250
|
assert.throws(
|
|
1932
2251
|
() =>
|
|
1933
2252
|
new EngramAccessHttpServer({
|
|
@@ -1959,7 +2278,8 @@ test("HTTP server rejects invalid resourceMetadataUrl at construction", () => {
|
|
|
1959
2278
|
|
|
1960
2279
|
test("HTTP 401 www-authenticate carries resource_metadata exactly when configured", async () => {
|
|
1961
2280
|
const service = {} as EngramAccessService;
|
|
1962
|
-
const metadataUrl =
|
|
2281
|
+
const metadataUrl =
|
|
2282
|
+
"https://example.test/.well-known/oauth-protected-resource";
|
|
1963
2283
|
const server = new EngramAccessHttpServer({
|
|
1964
2284
|
service,
|
|
1965
2285
|
port: 0,
|
|
@@ -1969,7 +2289,9 @@ test("HTTP 401 www-authenticate carries resource_metadata exactly when configure
|
|
|
1969
2289
|
});
|
|
1970
2290
|
const status = await server.start();
|
|
1971
2291
|
try {
|
|
1972
|
-
const response = await fetch(
|
|
2292
|
+
const response = await fetch(
|
|
2293
|
+
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
2294
|
+
);
|
|
1973
2295
|
assert.equal(response.status, 401);
|
|
1974
2296
|
assert.equal(
|
|
1975
2297
|
response.headers.get("www-authenticate"),
|
|
@@ -1990,7 +2312,9 @@ test("HTTP 401 www-authenticate is the bare Bearer challenge when resourceMetada
|
|
|
1990
2312
|
});
|
|
1991
2313
|
const status = await server.start();
|
|
1992
2314
|
try {
|
|
1993
|
-
const response = await fetch(
|
|
2315
|
+
const response = await fetch(
|
|
2316
|
+
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
2317
|
+
);
|
|
1994
2318
|
assert.equal(response.status, 401);
|
|
1995
2319
|
assert.equal(response.headers.get("www-authenticate"), "Bearer");
|
|
1996
2320
|
} finally {
|
|
@@ -2014,12 +2338,18 @@ test("HTTP /mcp returns 405 with Allow: POST for GET and DELETE (authorized requ
|
|
|
2014
2338
|
headers: { authorization: "Bearer test-token" },
|
|
2015
2339
|
});
|
|
2016
2340
|
assert.equal(response.status, 405, `${method} /mcp must be 405`);
|
|
2017
|
-
assert.equal(
|
|
2018
|
-
|
|
2341
|
+
assert.equal(
|
|
2342
|
+
response.headers.get("allow"),
|
|
2343
|
+
"POST",
|
|
2344
|
+
`${method} /mcp must advertise Allow: POST`,
|
|
2345
|
+
);
|
|
2346
|
+
const body = (await response.json()) as { code?: string };
|
|
2019
2347
|
assert.equal(body.code, "method_not_allowed");
|
|
2020
2348
|
}
|
|
2021
2349
|
// Unauthenticated GET /mcp still gets 401 first (auth gate beats method-conformance).
|
|
2022
|
-
const unauth = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
2350
|
+
const unauth = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
2351
|
+
method: "GET",
|
|
2352
|
+
});
|
|
2023
2353
|
assert.equal(unauth.status, 401);
|
|
2024
2354
|
} finally {
|
|
2025
2355
|
await server.stop();
|
|
@@ -2046,7 +2376,10 @@ test("HTTP /mcp rejects unknown MCP-Protocol-Version header with 400 JSON-RPC er
|
|
|
2046
2376
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
2047
2377
|
});
|
|
2048
2378
|
assert.equal(response.status, 400);
|
|
2049
|
-
const body = await response.json() as {
|
|
2379
|
+
const body = (await response.json()) as {
|
|
2380
|
+
jsonrpc?: string;
|
|
2381
|
+
error?: { message?: string };
|
|
2382
|
+
};
|
|
2050
2383
|
assert.equal(body.jsonrpc, "2.0");
|
|
2051
2384
|
assert.match(body.error?.message ?? "", /unsupported MCP-Protocol-Version/);
|
|
2052
2385
|
|
|
@@ -2067,7 +2400,10 @@ test("HTTP /mcp rejects unknown MCP-Protocol-Version header with 400 JSON-RPC er
|
|
|
2067
2400
|
// Absent header is also fine.
|
|
2068
2401
|
const absent = await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
2069
2402
|
method: "POST",
|
|
2070
|
-
headers: {
|
|
2403
|
+
headers: {
|
|
2404
|
+
authorization: "Bearer test-token",
|
|
2405
|
+
"content-type": "application/json",
|
|
2406
|
+
},
|
|
2071
2407
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
|
|
2072
2408
|
});
|
|
2073
2409
|
assert.equal(absent.status, 200);
|
|
@@ -2136,7 +2472,10 @@ test("HTTP externalRequestHandler runs pre-auth, can end responses, and falls th
|
|
|
2136
2472
|
method: _req.method ?? "",
|
|
2137
2473
|
authorized: ctx.authorized,
|
|
2138
2474
|
});
|
|
2139
|
-
if (
|
|
2475
|
+
if (
|
|
2476
|
+
new URL(_req.url ?? "/", "http://placeholder").pathname ===
|
|
2477
|
+
"/probe/handled"
|
|
2478
|
+
) {
|
|
2140
2479
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2141
2480
|
res.end(JSON.stringify({ handled: true, authorized: ctx.authorized }));
|
|
2142
2481
|
return true;
|
|
@@ -2147,11 +2486,20 @@ test("HTTP externalRequestHandler runs pre-auth, can end responses, and falls th
|
|
|
2147
2486
|
const status = await server.start();
|
|
2148
2487
|
try {
|
|
2149
2488
|
// Pre-auth, the handler sees authorized=false (no token sent).
|
|
2150
|
-
const handled = await fetch(
|
|
2489
|
+
const handled = await fetch(
|
|
2490
|
+
`http://127.0.0.1:${status.port}/probe/handled`,
|
|
2491
|
+
);
|
|
2151
2492
|
assert.equal(handled.status, 200);
|
|
2152
|
-
const handledBody = await handled.json() as {
|
|
2493
|
+
const handledBody = (await handled.json()) as {
|
|
2494
|
+
handled?: boolean;
|
|
2495
|
+
authorized?: boolean;
|
|
2496
|
+
};
|
|
2153
2497
|
assert.equal(handledBody.handled, true);
|
|
2154
|
-
assert.equal(
|
|
2498
|
+
assert.equal(
|
|
2499
|
+
handledBody.authorized,
|
|
2500
|
+
false,
|
|
2501
|
+
"handler must observe authorized=false pre-token",
|
|
2502
|
+
);
|
|
2155
2503
|
|
|
2156
2504
|
// Fall-through path: same handler returns false, request continues to normal
|
|
2157
2505
|
// routing. Hit a real endpoint so we know the request reached it.
|
|
@@ -2159,9 +2507,20 @@ test("HTTP externalRequestHandler runs pre-auth, can end responses, and falls th
|
|
|
2159
2507
|
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
2160
2508
|
{ headers: { authorization: "Bearer test-token" } },
|
|
2161
2509
|
);
|
|
2162
|
-
assert.equal(
|
|
2163
|
-
|
|
2164
|
-
|
|
2510
|
+
assert.equal(
|
|
2511
|
+
passthrough.status,
|
|
2512
|
+
200,
|
|
2513
|
+
"fall-through should reach the normal health route",
|
|
2514
|
+
);
|
|
2515
|
+
const passthroughBody = (await passthrough.json()) as {
|
|
2516
|
+
ok?: boolean;
|
|
2517
|
+
memoryDir?: string;
|
|
2518
|
+
};
|
|
2519
|
+
assert.equal(
|
|
2520
|
+
passthroughBody.ok,
|
|
2521
|
+
true,
|
|
2522
|
+
"fall-through must return the stubbed health payload",
|
|
2523
|
+
);
|
|
2165
2524
|
assert.equal(passthroughBody.memoryDir, "/tmp/remnic-test");
|
|
2166
2525
|
|
|
2167
2526
|
// Authorized request: handler sees ctx.authorized=true.
|
|
@@ -2170,8 +2529,12 @@ test("HTTP externalRequestHandler runs pre-auth, can end responses, and falls th
|
|
|
2170
2529
|
{ headers: { authorization: "Bearer test-token" } },
|
|
2171
2530
|
);
|
|
2172
2531
|
assert.equal(authed.status, 200);
|
|
2173
|
-
const authedBody = await authed.json() as { authorized?: boolean };
|
|
2174
|
-
assert.equal(
|
|
2532
|
+
const authedBody = (await authed.json()) as { authorized?: boolean };
|
|
2533
|
+
assert.equal(
|
|
2534
|
+
authedBody.authorized,
|
|
2535
|
+
true,
|
|
2536
|
+
"handler must observe authorized=true with valid token",
|
|
2537
|
+
);
|
|
2175
2538
|
|
|
2176
2539
|
assert.deepEqual(seen, [
|
|
2177
2540
|
{ path: "/probe/handled", method: "GET", authorized: false },
|
|
@@ -2196,9 +2559,15 @@ test("HTTP externalRequestHandler errors flow through the existing error handler
|
|
|
2196
2559
|
});
|
|
2197
2560
|
const status = await server.start();
|
|
2198
2561
|
try {
|
|
2199
|
-
const response = await fetch(
|
|
2200
|
-
|
|
2201
|
-
|
|
2562
|
+
const response = await fetch(
|
|
2563
|
+
`http://127.0.0.1:${status.port}/engram/v1/health`,
|
|
2564
|
+
);
|
|
2565
|
+
assert.equal(
|
|
2566
|
+
response.status,
|
|
2567
|
+
500,
|
|
2568
|
+
"thrown errors must produce a 500 via the existing error handler",
|
|
2569
|
+
);
|
|
2570
|
+
const body = (await response.json()) as { code?: string };
|
|
2202
2571
|
assert.equal(body.code, "internal_error");
|
|
2203
2572
|
} finally {
|
|
2204
2573
|
await server.stop();
|
|
@@ -2262,21 +2631,33 @@ test("HTTP authTokenEntriesGetter is authoritative: scope policy binds connector
|
|
|
2262
2631
|
// Dangerous shape on purpose: BOTH getters configured, and the string
|
|
2263
2632
|
// getter is a superset (extra "string_only_token"). The entries getter
|
|
2264
2633
|
// must decide alone; nothing may leak into the string getter.
|
|
2265
|
-
authTokensGetter: () => [
|
|
2634
|
+
authTokensGetter: () => [
|
|
2635
|
+
...entries.map((entry) => entry.token),
|
|
2636
|
+
"string_only_token",
|
|
2637
|
+
],
|
|
2266
2638
|
authTokenEntriesGetter: () => entries,
|
|
2267
|
-
tokenPathPolicy: (connector, pathname) =>
|
|
2639
|
+
tokenPathPolicy: (connector, pathname) =>
|
|
2640
|
+
connector !== "chatgpt" || pathname === "/mcp",
|
|
2268
2641
|
adminConsoleEnabled: false,
|
|
2269
2642
|
});
|
|
2270
2643
|
const status = await server.start();
|
|
2271
2644
|
const request = (token: string, path: string, method = "GET") =>
|
|
2272
2645
|
fetch(`http://127.0.0.1:${status.port}${path}`, {
|
|
2273
2646
|
method,
|
|
2274
|
-
headers: {
|
|
2275
|
-
|
|
2647
|
+
headers: {
|
|
2648
|
+
authorization: `Bearer ${token}`,
|
|
2649
|
+
"content-type": "application/json",
|
|
2650
|
+
},
|
|
2651
|
+
...(method === "POST"
|
|
2652
|
+
? { body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }) }
|
|
2653
|
+
: {}),
|
|
2276
2654
|
});
|
|
2277
2655
|
try {
|
|
2278
2656
|
// Scoped chatgpt token: /mcp only; health denied.
|
|
2279
|
-
assert.equal(
|
|
2657
|
+
assert.equal(
|
|
2658
|
+
(await request("remnic_cg_scoped", "/mcp", "POST")).status,
|
|
2659
|
+
200,
|
|
2660
|
+
);
|
|
2280
2661
|
assert.equal(
|
|
2281
2662
|
(await request("remnic_cg_scoped", "/engram/v1/health")).status,
|
|
2282
2663
|
401,
|
|
@@ -2291,10 +2672,19 @@ test("HTTP authTokenEntriesGetter is authoritative: scope policy binds connector
|
|
|
2291
2672
|
assert.equal((await request("remnic_xx_anon", "/mcp", "POST")).status, 401);
|
|
2292
2673
|
// A token present ONLY in the string getter is NOT honored: the entries
|
|
2293
2674
|
// getter is authoritative and there is no fall-through.
|
|
2294
|
-
assert.equal(
|
|
2295
|
-
|
|
2675
|
+
assert.equal(
|
|
2676
|
+
(await request("string_only_token", "/mcp", "POST")).status,
|
|
2677
|
+
401,
|
|
2678
|
+
);
|
|
2679
|
+
assert.equal(
|
|
2680
|
+
(await request("string_only_token", "/engram/v1/health")).status,
|
|
2681
|
+
401,
|
|
2682
|
+
);
|
|
2296
2683
|
// Unknown tokens are rejected everywhere.
|
|
2297
|
-
assert.equal(
|
|
2684
|
+
assert.equal(
|
|
2685
|
+
(await request("remnic_zz_unknown", "/mcp", "POST")).status,
|
|
2686
|
+
401,
|
|
2687
|
+
);
|
|
2298
2688
|
// Static operator token bypasses the policy entirely: /mcp AND health.
|
|
2299
2689
|
assert.equal((await request("operator-token", "/mcp", "POST")).status, 200);
|
|
2300
2690
|
const operatorHealth = await request("operator-token", "/engram/v1/health");
|
|
@@ -2336,7 +2726,10 @@ test("HTTP scoped-token namespace allow-list is enforced on every namespace rout
|
|
|
2336
2726
|
// Entry-based tokens with a capabilities record. The scoped token carries
|
|
2337
2727
|
// a namespaces allow-list; the operator token is explicit-unrestricted.
|
|
2338
2728
|
authTokenEntriesGetter: () => [
|
|
2339
|
-
{
|
|
2729
|
+
{
|
|
2730
|
+
token: "scoped-ns-a",
|
|
2731
|
+
capabilities: { version: 1, namespaces: ["ns_a"] },
|
|
2732
|
+
},
|
|
2340
2733
|
{ token: "operator", capabilities: { version: 1 } },
|
|
2341
2734
|
],
|
|
2342
2735
|
adminConsoleEnabled: false,
|
|
@@ -2354,13 +2747,25 @@ test("HTTP scoped-token namespace allow-list is enforced on every namespace rout
|
|
|
2354
2747
|
"scoped: omitting namespace must be rejected — server default is not in the allow-list",
|
|
2355
2748
|
);
|
|
2356
2749
|
assert.equal(
|
|
2357
|
-
(await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_b"))
|
|
2750
|
+
(await request("scoped-ns-a", "/engram/v1/memories?namespace=ns_b"))
|
|
2751
|
+
.status,
|
|
2358
2752
|
403,
|
|
2359
2753
|
"scoped: an unlisted namespace must be rejected",
|
|
2360
2754
|
);
|
|
2361
|
-
const okList = await request(
|
|
2362
|
-
|
|
2363
|
-
|
|
2755
|
+
const okList = await request(
|
|
2756
|
+
"scoped-ns-a",
|
|
2757
|
+
"/engram/v1/memories?namespace=ns_a",
|
|
2758
|
+
);
|
|
2759
|
+
assert.equal(
|
|
2760
|
+
okList.status,
|
|
2761
|
+
200,
|
|
2762
|
+
"scoped: an allowed namespace must succeed",
|
|
2763
|
+
);
|
|
2764
|
+
assert.equal(
|
|
2765
|
+
browseCalls.at(-1)?.namespace,
|
|
2766
|
+
"ns_a",
|
|
2767
|
+
"scoped: the allowed namespace is forwarded to the service",
|
|
2768
|
+
);
|
|
2364
2769
|
|
|
2365
2770
|
// ── memory_get: the other named bypass route (dispatches via the op registry) ──
|
|
2366
2771
|
assert.equal(
|
|
@@ -2369,12 +2774,20 @@ test("HTTP scoped-token namespace allow-list is enforced on every namespace rout
|
|
|
2369
2774
|
"scoped memory_get: omitting namespace must be rejected",
|
|
2370
2775
|
);
|
|
2371
2776
|
assert.equal(
|
|
2372
|
-
(await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_b"))
|
|
2777
|
+
(await request("scoped-ns-a", "/engram/v1/memories/m_1?namespace=ns_b"))
|
|
2778
|
+
.status,
|
|
2373
2779
|
403,
|
|
2374
2780
|
"scoped memory_get: an unlisted namespace must be rejected",
|
|
2375
2781
|
);
|
|
2376
|
-
const okGet = await request(
|
|
2377
|
-
|
|
2782
|
+
const okGet = await request(
|
|
2783
|
+
"scoped-ns-a",
|
|
2784
|
+
"/engram/v1/memories/m_1?namespace=ns_a",
|
|
2785
|
+
);
|
|
2786
|
+
assert.equal(
|
|
2787
|
+
okGet.status,
|
|
2788
|
+
200,
|
|
2789
|
+
"scoped memory_get: an allowed namespace must succeed",
|
|
2790
|
+
);
|
|
2378
2791
|
assert.deepEqual(
|
|
2379
2792
|
getCalls.at(-1),
|
|
2380
2793
|
{ memoryId: "m_1", namespace: "ns_a" },
|
|
@@ -2406,7 +2819,9 @@ test("HTTP scoped-token namespace allow-list gates id-loaded contradiction route
|
|
|
2406
2819
|
// knows a pair id in a namespace outside its allow-list must NOT be able to
|
|
2407
2820
|
// read (GET) or mutate (resolve) it. Fail closed (403) everywhere; the pair
|
|
2408
2821
|
// in the allowed namespace is reachable.
|
|
2409
|
-
const dir = await mkdtemp(
|
|
2822
|
+
const dir = await mkdtemp(
|
|
2823
|
+
path.join(os.tmpdir(), "remnic-http-scoped-contradiction-"),
|
|
2824
|
+
);
|
|
2410
2825
|
const allowedPair = writePair(dir, {
|
|
2411
2826
|
namespace: "ns_a",
|
|
2412
2827
|
memoryIds: ["a-1", "a-2"],
|
|
@@ -2445,20 +2860,29 @@ test("HTTP scoped-token namespace allow-list gates id-loaded contradiction route
|
|
|
2445
2860
|
service,
|
|
2446
2861
|
port: 0,
|
|
2447
2862
|
authTokenEntriesGetter: () => [
|
|
2448
|
-
{
|
|
2863
|
+
{
|
|
2864
|
+
token: "scoped-ns-a",
|
|
2865
|
+
capabilities: { version: 1, namespaces: ["ns_a"] },
|
|
2866
|
+
},
|
|
2449
2867
|
{ token: "operator", capabilities: { version: 1 } },
|
|
2450
2868
|
],
|
|
2451
2869
|
adminConsoleEnabled: false,
|
|
2452
2870
|
});
|
|
2453
2871
|
const status = await server.start();
|
|
2454
2872
|
const getDetail = (token: string, pairId: string) =>
|
|
2455
|
-
fetch(
|
|
2456
|
-
|
|
2457
|
-
|
|
2873
|
+
fetch(
|
|
2874
|
+
`http://127.0.0.1:${status.port}/engram/v1/review/contradictions/${pairId}`,
|
|
2875
|
+
{
|
|
2876
|
+
headers: { authorization: `Bearer ${token}` },
|
|
2877
|
+
},
|
|
2878
|
+
);
|
|
2458
2879
|
const resolve = (token: string, pairId: string, verb: string) =>
|
|
2459
2880
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/review/resolve`, {
|
|
2460
2881
|
method: "POST",
|
|
2461
|
-
headers: {
|
|
2882
|
+
headers: {
|
|
2883
|
+
authorization: `Bearer ${token}`,
|
|
2884
|
+
"content-type": "application/json",
|
|
2885
|
+
},
|
|
2462
2886
|
body: JSON.stringify({ pairId, verb }),
|
|
2463
2887
|
});
|
|
2464
2888
|
try {
|
|
@@ -2476,7 +2900,11 @@ test("HTTP scoped-token namespace allow-list gates id-loaded contradiction route
|
|
|
2476
2900
|
);
|
|
2477
2901
|
|
|
2478
2902
|
// ── review/resolve: scoped token, denied namespace → 403 before any mutation ──
|
|
2479
|
-
const deniedResolve = await resolve(
|
|
2903
|
+
const deniedResolve = await resolve(
|
|
2904
|
+
"scoped-ns-a",
|
|
2905
|
+
deniedPair.pairId,
|
|
2906
|
+
"both-valid",
|
|
2907
|
+
);
|
|
2480
2908
|
assert.equal(
|
|
2481
2909
|
deniedResolve.status,
|
|
2482
2910
|
403,
|
|
@@ -2490,7 +2918,11 @@ test("HTTP scoped-token namespace allow-list gates id-loaded contradiction route
|
|
|
2490
2918
|
);
|
|
2491
2919
|
|
|
2492
2920
|
// ── review/resolve: scoped token, allowed namespace → succeeds (not 403) ──
|
|
2493
|
-
const allowedResolve = await resolve(
|
|
2921
|
+
const allowedResolve = await resolve(
|
|
2922
|
+
"scoped-ns-a",
|
|
2923
|
+
allowedPair.pairId,
|
|
2924
|
+
"both-valid",
|
|
2925
|
+
);
|
|
2494
2926
|
assert.equal(
|
|
2495
2927
|
allowedResolve.status,
|
|
2496
2928
|
200,
|
|
@@ -2529,7 +2961,11 @@ test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 f
|
|
|
2529
2961
|
// reaches the service.
|
|
2530
2962
|
const capsuleCalls: Array<Record<string, unknown>> = [];
|
|
2531
2963
|
const service = {
|
|
2532
|
-
configRef: parseConfig({
|
|
2964
|
+
configRef: parseConfig({
|
|
2965
|
+
memoryDir: "/tmp/remnic-mcp-ns",
|
|
2966
|
+
namespacesEnabled: true,
|
|
2967
|
+
defaultNamespace: "default",
|
|
2968
|
+
}),
|
|
2533
2969
|
capsuleList: async (args: Record<string, unknown>) => {
|
|
2534
2970
|
capsuleCalls.push(args);
|
|
2535
2971
|
return { capsules: [] };
|
|
@@ -2539,7 +2975,10 @@ test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 f
|
|
|
2539
2975
|
service,
|
|
2540
2976
|
port: 0,
|
|
2541
2977
|
authTokenEntriesGetter: () => [
|
|
2542
|
-
{
|
|
2978
|
+
{
|
|
2979
|
+
token: "scoped-ns-a",
|
|
2980
|
+
capabilities: { version: 1, namespaces: ["ns_a"] },
|
|
2981
|
+
},
|
|
2543
2982
|
{ token: "operator", capabilities: { version: 1 } },
|
|
2544
2983
|
],
|
|
2545
2984
|
adminConsoleEnabled: false,
|
|
@@ -2548,7 +2987,10 @@ test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 f
|
|
|
2548
2987
|
const mcpCall = (token: string, namespace: string) =>
|
|
2549
2988
|
fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
2550
2989
|
method: "POST",
|
|
2551
|
-
headers: {
|
|
2990
|
+
headers: {
|
|
2991
|
+
authorization: `Bearer ${token}`,
|
|
2992
|
+
"content-type": "application/json",
|
|
2993
|
+
},
|
|
2552
2994
|
body: JSON.stringify({
|
|
2553
2995
|
jsonrpc: "2.0",
|
|
2554
2996
|
id: 1,
|
|
@@ -2563,14 +3005,30 @@ test("HTTP-MCP tools/call enforces the token namespace allow-list (issue #1850 f
|
|
|
2563
3005
|
403,
|
|
2564
3006
|
"scoped MCP: an unlisted namespace must be denied (403) before op.run",
|
|
2565
3007
|
);
|
|
2566
|
-
assert.equal(
|
|
3008
|
+
assert.equal(
|
|
3009
|
+
capsuleCalls.length,
|
|
3010
|
+
0,
|
|
3011
|
+
"scoped MCP: the service must NOT be reached for a denied namespace",
|
|
3012
|
+
);
|
|
2567
3013
|
// scoped: the allowed namespace proceeds past the gate (200, not 403).
|
|
2568
3014
|
const allowed = await mcpCall("scoped-ns-a", "ns_a");
|
|
2569
|
-
assert.notEqual(
|
|
2570
|
-
|
|
3015
|
+
assert.notEqual(
|
|
3016
|
+
allowed.status,
|
|
3017
|
+
403,
|
|
3018
|
+
"scoped MCP: the allowed namespace must proceed past the gate",
|
|
3019
|
+
);
|
|
3020
|
+
assert.equal(
|
|
3021
|
+
capsuleCalls.at(-1)?.namespace,
|
|
3022
|
+
"ns_a",
|
|
3023
|
+
"scoped MCP: the allowed namespace reaches the service",
|
|
3024
|
+
);
|
|
2571
3025
|
// unrestricted: an unlisted namespace is a no-op (proceeds, not 403).
|
|
2572
3026
|
const unrestricted = await mcpCall("operator", "ns_b");
|
|
2573
|
-
assert.notEqual(
|
|
3027
|
+
assert.notEqual(
|
|
3028
|
+
unrestricted.status,
|
|
3029
|
+
403,
|
|
3030
|
+
"unrestricted MCP: namespace scoping must be a no-op",
|
|
3031
|
+
);
|
|
2574
3032
|
} finally {
|
|
2575
3033
|
await server.stop();
|
|
2576
3034
|
}
|
|
@@ -2583,7 +3041,11 @@ test("HTTP coding POST routes gate the body namespace through the allow-list (is
|
|
|
2583
3041
|
// routes. 403 before op.run; the allowed namespace reaches the service.
|
|
2584
3042
|
const decisionCalls: Array<Record<string, unknown>> = [];
|
|
2585
3043
|
const service = {
|
|
2586
|
-
configRef: parseConfig({
|
|
3044
|
+
configRef: parseConfig({
|
|
3045
|
+
memoryDir: "/tmp/remnic-coding-ns",
|
|
3046
|
+
namespacesEnabled: true,
|
|
3047
|
+
defaultNamespace: "default",
|
|
3048
|
+
}),
|
|
2587
3049
|
codingDecision: async (input: Record<string, unknown>) => {
|
|
2588
3050
|
decisionCalls.push(input);
|
|
2589
3051
|
return { subcommand: "list", records: [], count: 0 };
|
|
@@ -2593,7 +3055,10 @@ test("HTTP coding POST routes gate the body namespace through the allow-list (is
|
|
|
2593
3055
|
service,
|
|
2594
3056
|
port: 0,
|
|
2595
3057
|
authTokenEntriesGetter: () => [
|
|
2596
|
-
{
|
|
3058
|
+
{
|
|
3059
|
+
token: "scoped-ns-a",
|
|
3060
|
+
capabilities: { version: 1, namespaces: ["ns_a"] },
|
|
3061
|
+
},
|
|
2597
3062
|
{ token: "operator", capabilities: { version: 1 } },
|
|
2598
3063
|
],
|
|
2599
3064
|
adminConsoleEnabled: false,
|
|
@@ -2602,8 +3067,14 @@ test("HTTP coding POST routes gate the body namespace through the allow-list (is
|
|
|
2602
3067
|
const postDecision = (token: string, namespace: string | undefined) =>
|
|
2603
3068
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/coding/decisions`, {
|
|
2604
3069
|
method: "POST",
|
|
2605
|
-
headers: {
|
|
2606
|
-
|
|
3070
|
+
headers: {
|
|
3071
|
+
authorization: `Bearer ${token}`,
|
|
3072
|
+
"content-type": "application/json",
|
|
3073
|
+
},
|
|
3074
|
+
body: JSON.stringify({
|
|
3075
|
+
subcommand: "list",
|
|
3076
|
+
...(namespace ? { namespace } : {}),
|
|
3077
|
+
}),
|
|
2607
3078
|
});
|
|
2608
3079
|
try {
|
|
2609
3080
|
// scoped: body.namespace to an unlisted tenant → 403 before op.run.
|
|
@@ -2612,11 +3083,23 @@ test("HTTP coding POST routes gate the body namespace through the allow-list (is
|
|
|
2612
3083
|
403,
|
|
2613
3084
|
"scoped coding: body.namespace to an unlisted namespace must be denied (403)",
|
|
2614
3085
|
);
|
|
2615
|
-
assert.equal(
|
|
3086
|
+
assert.equal(
|
|
3087
|
+
decisionCalls.length,
|
|
3088
|
+
0,
|
|
3089
|
+
"scoped coding: the service must NOT be reached for a denied namespace",
|
|
3090
|
+
);
|
|
2616
3091
|
// scoped: the allowed body.namespace reaches the service.
|
|
2617
3092
|
const allowed = await postDecision("scoped-ns-a", "ns_a");
|
|
2618
|
-
assert.notEqual(
|
|
2619
|
-
|
|
3093
|
+
assert.notEqual(
|
|
3094
|
+
allowed.status,
|
|
3095
|
+
403,
|
|
3096
|
+
"scoped coding: the allowed namespace must proceed past the gate",
|
|
3097
|
+
);
|
|
3098
|
+
assert.equal(
|
|
3099
|
+
decisionCalls.at(-1)?.namespace,
|
|
3100
|
+
"ns_a",
|
|
3101
|
+
"scoped coding: the allowed namespace reaches the service",
|
|
3102
|
+
);
|
|
2620
3103
|
// unrestricted: any body.namespace is a no-op.
|
|
2621
3104
|
assert.notEqual(
|
|
2622
3105
|
(await postDecision("operator", "ns_b")).status,
|
|
@@ -2635,7 +3118,9 @@ test("Legacy undefined-namespace contradiction pair: default-allow-list token re
|
|
|
2635
3118
|
// effective-namespace chokepoint maps undefined → default, so:
|
|
2636
3119
|
// - namespaces:['default'] (== server default) ⇒ ALLOWED read + resolve;
|
|
2637
3120
|
// - namespaces:['ns_a'] (not default) ⇒ DENIED.
|
|
2638
|
-
const dir = await mkdtemp(
|
|
3121
|
+
const dir = await mkdtemp(
|
|
3122
|
+
path.join(os.tmpdir(), "remnic-http-scoped-legacy-pair-"),
|
|
3123
|
+
);
|
|
2639
3124
|
const legacyPair = writePair(dir, {
|
|
2640
3125
|
memoryIds: ["legacy-x", "legacy-y"],
|
|
2641
3126
|
verdict: "contradicts",
|
|
@@ -2647,7 +3132,11 @@ test("Legacy undefined-namespace contradiction pair: default-allow-list token re
|
|
|
2647
3132
|
assert.equal(readPair(dir, legacyPair.pairId)?.namespace, undefined);
|
|
2648
3133
|
const storage = { dir } as unknown as StorageManager;
|
|
2649
3134
|
const baseService = {
|
|
2650
|
-
configRef: parseConfig({
|
|
3135
|
+
configRef: parseConfig({
|
|
3136
|
+
memoryDir: dir,
|
|
3137
|
+
namespacesEnabled: true,
|
|
3138
|
+
defaultNamespace: "default",
|
|
3139
|
+
}),
|
|
2651
3140
|
memoryDir: dir,
|
|
2652
3141
|
storageRef: storage,
|
|
2653
3142
|
getReadableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
@@ -2664,7 +3153,12 @@ test("Legacy undefined-namespace contradiction pair: default-allow-list token re
|
|
|
2664
3153
|
return new EngramAccessHttpServer({
|
|
2665
3154
|
service,
|
|
2666
3155
|
port: 0,
|
|
2667
|
-
authTokenEntriesGetter: () => [
|
|
3156
|
+
authTokenEntriesGetter: () => [
|
|
3157
|
+
{
|
|
3158
|
+
token: scopedToken,
|
|
3159
|
+
capabilities: { version: 1, namespaces: allowList },
|
|
3160
|
+
},
|
|
3161
|
+
],
|
|
2668
3162
|
adminConsoleEnabled: false,
|
|
2669
3163
|
});
|
|
2670
3164
|
};
|
|
@@ -2677,13 +3171,27 @@ test("Legacy undefined-namespace contradiction pair: default-allow-list token re
|
|
|
2677
3171
|
`http://127.0.0.1:${defaultStatus.port}/engram/v1/review/contradictions/${legacyPair.pairId}`,
|
|
2678
3172
|
{ headers: { authorization: "Bearer allow-default" } },
|
|
2679
3173
|
);
|
|
2680
|
-
assert.equal(
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
3174
|
+
assert.equal(
|
|
3175
|
+
detail.status,
|
|
3176
|
+
200,
|
|
3177
|
+
"default-allow token: legacy undefined pair must be readable (undefined→default)",
|
|
3178
|
+
);
|
|
3179
|
+
const resolve = await fetch(
|
|
3180
|
+
`http://127.0.0.1:${defaultStatus.port}/engram/v1/review/resolve`,
|
|
3181
|
+
{
|
|
3182
|
+
method: "POST",
|
|
3183
|
+
headers: {
|
|
3184
|
+
authorization: "Bearer allow-default",
|
|
3185
|
+
"content-type": "application/json",
|
|
3186
|
+
},
|
|
3187
|
+
body: JSON.stringify({ pairId: legacyPair.pairId, verb: "both-valid" }),
|
|
3188
|
+
},
|
|
3189
|
+
);
|
|
3190
|
+
assert.equal(
|
|
3191
|
+
resolve.status,
|
|
3192
|
+
200,
|
|
3193
|
+
"default-allow token: legacy undefined pair must be resolvable",
|
|
3194
|
+
);
|
|
2687
3195
|
assert.equal(
|
|
2688
3196
|
readPair(dir, legacyPair.pairId)?.resolution,
|
|
2689
3197
|
"both-valid",
|
|
@@ -2718,12 +3226,25 @@ test("Legacy undefined-namespace contradiction pair: default-allow-list token re
|
|
|
2718
3226
|
403,
|
|
2719
3227
|
"non-default token: legacy undefined pair must be denied (undefined→default not in allow-list)",
|
|
2720
3228
|
);
|
|
2721
|
-
const resolve = await fetch(
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
3229
|
+
const resolve = await fetch(
|
|
3230
|
+
`http://127.0.0.1:${deniedStatus.port}/engram/v1/review/resolve`,
|
|
3231
|
+
{
|
|
3232
|
+
method: "POST",
|
|
3233
|
+
headers: {
|
|
3234
|
+
authorization: "Bearer allow-ns-a",
|
|
3235
|
+
"content-type": "application/json",
|
|
3236
|
+
},
|
|
3237
|
+
body: JSON.stringify({
|
|
3238
|
+
pairId: deniedLegacyPair.pairId,
|
|
3239
|
+
verb: "both-valid",
|
|
3240
|
+
}),
|
|
3241
|
+
},
|
|
3242
|
+
);
|
|
3243
|
+
assert.equal(
|
|
3244
|
+
resolve.status,
|
|
3245
|
+
403,
|
|
3246
|
+
"non-default token: legacy undefined pair must not be resolvable",
|
|
3247
|
+
);
|
|
2727
3248
|
assert.notEqual(
|
|
2728
3249
|
readPair(dir, deniedLegacyPair.pairId)?.resolution,
|
|
2729
3250
|
"both-valid",
|
|
@@ -2744,7 +3265,10 @@ test("HTTP adapters route is op-gated: deny-all → 403, unrestricted → 200 (i
|
|
|
2744
3265
|
// NO enforceTokenOp, so a deny-all / narrow-ops token reached it. It now
|
|
2745
3266
|
// dispatches through the adapters_status op; a scoped token is rejected.
|
|
2746
3267
|
const service = {
|
|
2747
|
-
configRef: parseConfig({
|
|
3268
|
+
configRef: parseConfig({
|
|
3269
|
+
memoryDir: "/tmp/remnic-http-adapters-opgate",
|
|
3270
|
+
defaultNamespace: "default",
|
|
3271
|
+
}),
|
|
2748
3272
|
} as unknown as EngramAccessService;
|
|
2749
3273
|
const server = new EngramAccessHttpServer({
|
|
2750
3274
|
service,
|
|
@@ -2773,9 +3297,17 @@ test("HTTP adapters route is op-gated: deny-all → 403, unrestricted → 200 (i
|
|
|
2773
3297
|
"narrow-ops token without adapters_status must be rejected",
|
|
2774
3298
|
);
|
|
2775
3299
|
const ok = await request("operator", "/engram/v1/adapters");
|
|
2776
|
-
assert.equal(
|
|
3300
|
+
assert.equal(
|
|
3301
|
+
ok.status,
|
|
3302
|
+
200,
|
|
3303
|
+
"unrestricted token must reach adapter metadata",
|
|
3304
|
+
);
|
|
2777
3305
|
const body = (await ok.json()) as { adaptersEnabled?: boolean };
|
|
2778
|
-
assert.equal(
|
|
3306
|
+
assert.equal(
|
|
3307
|
+
typeof body.adaptersEnabled,
|
|
3308
|
+
"boolean",
|
|
3309
|
+
"adapter metadata body shape preserved",
|
|
3310
|
+
);
|
|
2779
3311
|
} finally {
|
|
2780
3312
|
await server.stop();
|
|
2781
3313
|
}
|
|
@@ -2791,53 +3323,93 @@ test("HTTP chat routes gate the resumed session's STORED namespace (issue #1850
|
|
|
2791
3323
|
// sessionBelongsToPrincipal, so the namespace gate is the ONLY gate in play).
|
|
2792
3324
|
const session = await createChatSession(dir, { namespace: "ns_a" });
|
|
2793
3325
|
const service = {
|
|
2794
|
-
configRef: parseConfig({
|
|
3326
|
+
configRef: parseConfig({
|
|
3327
|
+
memoryDir: dir,
|
|
3328
|
+
namespacesEnabled: true,
|
|
3329
|
+
defaultNamespace: "default",
|
|
3330
|
+
}),
|
|
2795
3331
|
memoryDir: dir,
|
|
2796
3332
|
} as unknown as EngramAccessService;
|
|
2797
3333
|
const server = new EngramAccessHttpServer({
|
|
2798
3334
|
service,
|
|
2799
3335
|
port: 0,
|
|
2800
3336
|
authTokenEntriesGetter: () => [
|
|
2801
|
-
{
|
|
2802
|
-
|
|
3337
|
+
{
|
|
3338
|
+
token: "scoped-ns-b",
|
|
3339
|
+
capabilities: { version: 1, namespaces: ["ns_b"] },
|
|
3340
|
+
},
|
|
3341
|
+
{
|
|
3342
|
+
token: "scoped-ns-a",
|
|
3343
|
+
capabilities: { version: 1, namespaces: ["ns_a"] },
|
|
3344
|
+
},
|
|
2803
3345
|
],
|
|
2804
3346
|
adminConsoleEnabled: false,
|
|
2805
3347
|
});
|
|
2806
3348
|
const status = await server.start();
|
|
2807
|
-
const authed = (token: string) => ({
|
|
3349
|
+
const authed = (token: string) => ({
|
|
3350
|
+
headers: { authorization: `Bearer ${token}` },
|
|
3351
|
+
});
|
|
2808
3352
|
try {
|
|
2809
3353
|
// ── POST chat/message resuming a CROSS-namespace session → 403 ──
|
|
2810
|
-
const cross = await fetch(
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
3354
|
+
const cross = await fetch(
|
|
3355
|
+
`http://127.0.0.1:${status.port}/engram/v1/chat/message`,
|
|
3356
|
+
{
|
|
3357
|
+
method: "POST",
|
|
3358
|
+
...authed("scoped-ns-b"),
|
|
3359
|
+
headers: {
|
|
3360
|
+
...authed("scoped-ns-b").headers,
|
|
3361
|
+
"content-type": "application/json",
|
|
3362
|
+
},
|
|
3363
|
+
body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
|
|
3364
|
+
},
|
|
3365
|
+
);
|
|
3366
|
+
assert.equal(
|
|
3367
|
+
cross.status,
|
|
3368
|
+
403,
|
|
3369
|
+
"scoped-ns-b must NOT post to a session bound to ns_a",
|
|
3370
|
+
);
|
|
2817
3371
|
|
|
2818
3372
|
// ── POST chat/message resuming a SAME-namespace session → passes the gate ──
|
|
2819
3373
|
// (Chat is unconfigured here so the handler returns chat_disabled 404; the
|
|
2820
3374
|
// point is it is NOT 403 — the namespace gate let it through.)
|
|
2821
|
-
const same = await fetch(
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3375
|
+
const same = await fetch(
|
|
3376
|
+
`http://127.0.0.1:${status.port}/engram/v1/chat/message`,
|
|
3377
|
+
{
|
|
3378
|
+
method: "POST",
|
|
3379
|
+
headers: {
|
|
3380
|
+
authorization: "Bearer scoped-ns-a",
|
|
3381
|
+
"content-type": "application/json",
|
|
3382
|
+
},
|
|
3383
|
+
body: JSON.stringify({ message: "hi", chatSessionId: session.id }),
|
|
3384
|
+
},
|
|
3385
|
+
);
|
|
3386
|
+
assert.notEqual(
|
|
3387
|
+
same.status,
|
|
3388
|
+
403,
|
|
3389
|
+
"scoped-ns-a must pass the namespace gate for its own session",
|
|
3390
|
+
);
|
|
2827
3391
|
|
|
2828
3392
|
// ── GET chat/events for a CROSS-namespace session → 403 ──
|
|
2829
3393
|
const crossEvents = await fetch(
|
|
2830
3394
|
`http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
|
|
2831
3395
|
authed("scoped-ns-b"),
|
|
2832
3396
|
);
|
|
2833
|
-
assert.equal(
|
|
3397
|
+
assert.equal(
|
|
3398
|
+
crossEvents.status,
|
|
3399
|
+
403,
|
|
3400
|
+
"scoped-ns-b must NOT stream a session bound to ns_a",
|
|
3401
|
+
);
|
|
2834
3402
|
|
|
2835
3403
|
// ── GET chat/events for a SAME-namespace session → passes the gate ──
|
|
2836
3404
|
const sameEvents = await fetch(
|
|
2837
3405
|
`http://127.0.0.1:${status.port}/engram/v1/chat/events/${session.id}`,
|
|
2838
3406
|
authed("scoped-ns-a"),
|
|
2839
3407
|
);
|
|
2840
|
-
assert.notEqual(
|
|
3408
|
+
assert.notEqual(
|
|
3409
|
+
sameEvents.status,
|
|
3410
|
+
403,
|
|
3411
|
+
"scoped-ns-a must pass the namespace gate for its own session",
|
|
3412
|
+
);
|
|
2841
3413
|
} finally {
|
|
2842
3414
|
await server.stop();
|
|
2843
3415
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -2876,16 +3448,33 @@ test("HTTP per-request token capabilities never bleed across concurrent async re
|
|
|
2876
3448
|
port: 0,
|
|
2877
3449
|
authTokenEntriesGetter: () => [
|
|
2878
3450
|
{ token: "deny-all", capabilities: { version: 1, ops: [] } },
|
|
2879
|
-
{
|
|
2880
|
-
|
|
3451
|
+
{
|
|
3452
|
+
token: "scoped-ns-a",
|
|
3453
|
+
capabilities: {
|
|
3454
|
+
version: 1,
|
|
3455
|
+
ops: ["memory_list"],
|
|
3456
|
+
namespaces: ["ns_a"],
|
|
3457
|
+
},
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
token: "scoped-ns-b",
|
|
3461
|
+
capabilities: {
|
|
3462
|
+
version: 1,
|
|
3463
|
+
ops: ["memory_list"],
|
|
3464
|
+
namespaces: ["ns_b"],
|
|
3465
|
+
},
|
|
3466
|
+
},
|
|
2881
3467
|
],
|
|
2882
3468
|
adminConsoleEnabled: false,
|
|
2883
3469
|
});
|
|
2884
3470
|
const status = await server.start();
|
|
2885
3471
|
const get = (token: string, namespace: string) =>
|
|
2886
|
-
fetch(
|
|
2887
|
-
|
|
2888
|
-
|
|
3472
|
+
fetch(
|
|
3473
|
+
`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`,
|
|
3474
|
+
{
|
|
3475
|
+
headers: { authorization: `Bearer ${token}` },
|
|
3476
|
+
},
|
|
3477
|
+
);
|
|
2889
3478
|
try {
|
|
2890
3479
|
const REPEATS = 15;
|
|
2891
3480
|
const batch: Promise<Response>[] = [];
|
|
@@ -2898,26 +3487,54 @@ test("HTTP per-request token capabilities never bleed across concurrent async re
|
|
|
2898
3487
|
|
|
2899
3488
|
// (1) Op-gate holds under concurrency: every deny-all request is 403, never
|
|
2900
3489
|
// fail-open 200 (which would mean its caps bled to unrestricted).
|
|
2901
|
-
const denyAllStatuses = responses
|
|
3490
|
+
const denyAllStatuses = responses
|
|
3491
|
+
.filter((_, i) => i % 3 === 0)
|
|
3492
|
+
.map((r) => r.status);
|
|
2902
3493
|
assert.ok(
|
|
2903
3494
|
denyAllStatuses.every((s) => s === 403),
|
|
2904
3495
|
`deny-all token must always be 403, got: ${JSON.stringify(denyAllStatuses)}`,
|
|
2905
3496
|
);
|
|
2906
3497
|
|
|
2907
3498
|
// scoped tokens reach the service (200) — op + namespace gates both pass.
|
|
2908
|
-
const nsAStatuses = responses
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3499
|
+
const nsAStatuses = responses
|
|
3500
|
+
.filter((_, i) => i % 3 === 1)
|
|
3501
|
+
.map((r) => r.status);
|
|
3502
|
+
const nsBStatuses = responses
|
|
3503
|
+
.filter((_, i) => i % 3 === 2)
|
|
3504
|
+
.map((r) => r.status);
|
|
3505
|
+
assert.ok(
|
|
3506
|
+
nsAStatuses.every((s) => s === 200),
|
|
3507
|
+
`scoped-ns-a should all be 200: ${JSON.stringify(nsAStatuses)}`,
|
|
3508
|
+
);
|
|
3509
|
+
assert.ok(
|
|
3510
|
+
nsBStatuses.every((s) => s === 200),
|
|
3511
|
+
`scoped-ns-b should all be 200: ${JSON.stringify(nsBStatuses)}`,
|
|
3512
|
+
);
|
|
2912
3513
|
|
|
2913
3514
|
// (2) No cross-request bleed: the store read AFTER the async yield carried
|
|
2914
3515
|
// the REQUESTING token's namespace on every service call. memoryBrowse
|
|
2915
3516
|
// ran once per scoped request (2 * REPEATS).
|
|
2916
|
-
assert.equal(
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3517
|
+
assert.equal(
|
|
3518
|
+
observed.length,
|
|
3519
|
+
2 * REPEATS,
|
|
3520
|
+
"memoryBrowse called once per scoped request",
|
|
3521
|
+
);
|
|
3522
|
+
const nsAObserved = observed.filter((c) =>
|
|
3523
|
+
c?.namespaces?.includes("ns_a"),
|
|
3524
|
+
).length;
|
|
3525
|
+
const nsBObserved = observed.filter((c) =>
|
|
3526
|
+
c?.namespaces?.includes("ns_b"),
|
|
3527
|
+
).length;
|
|
3528
|
+
assert.equal(
|
|
3529
|
+
nsAObserved,
|
|
3530
|
+
REPEATS,
|
|
3531
|
+
"every ns_a request observed its own scope — no bleed",
|
|
3532
|
+
);
|
|
3533
|
+
assert.equal(
|
|
3534
|
+
nsBObserved,
|
|
3535
|
+
REPEATS,
|
|
3536
|
+
"every ns_b request observed its own scope — no bleed",
|
|
3537
|
+
);
|
|
2921
3538
|
} finally {
|
|
2922
3539
|
await server.stop();
|
|
2923
3540
|
}
|
|
@@ -2955,22 +3572,39 @@ test("HTTP allow-capability is never inherited by a concurrent deny token across
|
|
|
2955
3572
|
port: 0,
|
|
2956
3573
|
authTokenEntriesGetter: () => [
|
|
2957
3574
|
// ALLOW: op + namespace both permitted → 200.
|
|
2958
|
-
{
|
|
3575
|
+
{
|
|
3576
|
+
token: "allow",
|
|
3577
|
+
capabilities: {
|
|
3578
|
+
version: 1,
|
|
3579
|
+
ops: ["memory_list"],
|
|
3580
|
+
namespaces: ["ns_allow"],
|
|
3581
|
+
},
|
|
3582
|
+
},
|
|
2959
3583
|
// DENY (op-level): empty ops → 403 at the op-gate.
|
|
2960
3584
|
{ token: "deny-all", capabilities: { version: 1, ops: [] } },
|
|
2961
3585
|
// DENY (namespace-level): op permitted but scoped to a DIFFERENT
|
|
2962
3586
|
// namespace ("ns_other") than the request ("ns_allow") → 403 at the
|
|
2963
3587
|
// namespace-gate. The ONLY way this becomes 200 is by inheriting the
|
|
2964
3588
|
// allow token's ns_allow cap — exactly the bleed run() prevents.
|
|
2965
|
-
{
|
|
3589
|
+
{
|
|
3590
|
+
token: "deny-ns",
|
|
3591
|
+
capabilities: {
|
|
3592
|
+
version: 1,
|
|
3593
|
+
ops: ["memory_list"],
|
|
3594
|
+
namespaces: ["ns_other"],
|
|
3595
|
+
},
|
|
3596
|
+
},
|
|
2966
3597
|
],
|
|
2967
3598
|
adminConsoleEnabled: false,
|
|
2968
3599
|
});
|
|
2969
3600
|
const status = await server.start();
|
|
2970
3601
|
const get = (token: string, namespace: string) =>
|
|
2971
|
-
fetch(
|
|
2972
|
-
|
|
2973
|
-
|
|
3602
|
+
fetch(
|
|
3603
|
+
`http://127.0.0.1:${status.port}/engram/v1/memories?namespace=${namespace}`,
|
|
3604
|
+
{
|
|
3605
|
+
headers: { authorization: `Bearer ${token}` },
|
|
3606
|
+
},
|
|
3607
|
+
);
|
|
2974
3608
|
try {
|
|
2975
3609
|
const REPEATS = 15;
|
|
2976
3610
|
const batch: Promise<Response>[] = [];
|
|
@@ -2980,13 +3614,22 @@ test("HTTP allow-capability is never inherited by a concurrent deny token across
|
|
|
2980
3614
|
batch.push(get("deny-ns", "ns_allow")); // 3i+2 → 403 (namespace-gate)
|
|
2981
3615
|
}
|
|
2982
3616
|
const responses = await Promise.all(batch);
|
|
2983
|
-
const allowStatuses = responses
|
|
2984
|
-
|
|
2985
|
-
|
|
3617
|
+
const allowStatuses = responses
|
|
3618
|
+
.filter((_, i) => i % 3 === 0)
|
|
3619
|
+
.map((r) => r.status);
|
|
3620
|
+
const denyAllStatuses = responses
|
|
3621
|
+
.filter((_, i) => i % 3 === 1)
|
|
3622
|
+
.map((r) => r.status);
|
|
3623
|
+
const denyNsStatuses = responses
|
|
3624
|
+
.filter((_, i) => i % 3 === 2)
|
|
3625
|
+
.map((r) => r.status);
|
|
2986
3626
|
|
|
2987
3627
|
// ALLOW requests succeed (200) — the caps DID resolve correctly and the
|
|
2988
3628
|
// service read happened under the allow scope.
|
|
2989
|
-
assert.ok(
|
|
3629
|
+
assert.ok(
|
|
3630
|
+
allowStatuses.every((s) => s === 200),
|
|
3631
|
+
`allow token should all be 200: ${JSON.stringify(allowStatuses)}`,
|
|
3632
|
+
);
|
|
2990
3633
|
|
|
2991
3634
|
// NON-INHERITANCE: in the SAME interleaved batch where allow got 200, every
|
|
2992
3635
|
// deny request stayed 403 — neither deny token ever inherited the allow
|
|
@@ -3006,7 +3649,11 @@ test("HTTP allow-capability is never inherited by a concurrent deny token across
|
|
|
3006
3649
|
|
|
3007
3650
|
// Only allow requests reached the service; the store read after the async
|
|
3008
3651
|
// yield carried the allow token's namespace every time — zero deny bleed.
|
|
3009
|
-
assert.equal(
|
|
3652
|
+
assert.equal(
|
|
3653
|
+
observed.length,
|
|
3654
|
+
REPEATS,
|
|
3655
|
+
"only allow requests reached the service",
|
|
3656
|
+
);
|
|
3010
3657
|
assert.ok(
|
|
3011
3658
|
observed.every((c) => c?.namespaces?.includes("ns_allow")),
|
|
3012
3659
|
"every service read carried the allow token's namespace — no deny bleed",
|
|
@@ -3033,7 +3680,9 @@ test("HTTP chat/message gates the NEW session's namespace — scoped token canno
|
|
|
3033
3680
|
chat: { ...DEFAULT_CHAT_CONFIG, enabled: true },
|
|
3034
3681
|
}),
|
|
3035
3682
|
memoryDir: dir,
|
|
3036
|
-
fallbackLlmRef: {
|
|
3683
|
+
fallbackLlmRef: {
|
|
3684
|
+
chatCompletion: async () => ({ content: "stub reply" }),
|
|
3685
|
+
},
|
|
3037
3686
|
localLlmRef: null,
|
|
3038
3687
|
memoryGet: () => Promise.resolve(null),
|
|
3039
3688
|
memoryTimeline: () => Promise.resolve([]),
|
|
@@ -3050,9 +3699,23 @@ test("HTTP chat/message gates the NEW session's namespace — scoped token canno
|
|
|
3050
3699
|
port: 0,
|
|
3051
3700
|
authTokenEntriesGetter: () => [
|
|
3052
3701
|
// scoped to ns_a (NOT the server default) — must be denied a new chat.
|
|
3053
|
-
{
|
|
3702
|
+
{
|
|
3703
|
+
token: "scoped-ns-a",
|
|
3704
|
+
capabilities: {
|
|
3705
|
+
version: 1,
|
|
3706
|
+
ops: ["chat_message"],
|
|
3707
|
+
namespaces: ["ns_a"],
|
|
3708
|
+
},
|
|
3709
|
+
},
|
|
3054
3710
|
// scoped to the server default — may start a new chat there.
|
|
3055
|
-
{
|
|
3711
|
+
{
|
|
3712
|
+
token: "scoped-default",
|
|
3713
|
+
capabilities: {
|
|
3714
|
+
version: 1,
|
|
3715
|
+
ops: ["chat_message"],
|
|
3716
|
+
namespaces: ["default"],
|
|
3717
|
+
},
|
|
3718
|
+
},
|
|
3056
3719
|
],
|
|
3057
3720
|
adminConsoleEnabled: false,
|
|
3058
3721
|
});
|
|
@@ -3060,21 +3723,35 @@ test("HTTP chat/message gates the NEW session's namespace — scoped token canno
|
|
|
3060
3723
|
const post = (token: string) =>
|
|
3061
3724
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/chat/message`, {
|
|
3062
3725
|
method: "POST",
|
|
3063
|
-
headers: {
|
|
3726
|
+
headers: {
|
|
3727
|
+
authorization: `Bearer ${token}`,
|
|
3728
|
+
"content-type": "application/json",
|
|
3729
|
+
},
|
|
3064
3730
|
body: JSON.stringify({ message: "hi" }), // no chatSessionId → NEW session
|
|
3065
3731
|
});
|
|
3066
3732
|
try {
|
|
3067
3733
|
// DISALLOWED: token scoped to ns_a; the new session would land in the
|
|
3068
3734
|
// server default ("default"), which is not in ["ns_a"] → 403.
|
|
3069
3735
|
const denied = await post("scoped-ns-a");
|
|
3070
|
-
assert.equal(
|
|
3736
|
+
assert.equal(
|
|
3737
|
+
denied.status,
|
|
3738
|
+
403,
|
|
3739
|
+
"scoped-to-ns_a must not start a new chat in the default namespace",
|
|
3740
|
+
);
|
|
3071
3741
|
|
|
3072
3742
|
// ALLOWED: token scoped to the server default → gate passes → chat runs
|
|
3073
3743
|
// and returns 200 with a reply.
|
|
3074
3744
|
const allowed = await post("scoped-default");
|
|
3075
|
-
assert.equal(
|
|
3745
|
+
assert.equal(
|
|
3746
|
+
allowed.status,
|
|
3747
|
+
200,
|
|
3748
|
+
"scoped-to-default may start a new chat in the default namespace",
|
|
3749
|
+
);
|
|
3076
3750
|
const allowedBody = (await allowed.json()) as { reply?: string };
|
|
3077
|
-
assert.ok(
|
|
3751
|
+
assert.ok(
|
|
3752
|
+
allowedBody.reply && allowedBody.reply.length > 0,
|
|
3753
|
+
"allowed new chat returns a reply",
|
|
3754
|
+
);
|
|
3078
3755
|
} finally {
|
|
3079
3756
|
await server.stop();
|
|
3080
3757
|
}
|
|
@@ -3088,7 +3765,12 @@ test("HTTP recall forwards the connector from the authorized token entry", async
|
|
|
3088
3765
|
const service = {
|
|
3089
3766
|
recall: (request: EngramAccessRecallRequest) => {
|
|
3090
3767
|
captured = request;
|
|
3091
|
-
return Promise.resolve({
|
|
3768
|
+
return Promise.resolve({
|
|
3769
|
+
context: "",
|
|
3770
|
+
count: 0,
|
|
3771
|
+
memoryIds: [],
|
|
3772
|
+
results: [],
|
|
3773
|
+
});
|
|
3092
3774
|
},
|
|
3093
3775
|
} as unknown as EngramAccessService;
|
|
3094
3776
|
const server = new EngramAccessHttpServer({
|
|
@@ -3166,19 +3848,26 @@ test("PR #1852: token rotation between authorization and write does not stamp th
|
|
|
3166
3848
|
});
|
|
3167
3849
|
const status = await server.start();
|
|
3168
3850
|
try {
|
|
3169
|
-
const res = await fetch(
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3851
|
+
const res = await fetch(
|
|
3852
|
+
`http://127.0.0.1:${status.port}/engram/v1/memories`,
|
|
3853
|
+
{
|
|
3854
|
+
method: "POST",
|
|
3855
|
+
headers: {
|
|
3856
|
+
authorization: "Bearer race_token",
|
|
3857
|
+
"content-type": "application/json",
|
|
3858
|
+
},
|
|
3859
|
+
body: JSON.stringify({
|
|
3860
|
+
content: "race-test memory",
|
|
3861
|
+
schemaVersion: 1,
|
|
3862
|
+
idempotencyKey: "race-key-1852",
|
|
3863
|
+
}),
|
|
3174
3864
|
},
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
assert.equal(res.status, 201, "memory_store must succeed with a valid entry token");
|
|
3865
|
+
);
|
|
3866
|
+
assert.equal(
|
|
3867
|
+
res.status,
|
|
3868
|
+
201,
|
|
3869
|
+
"memory_store must succeed with a valid entry token",
|
|
3870
|
+
);
|
|
3182
3871
|
|
|
3183
3872
|
// The provenance must come from the entry that AUTHORIZED the request
|
|
3184
3873
|
// ("chatgpt"), not from the rotated store ("codex-cli").
|
|
@@ -3189,7 +3878,11 @@ test("PR #1852: token rotation between authorization and write does not stamp th
|
|
|
3189
3878
|
);
|
|
3190
3879
|
// The getter must have been called only once — the matched entry is
|
|
3191
3880
|
// cached per-request.
|
|
3192
|
-
assert.equal(
|
|
3881
|
+
assert.equal(
|
|
3882
|
+
getterCalls,
|
|
3883
|
+
1,
|
|
3884
|
+
"authTokenEntriesGetter must be called once (cached per-request)",
|
|
3885
|
+
);
|
|
3193
3886
|
} finally {
|
|
3194
3887
|
await server.stop();
|
|
3195
3888
|
}
|
|
@@ -3220,23 +3913,28 @@ test("PR #1852: static operator token has no connector provenance under rotation
|
|
|
3220
3913
|
service,
|
|
3221
3914
|
port: 0,
|
|
3222
3915
|
authToken: "operator-static",
|
|
3223
|
-
authTokenEntriesGetter: () => [
|
|
3916
|
+
authTokenEntriesGetter: () => [
|
|
3917
|
+
{ token: "operator-static", connector: "chatgpt" },
|
|
3918
|
+
],
|
|
3224
3919
|
adminConsoleEnabled: false,
|
|
3225
3920
|
});
|
|
3226
3921
|
const status = await server.start();
|
|
3227
3922
|
try {
|
|
3228
|
-
const res = await fetch(
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3923
|
+
const res = await fetch(
|
|
3924
|
+
`http://127.0.0.1:${status.port}/engram/v1/memories`,
|
|
3925
|
+
{
|
|
3926
|
+
method: "POST",
|
|
3927
|
+
headers: {
|
|
3928
|
+
authorization: "Bearer operator-static",
|
|
3929
|
+
"content-type": "application/json",
|
|
3930
|
+
},
|
|
3931
|
+
body: JSON.stringify({
|
|
3932
|
+
content: "operator-token memory",
|
|
3933
|
+
schemaVersion: 1,
|
|
3934
|
+
idempotencyKey: "static-key-1852",
|
|
3935
|
+
}),
|
|
3233
3936
|
},
|
|
3234
|
-
|
|
3235
|
-
content: "operator-token memory",
|
|
3236
|
-
schemaVersion: 1,
|
|
3237
|
-
idempotencyKey: "static-key-1852",
|
|
3238
|
-
}),
|
|
3239
|
-
});
|
|
3937
|
+
);
|
|
3240
3938
|
assert.equal(res.status, 201);
|
|
3241
3939
|
assert.equal(
|
|
3242
3940
|
capturedSourceConnector,
|
|
@@ -3253,8 +3951,15 @@ test("HTTP namespace/writable preflight returns the structured writability resul
|
|
|
3253
3951
|
// for any principal; a foreign namespace with no policy is not. A rejection
|
|
3254
3952
|
// is a valid 200 answer (read via `ok`), not an HTTP error.
|
|
3255
3953
|
const service = {
|
|
3256
|
-
configRef: parseConfig({
|
|
3257
|
-
|
|
3954
|
+
configRef: parseConfig({
|
|
3955
|
+
memoryDir: "/tmp/remnic-http-preflight-test",
|
|
3956
|
+
defaultNamespace: "default",
|
|
3957
|
+
}),
|
|
3958
|
+
namespaceWritablePreflight: async ({
|
|
3959
|
+
namespace,
|
|
3960
|
+
}: {
|
|
3961
|
+
namespace?: string;
|
|
3962
|
+
}) =>
|
|
3258
3963
|
namespace && namespace !== "default"
|
|
3259
3964
|
? { ok: false as const, reason: "not_writable" as const, namespace }
|
|
3260
3965
|
: { ok: true as const, namespace: "default" },
|
|
@@ -3298,7 +4003,10 @@ test("HTTP namespace/writable forwards project context to the scoped write prefl
|
|
|
3298
4003
|
}
|
|
3299
4004
|
| undefined;
|
|
3300
4005
|
const service = {
|
|
3301
|
-
configRef: parseConfig({
|
|
4006
|
+
configRef: parseConfig({
|
|
4007
|
+
memoryDir: "/tmp/remnic-http-preflight-context",
|
|
4008
|
+
defaultNamespace: "default",
|
|
4009
|
+
}),
|
|
3302
4010
|
namespaceWritablePreflight: async (request: {
|
|
3303
4011
|
namespace?: string;
|
|
3304
4012
|
sessionKey?: string;
|
|
@@ -3322,7 +4030,10 @@ test("HTTP namespace/writable forwards project context to the scoped write prefl
|
|
|
3322
4030
|
`http://127.0.0.1:${status.port}/engram/v1/namespace/writable?session=s-1&projectTag=Acme%2FWebshop&cwd=%2Ftmp%2Frepo`,
|
|
3323
4031
|
{ headers: { authorization: "Bearer test-token" } },
|
|
3324
4032
|
);
|
|
3325
|
-
assert.deepEqual(await response.json(), {
|
|
4033
|
+
assert.deepEqual(await response.json(), {
|
|
4034
|
+
ok: true,
|
|
4035
|
+
namespace: "derived-project-namespace",
|
|
4036
|
+
});
|
|
3326
4037
|
assert.deepEqual(received, {
|
|
3327
4038
|
namespace: undefined,
|
|
3328
4039
|
sessionKey: "s-1",
|
|
@@ -3337,7 +4048,10 @@ test("HTTP namespace/writable forwards project context to the scoped write prefl
|
|
|
3337
4048
|
|
|
3338
4049
|
test("HTTP namespace/writable checks token scope against the effective scoped namespace", async () => {
|
|
3339
4050
|
const service = {
|
|
3340
|
-
configRef: parseConfig({
|
|
4051
|
+
configRef: parseConfig({
|
|
4052
|
+
memoryDir: "/tmp/remnic-http-preflight-effective",
|
|
4053
|
+
defaultNamespace: "default",
|
|
4054
|
+
}),
|
|
3341
4055
|
namespaceWritablePreflight: async () => ({
|
|
3342
4056
|
ok: true as const,
|
|
3343
4057
|
namespace: "principal-project-derived",
|
|
@@ -3347,7 +4061,10 @@ test("HTTP namespace/writable checks token scope against the effective scoped na
|
|
|
3347
4061
|
service,
|
|
3348
4062
|
port: 0,
|
|
3349
4063
|
authTokenEntriesGetter: () => [
|
|
3350
|
-
{
|
|
4064
|
+
{
|
|
4065
|
+
token: "default-only",
|
|
4066
|
+
capabilities: { version: 1, ops: ["observe"], namespaces: ["default"] },
|
|
4067
|
+
},
|
|
3351
4068
|
],
|
|
3352
4069
|
adminConsoleEnabled: false,
|
|
3353
4070
|
});
|
|
@@ -3370,16 +4087,31 @@ test("HTTP namespace/writable checks token scope against the effective scoped na
|
|
|
3370
4087
|
|
|
3371
4088
|
test("HTTP namespace/writable preflight admits write-scoped tokens and rejects read-only ops", async () => {
|
|
3372
4089
|
const service = {
|
|
3373
|
-
configRef: parseConfig({
|
|
3374
|
-
|
|
4090
|
+
configRef: parseConfig({
|
|
4091
|
+
memoryDir: "/tmp/remnic-http-preflight-ops-test",
|
|
4092
|
+
defaultNamespace: "default",
|
|
4093
|
+
}),
|
|
4094
|
+
namespaceWritablePreflight: async () => ({
|
|
4095
|
+
ok: true as const,
|
|
4096
|
+
namespace: "default",
|
|
4097
|
+
}),
|
|
3375
4098
|
} as unknown as EngramAccessService;
|
|
3376
4099
|
const server = new EngramAccessHttpServer({
|
|
3377
4100
|
service,
|
|
3378
4101
|
port: 0,
|
|
3379
4102
|
authTokenEntriesGetter: () => [
|
|
3380
|
-
{
|
|
3381
|
-
|
|
3382
|
-
|
|
4103
|
+
{
|
|
4104
|
+
token: "write-scoped",
|
|
4105
|
+
capabilities: { version: 1, ops: ["observe", "memory_store"] },
|
|
4106
|
+
},
|
|
4107
|
+
{
|
|
4108
|
+
token: "store-only",
|
|
4109
|
+
capabilities: { version: 1, ops: ["memory_store"] },
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
token: "diagnostic-only",
|
|
4113
|
+
capabilities: { version: 1, ops: ["namespace_writable"] },
|
|
4114
|
+
},
|
|
3383
4115
|
{ token: "read-only", capabilities: { version: 1, ops: ["recall"] } },
|
|
3384
4116
|
],
|
|
3385
4117
|
adminConsoleEnabled: false,
|
|
@@ -3443,16 +4175,31 @@ test("HTTP meetings build enforces the per-principal write rate limit (issue #19
|
|
|
3443
4175
|
const build = () =>
|
|
3444
4176
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/meetings/build`, {
|
|
3445
4177
|
method: "POST",
|
|
3446
|
-
headers: {
|
|
4178
|
+
headers: {
|
|
4179
|
+
authorization: "Bearer test-token",
|
|
4180
|
+
"content-type": "application/json",
|
|
4181
|
+
},
|
|
3447
4182
|
body: JSON.stringify({ date: "2026-03-10" }),
|
|
3448
4183
|
});
|
|
3449
4184
|
const first = await build();
|
|
3450
|
-
assert.equal(
|
|
4185
|
+
assert.equal(
|
|
4186
|
+
first.status,
|
|
4187
|
+
200,
|
|
4188
|
+
"the first build is within the write quota",
|
|
4189
|
+
);
|
|
3451
4190
|
const second = await build();
|
|
3452
|
-
assert.equal(
|
|
3453
|
-
|
|
4191
|
+
assert.equal(
|
|
4192
|
+
second.status,
|
|
4193
|
+
429,
|
|
4194
|
+
"the second build exceeds the per-principal write quota",
|
|
4195
|
+
);
|
|
4196
|
+
const rateLimited = (await second.json()) as { code?: string };
|
|
3454
4197
|
assert.equal(rateLimited.code, "write_rate_limited");
|
|
3455
|
-
assert.equal(
|
|
4198
|
+
assert.equal(
|
|
4199
|
+
builds,
|
|
4200
|
+
1,
|
|
4201
|
+
"the rate-limited build never reached the service (no persist/reindex)",
|
|
4202
|
+
);
|
|
3456
4203
|
} finally {
|
|
3457
4204
|
await server.stop();
|
|
3458
4205
|
}
|
|
@@ -3480,7 +4227,10 @@ test("HTTP-MCP extraction force-flush aliases consume the write quota", async ()
|
|
|
3480
4227
|
const status = await server.start();
|
|
3481
4228
|
try {
|
|
3482
4229
|
const responses: Response[] = [];
|
|
3483
|
-
for (const name of [
|
|
4230
|
+
for (const name of [
|
|
4231
|
+
"remnic.extraction_force_flush",
|
|
4232
|
+
"engram.extraction_force_flush",
|
|
4233
|
+
]) {
|
|
3484
4234
|
responses.push(
|
|
3485
4235
|
await fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
3486
4236
|
method: "POST",
|
|
@@ -3510,9 +4260,214 @@ test("HTTP-MCP extraction force-flush aliases consume the write quota", async ()
|
|
|
3510
4260
|
assert.equal(first.status, 200);
|
|
3511
4261
|
await first.text();
|
|
3512
4262
|
assert.equal(second.status, 429);
|
|
3513
|
-
const rateLimited = await second.json() as { code?: string };
|
|
4263
|
+
const rateLimited = (await second.json()) as { code?: string };
|
|
3514
4264
|
assert.equal(rateLimited.code, "write_rate_limited");
|
|
3515
|
-
assert.equal(
|
|
4265
|
+
assert.equal(
|
|
4266
|
+
flushes,
|
|
4267
|
+
1,
|
|
4268
|
+
"the rate-limited alias must not reach the service",
|
|
4269
|
+
);
|
|
4270
|
+
} finally {
|
|
4271
|
+
await server.stop();
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
|
|
4275
|
+
test("concurrent support-passport MCP writes reserve quota before dispatch", async () => {
|
|
4276
|
+
const firstStarted = Promise.withResolvers<void>();
|
|
4277
|
+
const secondStarted = Promise.withResolvers<void>();
|
|
4278
|
+
const releaseWrites = Promise.withResolvers<void>();
|
|
4279
|
+
let writes = 0;
|
|
4280
|
+
let requestId = 0;
|
|
4281
|
+
const service = {
|
|
4282
|
+
supportPassportGenerateDrafts: async () => {
|
|
4283
|
+
writes += 1;
|
|
4284
|
+
if (writes === 1) firstStarted.resolve();
|
|
4285
|
+
if (writes === 2) secondStarted.resolve();
|
|
4286
|
+
await releaseWrites.promise;
|
|
4287
|
+
return [];
|
|
4288
|
+
},
|
|
4289
|
+
} as unknown as EngramAccessService;
|
|
4290
|
+
const server = new EngramAccessHttpServer({
|
|
4291
|
+
service,
|
|
4292
|
+
port: 0,
|
|
4293
|
+
authToken: "test-token",
|
|
4294
|
+
adminConsoleEnabled: false,
|
|
4295
|
+
writeRateLimitMaxRequests: 1,
|
|
4296
|
+
});
|
|
4297
|
+
const status = await server.start();
|
|
4298
|
+
const generate = () =>
|
|
4299
|
+
fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
4300
|
+
method: "POST",
|
|
4301
|
+
headers: {
|
|
4302
|
+
authorization: "Bearer test-token",
|
|
4303
|
+
"content-type": "application/json",
|
|
4304
|
+
},
|
|
4305
|
+
body: JSON.stringify({
|
|
4306
|
+
jsonrpc: "2.0",
|
|
4307
|
+
id: ++requestId,
|
|
4308
|
+
method: "tools/call",
|
|
4309
|
+
params: {
|
|
4310
|
+
name: "remnic.support_passport_drafts_generate",
|
|
4311
|
+
arguments: {
|
|
4312
|
+
sourceMemoryIds: ["memory-1"],
|
|
4313
|
+
sourceMemoryRevisions: [
|
|
4314
|
+
{ memoryId: "memory-1", revision: "a".repeat(64) },
|
|
4315
|
+
],
|
|
4316
|
+
consent: true,
|
|
4317
|
+
},
|
|
4318
|
+
},
|
|
4319
|
+
}),
|
|
4320
|
+
});
|
|
4321
|
+
try {
|
|
4322
|
+
const firstRequest = generate();
|
|
4323
|
+
await firstStarted.promise;
|
|
4324
|
+
const secondRequest = generate();
|
|
4325
|
+
const secondOutcome = await Promise.race([
|
|
4326
|
+
secondRequest.then(() => "rate-limited" as const),
|
|
4327
|
+
secondStarted.promise.then(() => "dispatched" as const),
|
|
4328
|
+
]);
|
|
4329
|
+
assert.equal(secondOutcome, "rate-limited");
|
|
4330
|
+
releaseWrites.resolve();
|
|
4331
|
+
const [first, second] = await Promise.all([firstRequest, secondRequest]);
|
|
4332
|
+
assert.equal(first.status, 200);
|
|
4333
|
+
assert.equal(second.status, 429);
|
|
4334
|
+
assert.equal(writes, 1);
|
|
4335
|
+
} finally {
|
|
4336
|
+
releaseWrites.resolve();
|
|
4337
|
+
await server.stop();
|
|
4338
|
+
}
|
|
4339
|
+
});
|
|
4340
|
+
|
|
4341
|
+
test("support-passport MCP keeps reserved quota when generated drafts commit before failure", async () => {
|
|
4342
|
+
let writes = 0;
|
|
4343
|
+
let requestId = 0;
|
|
4344
|
+
const service = {
|
|
4345
|
+
supportPassportGenerateDrafts: async (
|
|
4346
|
+
_principal: string,
|
|
4347
|
+
input: { onCommitted?: () => void },
|
|
4348
|
+
) => {
|
|
4349
|
+
writes += 1;
|
|
4350
|
+
input.onCommitted?.();
|
|
4351
|
+
throw new SupportPassportError(
|
|
4352
|
+
"state_conflict",
|
|
4353
|
+
"Draft cleanup failed.",
|
|
4354
|
+
409,
|
|
4355
|
+
);
|
|
4356
|
+
},
|
|
4357
|
+
} as unknown as EngramAccessService;
|
|
4358
|
+
const server = new EngramAccessHttpServer({
|
|
4359
|
+
service,
|
|
4360
|
+
port: 0,
|
|
4361
|
+
authToken: "test-token",
|
|
4362
|
+
adminConsoleEnabled: false,
|
|
4363
|
+
writeRateLimitMaxRequests: 1,
|
|
4364
|
+
});
|
|
4365
|
+
const status = await server.start();
|
|
4366
|
+
const generate = () =>
|
|
4367
|
+
fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
4368
|
+
method: "POST",
|
|
4369
|
+
headers: {
|
|
4370
|
+
authorization: "Bearer test-token",
|
|
4371
|
+
"content-type": "application/json",
|
|
4372
|
+
},
|
|
4373
|
+
body: JSON.stringify({
|
|
4374
|
+
jsonrpc: "2.0",
|
|
4375
|
+
id: ++requestId,
|
|
4376
|
+
method: "tools/call",
|
|
4377
|
+
params: {
|
|
4378
|
+
name: "remnic.support_passport_drafts_generate",
|
|
4379
|
+
arguments: {
|
|
4380
|
+
sourceMemoryIds: ["memory-1"],
|
|
4381
|
+
sourceMemoryRevisions: [
|
|
4382
|
+
{ memoryId: "memory-1", revision: "a".repeat(64) },
|
|
4383
|
+
],
|
|
4384
|
+
consent: true,
|
|
4385
|
+
},
|
|
4386
|
+
},
|
|
4387
|
+
}),
|
|
4388
|
+
});
|
|
4389
|
+
try {
|
|
4390
|
+
const first = await generate();
|
|
4391
|
+
assert.equal(first.status, 200);
|
|
4392
|
+
assert.equal(
|
|
4393
|
+
((await first.json()) as { result?: { isError?: boolean } }).result
|
|
4394
|
+
?.isError,
|
|
4395
|
+
true,
|
|
4396
|
+
);
|
|
4397
|
+
assert.equal(
|
|
4398
|
+
(await generate()).status,
|
|
4399
|
+
429,
|
|
4400
|
+
"the committed draft keeps its reserved MCP quota slot",
|
|
4401
|
+
);
|
|
4402
|
+
assert.equal(writes, 1);
|
|
4403
|
+
} finally {
|
|
4404
|
+
await server.stop();
|
|
4405
|
+
}
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4408
|
+
test("support-passport MCP applies the write quota to share-link revocation", async () => {
|
|
4409
|
+
let drafts = 0;
|
|
4410
|
+
let revocations = 0;
|
|
4411
|
+
const service = {
|
|
4412
|
+
supportPassportEnabled: true,
|
|
4413
|
+
supportPassportCreateManualDraft: async () => {
|
|
4414
|
+
drafts += 1;
|
|
4415
|
+
return { cardId: "card-one" };
|
|
4416
|
+
},
|
|
4417
|
+
supportPassportRevokeGrant: async () => {
|
|
4418
|
+
revocations += 1;
|
|
4419
|
+
return {
|
|
4420
|
+
grantId: "grant-one",
|
|
4421
|
+
revokedAt: "2026-08-11T12:05:00.000Z",
|
|
4422
|
+
version: 2,
|
|
4423
|
+
};
|
|
4424
|
+
},
|
|
4425
|
+
} as unknown as EngramAccessService;
|
|
4426
|
+
const server = new EngramAccessHttpServer({
|
|
4427
|
+
service,
|
|
4428
|
+
port: 0,
|
|
4429
|
+
authToken: "test-token",
|
|
4430
|
+
adminConsoleEnabled: false,
|
|
4431
|
+
writeRateLimitMaxRequests: 1,
|
|
4432
|
+
});
|
|
4433
|
+
const status = await server.start();
|
|
4434
|
+
const call = (name: string, args: Record<string, unknown>, id: number) =>
|
|
4435
|
+
fetch(`http://127.0.0.1:${status.port}/mcp`, {
|
|
4436
|
+
method: "POST",
|
|
4437
|
+
headers: {
|
|
4438
|
+
authorization: "Bearer test-token",
|
|
4439
|
+
"content-type": "application/json",
|
|
4440
|
+
},
|
|
4441
|
+
body: JSON.stringify({
|
|
4442
|
+
jsonrpc: "2.0",
|
|
4443
|
+
id,
|
|
4444
|
+
method: "tools/call",
|
|
4445
|
+
params: { name, arguments: args },
|
|
4446
|
+
}),
|
|
4447
|
+
});
|
|
4448
|
+
try {
|
|
4449
|
+
const draft = await call(
|
|
4450
|
+
"remnic.support_passport_draft_create",
|
|
4451
|
+
{
|
|
4452
|
+
title: "Plan changes",
|
|
4453
|
+
statement: "Tell me before plans change.",
|
|
4454
|
+
category: "transitions",
|
|
4455
|
+
reviewBy: "2027-02-07T12:00:00.000Z",
|
|
4456
|
+
},
|
|
4457
|
+
1,
|
|
4458
|
+
);
|
|
4459
|
+
assert.equal(draft.status, 200);
|
|
4460
|
+
const revoked = await call(
|
|
4461
|
+
"remnic.support_passport_grant_revoke",
|
|
4462
|
+
{
|
|
4463
|
+
grantId: "grant-one",
|
|
4464
|
+
expectedVersion: 1,
|
|
4465
|
+
},
|
|
4466
|
+
2,
|
|
4467
|
+
);
|
|
4468
|
+
assert.equal(revoked.status, 429);
|
|
4469
|
+
assert.equal(drafts, 1);
|
|
4470
|
+
assert.equal(revocations, 0);
|
|
3516
4471
|
} finally {
|
|
3517
4472
|
await server.stop();
|
|
3518
4473
|
}
|
|
@@ -3522,9 +4477,7 @@ test("HTTP extraction force-flush records quota after commit when cleanup fails"
|
|
|
3522
4477
|
let flushes = 0;
|
|
3523
4478
|
let committed = 0;
|
|
3524
4479
|
const service = {
|
|
3525
|
-
extractionForceFlush: async (request: {
|
|
3526
|
-
onCommitted?: () => void;
|
|
3527
|
-
}) => {
|
|
4480
|
+
extractionForceFlush: async (request: { onCommitted?: () => void }) => {
|
|
3528
4481
|
flushes += 1;
|
|
3529
4482
|
request.onCommitted?.();
|
|
3530
4483
|
committed += 1;
|
|
@@ -3554,7 +4507,7 @@ test("HTTP extraction force-flush records quota after commit when cleanup fails"
|
|
|
3554
4507
|
await first.text();
|
|
3555
4508
|
const second = await flush();
|
|
3556
4509
|
assert.equal(second.status, 429);
|
|
3557
|
-
const rateLimited = await second.json() as { code?: string };
|
|
4510
|
+
const rateLimited = (await second.json()) as { code?: string };
|
|
3558
4511
|
assert.equal(rateLimited.code, "write_rate_limited");
|
|
3559
4512
|
assert.equal(flushes, 1);
|
|
3560
4513
|
assert.equal(committed, 1);
|
|
@@ -3612,7 +4565,11 @@ test("HTTP-MCP extraction force-flush records quota at the commit boundary", asy
|
|
|
3612
4565
|
assert.equal(second.status, 429);
|
|
3613
4566
|
const rateLimited = (await second.json()) as { code?: string };
|
|
3614
4567
|
assert.equal(rateLimited.code, "write_rate_limited");
|
|
3615
|
-
assert.equal(
|
|
4568
|
+
assert.equal(
|
|
4569
|
+
flushes,
|
|
4570
|
+
1,
|
|
4571
|
+
"the rate-limited MCP flush must not reach the service",
|
|
4572
|
+
);
|
|
3616
4573
|
assert.equal(committed, 1);
|
|
3617
4574
|
} finally {
|
|
3618
4575
|
await server.stop();
|
|
@@ -3635,11 +4592,18 @@ test("HTTP meetings get returns 400 (not 500) for a malformed percent-encoded id
|
|
|
3635
4592
|
});
|
|
3636
4593
|
const status = await server.start();
|
|
3637
4594
|
try {
|
|
3638
|
-
const response = await fetch(
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
4595
|
+
const response = await fetch(
|
|
4596
|
+
`http://127.0.0.1:${status.port}/engram/v1/meetings/%E0%A4%A`,
|
|
4597
|
+
{
|
|
4598
|
+
headers: { authorization: "Bearer test-token" },
|
|
4599
|
+
},
|
|
4600
|
+
);
|
|
4601
|
+
assert.equal(
|
|
4602
|
+
response.status,
|
|
4603
|
+
400,
|
|
4604
|
+
"a malformed id is a client error, not an internal 500",
|
|
4605
|
+
);
|
|
4606
|
+
const errorBody = (await response.json()) as { code?: string };
|
|
3643
4607
|
assert.equal(errorBody.code, "invalid_request");
|
|
3644
4608
|
assert.equal(called, 0, "the malformed id never reached the service");
|
|
3645
4609
|
} finally {
|
|
@@ -3649,7 +4613,10 @@ test("HTTP meetings get returns 400 (not 500) for a malformed percent-encoded id
|
|
|
3649
4613
|
|
|
3650
4614
|
test("HTTP authorization probe verifies requested operation grants without invoking operations (issue #2129)", async () => {
|
|
3651
4615
|
const service = {
|
|
3652
|
-
configRef: parseConfig({
|
|
4616
|
+
configRef: parseConfig({
|
|
4617
|
+
memoryDir: "/tmp/remnic-http-authorization-probe",
|
|
4618
|
+
defaultNamespace: "default",
|
|
4619
|
+
}),
|
|
3653
4620
|
} as unknown as EngramAccessService;
|
|
3654
4621
|
const implicitNamespaceOperations = [
|
|
3655
4622
|
"offline_sync_snapshot",
|
|
@@ -3668,10 +4635,19 @@ test("HTTP authorization probe verifies requested operation grants without invok
|
|
|
3668
4635
|
authTokenEntriesGetter: () => [
|
|
3669
4636
|
{
|
|
3670
4637
|
token: "delegate-token",
|
|
3671
|
-
capabilities: {
|
|
4638
|
+
capabilities: {
|
|
4639
|
+
version: 1,
|
|
4640
|
+
ops: ["recall", "observe", "lcm_compaction_flush"],
|
|
4641
|
+
},
|
|
4642
|
+
},
|
|
4643
|
+
{
|
|
4644
|
+
token: "recall-only-token",
|
|
4645
|
+
capabilities: { version: 1, ops: ["recall"] },
|
|
4646
|
+
},
|
|
4647
|
+
{
|
|
4648
|
+
token: "observe-only-token",
|
|
4649
|
+
capabilities: { version: 1, ops: ["observe"] },
|
|
3672
4650
|
},
|
|
3673
|
-
{ token: "recall-only-token", capabilities: { version: 1, ops: ["recall"] } },
|
|
3674
|
-
{ token: "observe-only-token", capabilities: { version: 1, ops: ["observe"] } },
|
|
3675
4651
|
{
|
|
3676
4652
|
token: "namespace-scoped-delegate-token",
|
|
3677
4653
|
capabilities: {
|
|
@@ -3720,38 +4696,71 @@ test("HTTP authorization probe verifies requested operation grants without invok
|
|
|
3720
4696
|
const query = new URLSearchParams();
|
|
3721
4697
|
for (const operation of operations) query.append("op", operation);
|
|
3722
4698
|
if (namespace !== undefined) query.set("namespace", namespace);
|
|
3723
|
-
return fetch(
|
|
3724
|
-
|
|
3725
|
-
|
|
4699
|
+
return fetch(
|
|
4700
|
+
`http://127.0.0.1:${status.port}/engram/v1/authorization?${query}`,
|
|
4701
|
+
{
|
|
4702
|
+
headers: { authorization: `Bearer ${token}` },
|
|
4703
|
+
},
|
|
4704
|
+
);
|
|
3726
4705
|
};
|
|
3727
4706
|
const delegateOperations = ["recall", "observe", "lcm_compaction_flush"];
|
|
3728
4707
|
try {
|
|
3729
|
-
const authorized = await probe("delegate-token", [
|
|
4708
|
+
const authorized = await probe("delegate-token", [
|
|
4709
|
+
...delegateOperations,
|
|
4710
|
+
"recall",
|
|
4711
|
+
]);
|
|
3730
4712
|
assert.equal(authorized.status, 200);
|
|
3731
4713
|
assert.equal(authorized.headers.get("cache-control"), "no-store");
|
|
3732
|
-
assert.deepEqual(await authorized.json(), {
|
|
4714
|
+
assert.deepEqual(await authorized.json(), {
|
|
4715
|
+
authorized: true,
|
|
4716
|
+
operations: delegateOperations,
|
|
4717
|
+
});
|
|
3733
4718
|
|
|
3734
|
-
const alternateGrant = await probe("observe-only-token", [
|
|
4719
|
+
const alternateGrant = await probe("observe-only-token", [
|
|
4720
|
+
"namespace_writable",
|
|
4721
|
+
]);
|
|
3735
4722
|
assert.equal(alternateGrant.status, 200);
|
|
3736
4723
|
await alternateGrant.text();
|
|
3737
4724
|
|
|
3738
|
-
const namespaceDiagnostic = await probe(
|
|
3739
|
-
"
|
|
3740
|
-
|
|
4725
|
+
const namespaceDiagnostic = await probe("namespace-scoped-delegate-token", [
|
|
4726
|
+
"namespace_writable",
|
|
4727
|
+
]);
|
|
4728
|
+
assert.equal(
|
|
4729
|
+
namespaceDiagnostic.status,
|
|
4730
|
+
200,
|
|
4731
|
+
"writability diagnostics report scope in their response",
|
|
3741
4732
|
);
|
|
3742
|
-
assert.equal(namespaceDiagnostic.status, 200, "writability diagnostics report scope in their response");
|
|
3743
4733
|
await namespaceDiagnostic.text();
|
|
3744
4734
|
|
|
3745
|
-
const namespaceAgnostic = await probe("namespace-scoped-adapters-token", [
|
|
3746
|
-
|
|
4735
|
+
const namespaceAgnostic = await probe("namespace-scoped-adapters-token", [
|
|
4736
|
+
"adapters_status",
|
|
4737
|
+
]);
|
|
4738
|
+
assert.equal(
|
|
4739
|
+
namespaceAgnostic.status,
|
|
4740
|
+
200,
|
|
4741
|
+
"namespace-free operations must not use the daemon default",
|
|
4742
|
+
);
|
|
3747
4743
|
await namespaceAgnostic.text();
|
|
3748
4744
|
|
|
3749
|
-
const namespaceDenied = await probe(
|
|
3750
|
-
|
|
4745
|
+
const namespaceDenied = await probe(
|
|
4746
|
+
"namespace-scoped-delegate-token",
|
|
4747
|
+
delegateOperations,
|
|
4748
|
+
);
|
|
4749
|
+
assert.equal(
|
|
4750
|
+
namespaceDenied.status,
|
|
4751
|
+
403,
|
|
4752
|
+
"a scoped token must cover the daemon default namespace",
|
|
4753
|
+
);
|
|
3751
4754
|
|
|
3752
4755
|
for (const operation of implicitNamespaceOperations) {
|
|
3753
|
-
const implicitNamespace = await probe("implicit-namespace-token", [
|
|
3754
|
-
|
|
4756
|
+
const implicitNamespace = await probe("implicit-namespace-token", [
|
|
4757
|
+
operation,
|
|
4758
|
+
]);
|
|
4759
|
+
assert.equal(
|
|
4760
|
+
implicitNamespace.status,
|
|
4761
|
+
403,
|
|
4762
|
+
`${operation} must authorize the effective namespace`,
|
|
4763
|
+
);
|
|
3755
4764
|
await implicitNamespace.text();
|
|
3756
4765
|
}
|
|
3757
4766
|
await namespaceDenied.text();
|
|
@@ -3776,7 +4785,9 @@ test("HTTP authorization probe verifies requested operation grants without invok
|
|
|
3776
4785
|
);
|
|
3777
4786
|
await resourceNamespaceProbe.text();
|
|
3778
4787
|
|
|
3779
|
-
const fleetWide = await probe("fleet-scoped-token", [
|
|
4788
|
+
const fleetWide = await probe("fleet-scoped-token", [
|
|
4789
|
+
"continuity_audit_generate",
|
|
4790
|
+
]);
|
|
3780
4791
|
assert.equal(fleetWide.status, 403);
|
|
3781
4792
|
assert.equal(fleetWide.headers.get("cache-control"), "no-store");
|
|
3782
4793
|
await fleetWide.text();
|
|
@@ -3803,6 +4814,112 @@ test("HTTP authorization probe verifies requested operation grants without invok
|
|
|
3803
4814
|
assert.equal(unknown.status, 400);
|
|
3804
4815
|
assert.equal(unknown.headers.get("cache-control"), "no-store");
|
|
3805
4816
|
await unknown.text();
|
|
4817
|
+
|
|
4818
|
+
for (const helperOperation of [
|
|
4819
|
+
"support_passport_grant_read",
|
|
4820
|
+
"support_passport_grant_ask",
|
|
4821
|
+
]) {
|
|
4822
|
+
const helperOnly = await probe("delegate-token", [helperOperation]);
|
|
4823
|
+
assert.equal(
|
|
4824
|
+
helperOnly.status,
|
|
4825
|
+
400,
|
|
4826
|
+
`${helperOperation} cannot use bearer authorization probes`,
|
|
4827
|
+
);
|
|
4828
|
+
assert.equal(helperOnly.headers.get("cache-control"), "no-store");
|
|
4829
|
+
await helperOnly.text();
|
|
4830
|
+
}
|
|
4831
|
+
} finally {
|
|
4832
|
+
await server.stop();
|
|
4833
|
+
}
|
|
4834
|
+
});
|
|
4835
|
+
|
|
4836
|
+
test("HTTP authorization probe checks the principal-derived support passport namespace", async () => {
|
|
4837
|
+
let namespaceResolutions = 0;
|
|
4838
|
+
const service = {
|
|
4839
|
+
configRef: parseConfig({
|
|
4840
|
+
memoryDir: "/tmp/remnic-http-passport-authorization-probe",
|
|
4841
|
+
defaultNamespace: "default",
|
|
4842
|
+
namespacesEnabled: true,
|
|
4843
|
+
namespacePolicies: [
|
|
4844
|
+
{
|
|
4845
|
+
name: "owner:alice",
|
|
4846
|
+
readPrincipals: ["owner:alice"],
|
|
4847
|
+
writePrincipals: ["owner:alice"],
|
|
4848
|
+
},
|
|
4849
|
+
],
|
|
4850
|
+
}),
|
|
4851
|
+
getWritableStorageForNamespace: async (
|
|
4852
|
+
namespace: string | undefined,
|
|
4853
|
+
principal: string | undefined,
|
|
4854
|
+
) => {
|
|
4855
|
+
namespaceResolutions += 1;
|
|
4856
|
+
assert.equal(namespace, undefined);
|
|
4857
|
+
assert.equal(principal, "owner:alice");
|
|
4858
|
+
return { namespace: "owner:alice", storage: {} as StorageManager };
|
|
4859
|
+
},
|
|
4860
|
+
} as unknown as EngramAccessService;
|
|
4861
|
+
const server = new EngramAccessHttpServer({
|
|
4862
|
+
service,
|
|
4863
|
+
port: 0,
|
|
4864
|
+
principal: "owner:alice",
|
|
4865
|
+
authTokenEntriesGetter: () => [
|
|
4866
|
+
{
|
|
4867
|
+
token: "passport-token",
|
|
4868
|
+
capabilities: {
|
|
4869
|
+
version: 1,
|
|
4870
|
+
ops: ["support_passport_cards_list"],
|
|
4871
|
+
namespaces: ["other"],
|
|
4872
|
+
},
|
|
4873
|
+
},
|
|
4874
|
+
],
|
|
4875
|
+
adminConsoleEnabled: false,
|
|
4876
|
+
});
|
|
4877
|
+
const status = await server.start();
|
|
4878
|
+
try {
|
|
4879
|
+
const response = await fetch(
|
|
4880
|
+
`http://127.0.0.1:${status.port}/engram/v1/authorization?op=support_passport_cards_list&namespace=other`,
|
|
4881
|
+
{ headers: { authorization: "Bearer passport-token" } },
|
|
4882
|
+
);
|
|
4883
|
+
|
|
4884
|
+
assert.equal(response.status, 403);
|
|
4885
|
+
assert.equal(namespaceResolutions, 1);
|
|
4886
|
+
} finally {
|
|
4887
|
+
await server.stop();
|
|
4888
|
+
}
|
|
4889
|
+
});
|
|
4890
|
+
|
|
4891
|
+
test("HTTP authorization probe rejects support passport access without a trusted principal", async () => {
|
|
4892
|
+
let namespaceResolutions = 0;
|
|
4893
|
+
const service = {
|
|
4894
|
+
configRef: parseConfig({
|
|
4895
|
+
memoryDir: "/tmp/remnic-http-passport-principal-probe",
|
|
4896
|
+
defaultNamespace: "default",
|
|
4897
|
+
}),
|
|
4898
|
+
getWritableStorageForNamespace: async () => {
|
|
4899
|
+
namespaceResolutions += 1;
|
|
4900
|
+
return { namespace: "default", storage: {} as StorageManager };
|
|
4901
|
+
},
|
|
4902
|
+
} as unknown as EngramAccessService;
|
|
4903
|
+
const server = new EngramAccessHttpServer({
|
|
4904
|
+
service,
|
|
4905
|
+
port: 0,
|
|
4906
|
+
authTokenEntriesGetter: () => [
|
|
4907
|
+
{
|
|
4908
|
+
token: "passport-token",
|
|
4909
|
+
capabilities: { version: 1, ops: ["support_passport_cards_list"] },
|
|
4910
|
+
},
|
|
4911
|
+
],
|
|
4912
|
+
adminConsoleEnabled: false,
|
|
4913
|
+
});
|
|
4914
|
+
const status = await server.start();
|
|
4915
|
+
try {
|
|
4916
|
+
const response = await fetch(
|
|
4917
|
+
`http://127.0.0.1:${status.port}/engram/v1/authorization?op=support_passport_cards_list`,
|
|
4918
|
+
{ headers: { authorization: "Bearer passport-token" } },
|
|
4919
|
+
);
|
|
4920
|
+
|
|
4921
|
+
assert.equal(response.status, 403);
|
|
4922
|
+
assert.equal(namespaceResolutions, 0);
|
|
3806
4923
|
} finally {
|
|
3807
4924
|
await server.stop();
|
|
3808
4925
|
}
|
|
@@ -3838,7 +4955,11 @@ test("HTTP capabilities explicitly advertise receiver convergence finalization",
|
|
|
3838
4955
|
});
|
|
3839
4956
|
|
|
3840
4957
|
test("HTTP convergence completion forwards one authenticated namespace batch refresh", async () => {
|
|
3841
|
-
const calls: Array<{
|
|
4958
|
+
const calls: Array<{
|
|
4959
|
+
namespaces: string[];
|
|
4960
|
+
principal?: string;
|
|
4961
|
+
sourceId: string;
|
|
4962
|
+
}> = [];
|
|
3842
4963
|
const service = {
|
|
3843
4964
|
offlineSyncFinalizeConvergence: async (options: {
|
|
3844
4965
|
namespaces: string[];
|
|
@@ -3856,7 +4977,8 @@ test("HTTP convergence completion forwards one authenticated namespace batch ref
|
|
|
3856
4977
|
principal: "writer",
|
|
3857
4978
|
adminConsoleEnabled: false,
|
|
3858
4979
|
});
|
|
3859
|
-
const route =
|
|
4980
|
+
const route =
|
|
4981
|
+
"/remnic/v1/offline-sync/convergence-complete?namespace=team&namespace=shared";
|
|
3860
4982
|
|
|
3861
4983
|
const status = await server.start();
|
|
3862
4984
|
try {
|
|
@@ -3879,11 +5001,13 @@ test("HTTP convergence completion forwards one authenticated namespace batch ref
|
|
|
3879
5001
|
namespaces: ["team", "shared"],
|
|
3880
5002
|
refreshed: true,
|
|
3881
5003
|
});
|
|
3882
|
-
assert.deepEqual(calls, [
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
5004
|
+
assert.deepEqual(calls, [
|
|
5005
|
+
{
|
|
5006
|
+
namespaces: ["team", "shared"],
|
|
5007
|
+
principal: "writer",
|
|
5008
|
+
sourceId: "remnic-converge",
|
|
5009
|
+
},
|
|
5010
|
+
]);
|
|
3887
5011
|
} finally {
|
|
3888
5012
|
await server.stop();
|
|
3889
5013
|
}
|
|
@@ -3901,21 +5025,31 @@ test("HTTP convergence completion accepts either receiver mutation grant", async
|
|
|
3901
5025
|
service,
|
|
3902
5026
|
port: 0,
|
|
3903
5027
|
authTokenEntriesGetter: () => [
|
|
3904
|
-
{
|
|
3905
|
-
|
|
3906
|
-
|
|
5028
|
+
{
|
|
5029
|
+
token: "file-token",
|
|
5030
|
+
capabilities: { version: 1, ops: ["offline_sync_apply_file_content"] },
|
|
5031
|
+
},
|
|
5032
|
+
{
|
|
5033
|
+
token: "delete-token",
|
|
5034
|
+
capabilities: { version: 1, ops: ["offline_sync_apply"] },
|
|
5035
|
+
},
|
|
5036
|
+
{
|
|
5037
|
+
token: "read-token",
|
|
5038
|
+
capabilities: { version: 1, ops: ["offline_sync_snapshot"] },
|
|
5039
|
+
},
|
|
3907
5040
|
],
|
|
3908
5041
|
adminConsoleEnabled: false,
|
|
3909
5042
|
});
|
|
3910
5043
|
const status = await server.start();
|
|
3911
5044
|
const route = `http://127.0.0.1:${status.port}/remnic/v1/offline-sync/convergence-complete?namespace=team`;
|
|
3912
|
-
const complete = (token: string) =>
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
5045
|
+
const complete = (token: string) =>
|
|
5046
|
+
fetch(route, {
|
|
5047
|
+
method: "POST",
|
|
5048
|
+
headers: {
|
|
5049
|
+
authorization: `Bearer ${token}`,
|
|
5050
|
+
"x-remnic-source-id": encodeURIComponent("remnic-converge"),
|
|
5051
|
+
},
|
|
5052
|
+
});
|
|
3919
5053
|
|
|
3920
5054
|
try {
|
|
3921
5055
|
const fileResponse = await complete("file-token");
|
|
@@ -3966,9 +5100,15 @@ test("HTTP convergence completion enforces the per-principal write rate limit",
|
|
|
3966
5100
|
const second = await fetch(route, requestOptions);
|
|
3967
5101
|
assert.equal(second.status, 429);
|
|
3968
5102
|
const rateLimited: unknown = await second.json();
|
|
3969
|
-
assert.ok(
|
|
5103
|
+
assert.ok(
|
|
5104
|
+
rateLimited && typeof rateLimited === "object" && "code" in rateLimited,
|
|
5105
|
+
);
|
|
3970
5106
|
assert.equal(rateLimited.code, "write_rate_limited");
|
|
3971
|
-
assert.equal(
|
|
5107
|
+
assert.equal(
|
|
5108
|
+
completions,
|
|
5109
|
+
1,
|
|
5110
|
+
"the rate-limited completion must not reach the service",
|
|
5111
|
+
);
|
|
3972
5112
|
} finally {
|
|
3973
5113
|
await server.stop();
|
|
3974
5114
|
}
|
|
@@ -3991,7 +5131,10 @@ test("HTTP convergence completion denies scoped tokens that omit the namespace",
|
|
|
3991
5131
|
service,
|
|
3992
5132
|
port: 0,
|
|
3993
5133
|
authTokenEntriesGetter: () => [
|
|
3994
|
-
{
|
|
5134
|
+
{
|
|
5135
|
+
token: "scoped-token",
|
|
5136
|
+
capabilities: { version: 1, namespaces: ["team"] },
|
|
5137
|
+
},
|
|
3995
5138
|
],
|
|
3996
5139
|
adminConsoleEnabled: false,
|
|
3997
5140
|
});
|
|
@@ -4011,14 +5154,20 @@ test("HTTP convergence completion denies scoped tokens that omit the namespace",
|
|
|
4011
5154
|
|
|
4012
5155
|
assert.equal(response.status, 403);
|
|
4013
5156
|
await response.text();
|
|
4014
|
-
assert.equal(
|
|
5157
|
+
assert.equal(
|
|
5158
|
+
completions,
|
|
5159
|
+
0,
|
|
5160
|
+
"the denied completion must not reach the service",
|
|
5161
|
+
);
|
|
4015
5162
|
} finally {
|
|
4016
5163
|
await server.stop();
|
|
4017
5164
|
}
|
|
4018
5165
|
});
|
|
4019
5166
|
|
|
4020
5167
|
test("HTTP external wiki search aliases return the same cited result", async () => {
|
|
4021
|
-
const rootDir = await mkdtemp(
|
|
5168
|
+
const rootDir = await mkdtemp(
|
|
5169
|
+
path.join(os.tmpdir(), "remnic-http-external-wiki-"),
|
|
5170
|
+
);
|
|
4022
5171
|
await mkdir(path.join(rootDir, "wiki"), { recursive: true });
|
|
4023
5172
|
await writeFile(
|
|
4024
5173
|
path.join(rootDir, "INDEX.md"),
|
|
@@ -4032,15 +5181,17 @@ test("HTTP external wiki search aliases return the same cited result", async ()
|
|
|
4032
5181
|
);
|
|
4033
5182
|
const service = {
|
|
4034
5183
|
configRef: {
|
|
4035
|
-
externalWikis: [
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
5184
|
+
externalWikis: [
|
|
5185
|
+
{
|
|
5186
|
+
id: "planning",
|
|
5187
|
+
rootDir,
|
|
5188
|
+
enabled: true,
|
|
5189
|
+
pagesDir: "wiki",
|
|
5190
|
+
indexFile: "INDEX.md",
|
|
5191
|
+
indexInQmd: false,
|
|
5192
|
+
includeInDefaultRecall: false,
|
|
5193
|
+
},
|
|
5194
|
+
],
|
|
4044
5195
|
},
|
|
4045
5196
|
} as unknown as EngramAccessService;
|
|
4046
5197
|
const server = new EngramAccessHttpServer({
|
|
@@ -4054,38 +5205,48 @@ test("HTTP external wiki search aliases return the same cited result", async ()
|
|
|
4054
5205
|
try {
|
|
4055
5206
|
let canonicalBody: unknown;
|
|
4056
5207
|
for (const prefix of ["remnic", "engram"]) {
|
|
4057
|
-
const response = await fetch(
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
5208
|
+
const response = await fetch(
|
|
5209
|
+
`http://127.0.0.1:${status.port}/${prefix}/v1/external-wikis/search`,
|
|
5210
|
+
{
|
|
5211
|
+
method: "POST",
|
|
5212
|
+
headers: {
|
|
5213
|
+
authorization: "Bearer test-token",
|
|
5214
|
+
"content-type": "application/json",
|
|
5215
|
+
},
|
|
5216
|
+
body: JSON.stringify({
|
|
5217
|
+
query: "deterministic planner",
|
|
5218
|
+
wikiId: "planning",
|
|
5219
|
+
limit: 2,
|
|
5220
|
+
maxCharsPerHit: 200,
|
|
5221
|
+
}),
|
|
4062
5222
|
},
|
|
4063
|
-
|
|
4064
|
-
query: "deterministic planner",
|
|
4065
|
-
wikiId: "planning",
|
|
4066
|
-
limit: 2,
|
|
4067
|
-
maxCharsPerHit: 200,
|
|
4068
|
-
}),
|
|
4069
|
-
});
|
|
5223
|
+
);
|
|
4070
5224
|
assert.equal(response.status, 200);
|
|
4071
5225
|
const body: unknown = await response.json();
|
|
4072
5226
|
if (canonicalBody === undefined) canonicalBody = body;
|
|
4073
5227
|
else assert.deepEqual(body, canonicalBody);
|
|
4074
5228
|
}
|
|
4075
|
-
assert.ok(
|
|
5229
|
+
assert.ok(
|
|
5230
|
+
canonicalBody &&
|
|
5231
|
+
typeof canonicalBody === "object" &&
|
|
5232
|
+
"hits" in canonicalBody,
|
|
5233
|
+
);
|
|
4076
5234
|
assert.ok(Array.isArray(canonicalBody.hits));
|
|
4077
5235
|
const firstHit = canonicalBody.hits[0];
|
|
4078
5236
|
assert.ok(firstHit && typeof firstHit === "object" && "path" in firstHit);
|
|
4079
5237
|
assert.equal(firstHit.path, "wiki/planning.md");
|
|
4080
5238
|
|
|
4081
|
-
const invalid = await fetch(
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
5239
|
+
const invalid = await fetch(
|
|
5240
|
+
`http://127.0.0.1:${status.port}/remnic/v1/external-wikis/search`,
|
|
5241
|
+
{
|
|
5242
|
+
method: "POST",
|
|
5243
|
+
headers: {
|
|
5244
|
+
authorization: "Bearer test-token",
|
|
5245
|
+
"content-type": "application/json",
|
|
5246
|
+
},
|
|
5247
|
+
body: JSON.stringify({ query: " " }),
|
|
4086
5248
|
},
|
|
4087
|
-
|
|
4088
|
-
});
|
|
5249
|
+
);
|
|
4089
5250
|
assert.equal(invalid.status, 400);
|
|
4090
5251
|
} finally {
|
|
4091
5252
|
await server.stop();
|
|
@@ -4100,7 +5261,13 @@ test("HTTP memory search aliases dispatch the boundary operation identically", a
|
|
|
4100
5261
|
calls.push(request);
|
|
4101
5262
|
return {
|
|
4102
5263
|
query: request.query,
|
|
4103
|
-
results: [
|
|
5264
|
+
results: [
|
|
5265
|
+
{
|
|
5266
|
+
path: "facts/alice.md",
|
|
5267
|
+
score: 0.71,
|
|
5268
|
+
snippet: "alice prefers dark mode",
|
|
5269
|
+
},
|
|
5270
|
+
],
|
|
4104
5271
|
count: 1,
|
|
4105
5272
|
};
|
|
4106
5273
|
},
|
|
@@ -4116,22 +5283,36 @@ test("HTTP memory search aliases dispatch the boundary operation identically", a
|
|
|
4116
5283
|
try {
|
|
4117
5284
|
let canonicalBody: unknown;
|
|
4118
5285
|
for (const prefix of ["engram", "remnic"]) {
|
|
4119
|
-
const response = await fetch(
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
5286
|
+
const response = await fetch(
|
|
5287
|
+
`http://127.0.0.1:${status.port}/${prefix}/v1/memories/search`,
|
|
5288
|
+
{
|
|
5289
|
+
method: "POST",
|
|
5290
|
+
headers: {
|
|
5291
|
+
authorization: "Bearer test-token",
|
|
5292
|
+
"content-type": "application/json",
|
|
5293
|
+
},
|
|
5294
|
+
body: JSON.stringify({ query: "dark mode", maxResults: 3 }),
|
|
4124
5295
|
},
|
|
4125
|
-
|
|
4126
|
-
});
|
|
5296
|
+
);
|
|
4127
5297
|
assert.equal(response.status, 200);
|
|
4128
5298
|
const body: unknown = await response.json();
|
|
4129
5299
|
if (canonicalBody === undefined) canonicalBody = body;
|
|
4130
|
-
else
|
|
5300
|
+
else
|
|
5301
|
+
assert.deepEqual(
|
|
5302
|
+
body,
|
|
5303
|
+
canonicalBody,
|
|
5304
|
+
"both prefixes must return one shape",
|
|
5305
|
+
);
|
|
4131
5306
|
}
|
|
4132
5307
|
assert.deepEqual(canonicalBody, {
|
|
4133
5308
|
query: "dark mode",
|
|
4134
|
-
results: [
|
|
5309
|
+
results: [
|
|
5310
|
+
{
|
|
5311
|
+
path: "facts/alice.md",
|
|
5312
|
+
score: 0.71,
|
|
5313
|
+
snippet: "alice prefers dark mode",
|
|
5314
|
+
},
|
|
5315
|
+
],
|
|
4135
5316
|
count: 1,
|
|
4136
5317
|
});
|
|
4137
5318
|
assert.equal(calls.length, 2);
|
|
@@ -4174,9 +5355,17 @@ test("HTTP memory search rejects an invalid body before service dispatch", async
|
|
|
4174
5355
|
body: JSON.stringify(body),
|
|
4175
5356
|
});
|
|
4176
5357
|
try {
|
|
4177
|
-
assert.equal(
|
|
5358
|
+
assert.equal(
|
|
5359
|
+
(await post({ query: " " })).status,
|
|
5360
|
+
400,
|
|
5361
|
+
"blank query is rejected",
|
|
5362
|
+
);
|
|
4178
5363
|
assert.equal((await post({})).status, 400, "a missing query is rejected");
|
|
4179
|
-
assert.equal(
|
|
5364
|
+
assert.equal(
|
|
5365
|
+
(await post({ query: "ok", maxResults: 0 })).status,
|
|
5366
|
+
400,
|
|
5367
|
+
"maxResults must be >= 1",
|
|
5368
|
+
);
|
|
4180
5369
|
assert.equal(
|
|
4181
5370
|
(await post({ query: "ok", namespace: 123 })).status,
|
|
4182
5371
|
400,
|
|
@@ -4212,11 +5401,19 @@ test("HTTP memory search enforces token op and namespace allow-lists", async ()
|
|
|
4212
5401
|
{ token: "wrong-op", capabilities: { version: 1, ops: ["recall"] } },
|
|
4213
5402
|
{
|
|
4214
5403
|
token: "wrong-namespace",
|
|
4215
|
-
capabilities: {
|
|
5404
|
+
capabilities: {
|
|
5405
|
+
version: 1,
|
|
5406
|
+
ops: ["memory_search"],
|
|
5407
|
+
namespaces: ["other"],
|
|
5408
|
+
},
|
|
4216
5409
|
},
|
|
4217
5410
|
{
|
|
4218
5411
|
token: "reader",
|
|
4219
|
-
capabilities: {
|
|
5412
|
+
capabilities: {
|
|
5413
|
+
version: 1,
|
|
5414
|
+
ops: ["memory_search"],
|
|
5415
|
+
namespaces: ["team"],
|
|
5416
|
+
},
|
|
4220
5417
|
},
|
|
4221
5418
|
],
|
|
4222
5419
|
adminConsoleEnabled: false,
|
|
@@ -4232,14 +5429,20 @@ test("HTTP memory search enforces token op and namespace allow-lists", async ()
|
|
|
4232
5429
|
body: JSON.stringify(body),
|
|
4233
5430
|
});
|
|
4234
5431
|
try {
|
|
4235
|
-
assert.equal(
|
|
5432
|
+
assert.equal(
|
|
5433
|
+
(await post("wrong-op", { query: "q", namespace: "team" })).status,
|
|
5434
|
+
403,
|
|
5435
|
+
);
|
|
4236
5436
|
assert.equal(
|
|
4237
5437
|
(await post("wrong-namespace", { query: "q", namespace: "team" })).status,
|
|
4238
5438
|
403,
|
|
4239
5439
|
"a body namespace outside the token allow-list must fail closed",
|
|
4240
5440
|
);
|
|
4241
5441
|
assert.equal(calls.length, 0, "no denied request may reach the service");
|
|
4242
|
-
assert.equal(
|
|
5442
|
+
assert.equal(
|
|
5443
|
+
(await post("reader", { query: "q", namespace: "team" })).status,
|
|
5444
|
+
200,
|
|
5445
|
+
);
|
|
4243
5446
|
assert.equal(calls.length, 1);
|
|
4244
5447
|
assert.equal(
|
|
4245
5448
|
(calls[0] as { namespace?: unknown }).namespace,
|
|
@@ -4267,9 +5470,16 @@ test("HTTP memory search binds a scoped token to one namespace instead of fannin
|
|
|
4267
5470
|
authTokenEntriesGetter: () => [
|
|
4268
5471
|
{
|
|
4269
5472
|
token: "scoped",
|
|
4270
|
-
capabilities: {
|
|
5473
|
+
capabilities: {
|
|
5474
|
+
version: 1,
|
|
5475
|
+
ops: ["memory_search"],
|
|
5476
|
+
namespaces: ["generalist"],
|
|
5477
|
+
},
|
|
5478
|
+
},
|
|
5479
|
+
{
|
|
5480
|
+
token: "unrestricted",
|
|
5481
|
+
capabilities: { version: 1, ops: ["memory_search"] },
|
|
4271
5482
|
},
|
|
4272
|
-
{ token: "unrestricted", capabilities: { version: 1, ops: ["memory_search"] } },
|
|
4273
5483
|
],
|
|
4274
5484
|
adminConsoleEnabled: false,
|
|
4275
5485
|
});
|
|
@@ -4277,7 +5487,10 @@ test("HTTP memory search binds a scoped token to one namespace instead of fannin
|
|
|
4277
5487
|
const post = (token: string) =>
|
|
4278
5488
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4279
5489
|
method: "POST",
|
|
4280
|
-
headers: {
|
|
5490
|
+
headers: {
|
|
5491
|
+
authorization: `Bearer ${token}`,
|
|
5492
|
+
"content-type": "application/json",
|
|
5493
|
+
},
|
|
4281
5494
|
body: JSON.stringify({ query: "q" }),
|
|
4282
5495
|
});
|
|
4283
5496
|
try {
|
|
@@ -4312,7 +5525,14 @@ test("HTTP memory search forwards a validated ranking mode and trims the namespa
|
|
|
4312
5525
|
port: 0,
|
|
4313
5526
|
principal: "reader",
|
|
4314
5527
|
authTokenEntriesGetter: () => [
|
|
4315
|
-
{
|
|
5528
|
+
{
|
|
5529
|
+
token: "scoped",
|
|
5530
|
+
capabilities: {
|
|
5531
|
+
version: 1,
|
|
5532
|
+
ops: ["memory_search"],
|
|
5533
|
+
namespaces: ["team"],
|
|
5534
|
+
},
|
|
5535
|
+
},
|
|
4316
5536
|
],
|
|
4317
5537
|
adminConsoleEnabled: false,
|
|
4318
5538
|
});
|
|
@@ -4320,17 +5540,25 @@ test("HTTP memory search forwards a validated ranking mode and trims the namespa
|
|
|
4320
5540
|
const post = (body: unknown) =>
|
|
4321
5541
|
fetch(`http://127.0.0.1:${status.port}/engram/v1/memories/search`, {
|
|
4322
5542
|
method: "POST",
|
|
4323
|
-
headers: {
|
|
5543
|
+
headers: {
|
|
5544
|
+
authorization: "Bearer scoped",
|
|
5545
|
+
"content-type": "application/json",
|
|
5546
|
+
},
|
|
4324
5547
|
body: JSON.stringify(body),
|
|
4325
5548
|
});
|
|
4326
5549
|
try {
|
|
4327
5550
|
assert.equal(
|
|
4328
|
-
(await post({ query: "q", namespace: " team ", mode: "vector" }))
|
|
5551
|
+
(await post({ query: "q", namespace: " team ", mode: "vector" }))
|
|
5552
|
+
.status,
|
|
4329
5553
|
200,
|
|
4330
5554
|
"surrounding whitespace must not 403 a namespace the token allows",
|
|
4331
5555
|
);
|
|
4332
5556
|
assert.equal(calls[0]?.namespace, "team");
|
|
4333
|
-
assert.equal(
|
|
5557
|
+
assert.equal(
|
|
5558
|
+
calls[0]?.mode,
|
|
5559
|
+
"vector",
|
|
5560
|
+
"the ranking mode reaches the service",
|
|
5561
|
+
);
|
|
4334
5562
|
assert.equal(
|
|
4335
5563
|
(await post({ query: "q", namespace: "team", mode: "vsearch" })).status,
|
|
4336
5564
|
400,
|
|
@@ -4353,7 +5581,8 @@ test("HTTP memory search excludes artifacts and tops up to a full page", async (
|
|
|
4353
5581
|
{ path: "facts/one.md", score: 0.5, snippet: "one" },
|
|
4354
5582
|
{ path: "facts/two.md", score: 0.4, snippet: "two" },
|
|
4355
5583
|
];
|
|
4356
|
-
const { runScopedMemorySearch } =
|
|
5584
|
+
const { runScopedMemorySearch } =
|
|
5585
|
+
await import("./access-memory-search-fanout.js");
|
|
4357
5586
|
const results = await runScopedMemorySearch({
|
|
4358
5587
|
query: "q",
|
|
4359
5588
|
budget: 2,
|
|
@@ -4361,6 +5590,7 @@ test("HTTP memory search excludes artifacts and tops up to a full page", async (
|
|
|
4361
5590
|
authorizeScope: () => {},
|
|
4362
5591
|
namespacesEnabled: false,
|
|
4363
5592
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
5593
|
+
filterPrivate: async (hits) => hits,
|
|
4364
5594
|
flatCorpus: async (limit) => {
|
|
4365
5595
|
seen.push(limit ?? -1);
|
|
4366
5596
|
return corpus.slice(0, limit);
|
|
@@ -4372,12 +5602,17 @@ test("HTTP memory search excludes artifacts and tops up to a full page", async (
|
|
|
4372
5602
|
["facts/one.md", "facts/two.md"],
|
|
4373
5603
|
"a full page of real memories",
|
|
4374
5604
|
);
|
|
4375
|
-
assert.deepEqual(
|
|
5605
|
+
assert.deepEqual(
|
|
5606
|
+
seen,
|
|
5607
|
+
[2, 4, 8],
|
|
5608
|
+
"doubles the candidate request until the budget is met",
|
|
5609
|
+
);
|
|
4376
5610
|
});
|
|
4377
5611
|
|
|
4378
5612
|
test("HTTP memory search stops topping up when the corpus is exhausted", async () => {
|
|
4379
5613
|
const seen: number[] = [];
|
|
4380
|
-
const { runScopedMemorySearch } =
|
|
5614
|
+
const { runScopedMemorySearch } =
|
|
5615
|
+
await import("./access-memory-search-fanout.js");
|
|
4381
5616
|
const results = await runScopedMemorySearch({
|
|
4382
5617
|
query: "q",
|
|
4383
5618
|
budget: 5,
|
|
@@ -4385,6 +5620,7 @@ test("HTTP memory search stops topping up when the corpus is exhausted", async (
|
|
|
4385
5620
|
authorizeScope: () => {},
|
|
4386
5621
|
namespacesEnabled: false,
|
|
4387
5622
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
5623
|
+
filterPrivate: async (hits) => hits,
|
|
4388
5624
|
flatCorpus: async (limit) => {
|
|
4389
5625
|
seen.push(limit ?? -1);
|
|
4390
5626
|
return [{ path: "facts/only.md", score: 0.5, snippet: "only" }];
|
|
@@ -4392,14 +5628,19 @@ test("HTTP memory search stops topping up when the corpus is exhausted", async (
|
|
|
4392
5628
|
namespaced: async () => [],
|
|
4393
5629
|
});
|
|
4394
5630
|
assert.equal(results.length, 1);
|
|
4395
|
-
assert.deepEqual(
|
|
5631
|
+
assert.deepEqual(
|
|
5632
|
+
seen,
|
|
5633
|
+
[5],
|
|
5634
|
+
"a short page means there is nothing left to fetch",
|
|
5635
|
+
);
|
|
4396
5636
|
});
|
|
4397
5637
|
|
|
4398
5638
|
test("HTTP memory search omits maxResults when the caller named no budget", async () => {
|
|
4399
5639
|
// The wire stays exactly as it was for the default request: the backend's
|
|
4400
5640
|
// own page size, not a resolved number. Top-up rounds are explicit.
|
|
4401
5641
|
const seen: Array<number | undefined> = [];
|
|
4402
|
-
const { runScopedMemorySearch } =
|
|
5642
|
+
const { runScopedMemorySearch } =
|
|
5643
|
+
await import("./access-memory-search-fanout.js");
|
|
4403
5644
|
const results = await runScopedMemorySearch({
|
|
4404
5645
|
query: "q",
|
|
4405
5646
|
budget: 3,
|
|
@@ -4407,6 +5648,7 @@ test("HTTP memory search omits maxResults when the caller named no budget", asyn
|
|
|
4407
5648
|
authorizeScope: () => {},
|
|
4408
5649
|
namespacesEnabled: false,
|
|
4409
5650
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
5651
|
+
filterPrivate: async (hits) => hits,
|
|
4410
5652
|
flatCorpus: async (limit) => {
|
|
4411
5653
|
seen.push(limit);
|
|
4412
5654
|
const corpus = [
|
|
@@ -4419,12 +5661,15 @@ test("HTTP memory search omits maxResults when the caller named no budget", asyn
|
|
|
4419
5661
|
},
|
|
4420
5662
|
namespaced: async () => [],
|
|
4421
5663
|
});
|
|
4422
|
-
assert.deepEqual(
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
"
|
|
4426
|
-
|
|
4427
|
-
|
|
5664
|
+
assert.deepEqual(
|
|
5665
|
+
seen,
|
|
5666
|
+
[undefined, 6],
|
|
5667
|
+
"first request carries no cap, the top-up does",
|
|
5668
|
+
);
|
|
5669
|
+
assert.deepEqual(
|
|
5670
|
+
results.map((hit) => hit.path),
|
|
5671
|
+
["facts/one.md", "facts/two.md", "facts/three.md"],
|
|
5672
|
+
);
|
|
4428
5673
|
});
|
|
4429
5674
|
|
|
4430
5675
|
test("HTTP memory search keeps topping up past four excluded pages", async () => {
|
|
@@ -4440,7 +5685,8 @@ test("HTTP memory search keeps topping up past four excluded pages", async () =>
|
|
|
4440
5685
|
{ path: "facts/two.md", score: 0.09, snippet: "two" },
|
|
4441
5686
|
];
|
|
4442
5687
|
const seen: Array<number | undefined> = [];
|
|
4443
|
-
const { runScopedMemorySearch } =
|
|
5688
|
+
const { runScopedMemorySearch } =
|
|
5689
|
+
await import("./access-memory-search-fanout.js");
|
|
4444
5690
|
const results = await runScopedMemorySearch({
|
|
4445
5691
|
query: "q",
|
|
4446
5692
|
budget: 2,
|
|
@@ -4448,21 +5694,30 @@ test("HTTP memory search keeps topping up past four excluded pages", async () =>
|
|
|
4448
5694
|
authorizeScope: () => {},
|
|
4449
5695
|
namespacesEnabled: false,
|
|
4450
5696
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
5697
|
+
filterPrivate: async (hits) => hits,
|
|
4451
5698
|
flatCorpus: async (limit) => {
|
|
4452
5699
|
seen.push(limit);
|
|
4453
5700
|
return corpus.slice(0, limit);
|
|
4454
5701
|
},
|
|
4455
5702
|
namespaced: async () => [],
|
|
4456
5703
|
});
|
|
4457
|
-
assert.deepEqual(
|
|
4458
|
-
|
|
5704
|
+
assert.deepEqual(
|
|
5705
|
+
seen,
|
|
5706
|
+
[2, 4, 8, 16, 32, 64],
|
|
5707
|
+
"doubles past four rounds until the budget is met",
|
|
5708
|
+
);
|
|
5709
|
+
assert.deepEqual(
|
|
5710
|
+
results.map((hit) => hit.path),
|
|
5711
|
+
["facts/one.md", "facts/two.md"],
|
|
5712
|
+
);
|
|
4459
5713
|
});
|
|
4460
5714
|
|
|
4461
5715
|
test("HTTP memory search stops at the candidate ceiling", async () => {
|
|
4462
5716
|
// A backend that always returns a full page of excluded hits must not be
|
|
4463
5717
|
// asked for an unbounded number of candidates.
|
|
4464
5718
|
const seen: Array<number | undefined> = [];
|
|
4465
|
-
const { runScopedMemorySearch } =
|
|
5719
|
+
const { runScopedMemorySearch } =
|
|
5720
|
+
await import("./access-memory-search-fanout.js");
|
|
4466
5721
|
const results = await runScopedMemorySearch({
|
|
4467
5722
|
query: "q",
|
|
4468
5723
|
budget: 1,
|
|
@@ -4470,6 +5725,7 @@ test("HTTP memory search stops at the candidate ceiling", async () => {
|
|
|
4470
5725
|
authorizeScope: () => {},
|
|
4471
5726
|
namespacesEnabled: false,
|
|
4472
5727
|
isExcluded: () => true,
|
|
5728
|
+
filterPrivate: async (hits) => hits,
|
|
4473
5729
|
flatCorpus: async (limit) => {
|
|
4474
5730
|
seen.push(limit);
|
|
4475
5731
|
return Array.from({ length: limit ?? 0 }, (_, index) => ({
|
|
@@ -4484,7 +5740,11 @@ test("HTTP memory search stops at the candidate ceiling", async () => {
|
|
|
4484
5740
|
// The cap is ABSOLUTE, not a multiple of the budget: a small request whose
|
|
4485
5741
|
// hits are all excluded still walks far enough to prove the corpus holds
|
|
4486
5742
|
// nothing for it, then stops on the backend-safety bound.
|
|
4487
|
-
assert.equal(
|
|
5743
|
+
assert.equal(
|
|
5744
|
+
seen.at(-1),
|
|
5745
|
+
25_000,
|
|
5746
|
+
"the last request lands exactly on the cap",
|
|
5747
|
+
);
|
|
4488
5748
|
assert.ok(seen.length < 20, "and the loop terminates");
|
|
4489
5749
|
});
|
|
4490
5750
|
|
|
@@ -4492,7 +5752,8 @@ test("HTTP memory search scales its ceiling above a large requested budget", asy
|
|
|
4492
5752
|
// A fixed 1000 ceiling would stop at or below a 2000-result request, so one
|
|
4493
5753
|
// excluded hit in the first page could never be replaced.
|
|
4494
5754
|
const seen: Array<number | undefined> = [];
|
|
4495
|
-
const { runScopedMemorySearch } =
|
|
5755
|
+
const { runScopedMemorySearch } =
|
|
5756
|
+
await import("./access-memory-search-fanout.js");
|
|
4496
5757
|
const results = await runScopedMemorySearch({
|
|
4497
5758
|
query: "q",
|
|
4498
5759
|
budget: 2_000,
|
|
@@ -4500,6 +5761,7 @@ test("HTTP memory search scales its ceiling above a large requested budget", asy
|
|
|
4500
5761
|
authorizeScope: () => {},
|
|
4501
5762
|
namespacesEnabled: false,
|
|
4502
5763
|
isExcluded: (memoryPath) => memoryPath === "artifacts/a.md",
|
|
5764
|
+
filterPrivate: async (hits) => hits,
|
|
4503
5765
|
flatCorpus: async (limit) => {
|
|
4504
5766
|
seen.push(limit);
|
|
4505
5767
|
const size = limit ?? 0;
|
|
@@ -4515,7 +5777,11 @@ test("HTTP memory search scales its ceiling above a large requested budget", asy
|
|
|
4515
5777
|
namespaced: async () => [],
|
|
4516
5778
|
});
|
|
4517
5779
|
assert.deepEqual(seen, [2_000, 4_000], "tops up past the requested budget");
|
|
4518
|
-
assert.equal(
|
|
5780
|
+
assert.equal(
|
|
5781
|
+
results.length,
|
|
5782
|
+
2_000,
|
|
5783
|
+
"the requested page is delivered in full",
|
|
5784
|
+
);
|
|
4519
5785
|
});
|
|
4520
5786
|
|
|
4521
5787
|
test("HTTP memory search keeps the backend page size when no limit is named", async () => {
|
|
@@ -4523,7 +5789,8 @@ test("HTTP memory search keeps the backend page size when no limit is named", as
|
|
|
4523
5789
|
// Treating the cap as the target made a FULL page look short and reissued
|
|
4524
5790
|
// the query, returning 8 where the same request used to return 6.
|
|
4525
5791
|
const seen: Array<number | undefined> = [];
|
|
4526
|
-
const { runScopedMemorySearch } =
|
|
5792
|
+
const { runScopedMemorySearch } =
|
|
5793
|
+
await import("./access-memory-search-fanout.js");
|
|
4527
5794
|
const results = await runScopedMemorySearch({
|
|
4528
5795
|
query: "q",
|
|
4529
5796
|
budget: 8,
|
|
@@ -4531,6 +5798,7 @@ test("HTTP memory search keeps the backend page size when no limit is named", as
|
|
|
4531
5798
|
authorizeScope: () => {},
|
|
4532
5799
|
namespacesEnabled: false,
|
|
4533
5800
|
isExcluded: () => false,
|
|
5801
|
+
filterPrivate: async (hits) => hits,
|
|
4534
5802
|
flatCorpus: async (limit) => {
|
|
4535
5803
|
seen.push(limit);
|
|
4536
5804
|
const size = limit ?? 6;
|
|
@@ -4550,7 +5818,8 @@ test("HTTP memory search still tops up a thinned default page", async () => {
|
|
|
4550
5818
|
// Same shape, but an excluded hit really does shorten the page: the target
|
|
4551
5819
|
// is the backend page size, so it tops up to restore it.
|
|
4552
5820
|
const seen: Array<number | undefined> = [];
|
|
4553
|
-
const { runScopedMemorySearch } =
|
|
5821
|
+
const { runScopedMemorySearch } =
|
|
5822
|
+
await import("./access-memory-search-fanout.js");
|
|
4554
5823
|
const results = await runScopedMemorySearch({
|
|
4555
5824
|
query: "q",
|
|
4556
5825
|
budget: 8,
|
|
@@ -4558,6 +5827,7 @@ test("HTTP memory search still tops up a thinned default page", async () => {
|
|
|
4558
5827
|
authorizeScope: () => {},
|
|
4559
5828
|
namespacesEnabled: false,
|
|
4560
5829
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
5830
|
+
filterPrivate: async (hits) => hits,
|
|
4561
5831
|
flatCorpus: async (limit) => {
|
|
4562
5832
|
seen.push(limit);
|
|
4563
5833
|
const size = limit ?? 6;
|
|
@@ -4572,7 +5842,11 @@ test("HTTP memory search still tops up a thinned default page", async () => {
|
|
|
4572
5842
|
},
|
|
4573
5843
|
namespaced: async () => [],
|
|
4574
5844
|
});
|
|
4575
|
-
assert.deepEqual(
|
|
5845
|
+
assert.deepEqual(
|
|
5846
|
+
seen,
|
|
5847
|
+
[undefined, 12],
|
|
5848
|
+
"tops up against the backend page size",
|
|
5849
|
+
);
|
|
4576
5850
|
assert.equal(results.length, 6);
|
|
4577
5851
|
});
|
|
4578
5852
|
|
|
@@ -4580,9 +5854,8 @@ test("search keeps archived memories but still excludes the dedicated surfaces",
|
|
|
4580
5854
|
// The lifecycle reserves archived memories for explicit read/search
|
|
4581
5855
|
// surfaces, so a ranked search must return them - while artifacts, activity
|
|
4582
5856
|
// digests, and meeting records stay on their own paths.
|
|
4583
|
-
const { isSearchExcludedPath, isGenericRecallExcludedPath } =
|
|
4584
|
-
"./orchestration/generic-recall-paths.js"
|
|
4585
|
-
);
|
|
5857
|
+
const { isSearchExcludedPath, isGenericRecallExcludedPath } =
|
|
5858
|
+
await import("./orchestration/generic-recall-paths.js");
|
|
4586
5859
|
assert.equal(
|
|
4587
5860
|
isSearchExcludedPath("archive/2026-01/fact-1.md"),
|
|
4588
5861
|
false,
|
|
@@ -4597,7 +5870,8 @@ test("search keeps archived memories but still excludes the dedicated surfaces",
|
|
|
4597
5870
|
assert.equal(isSearchExcludedPath("artifacts/report.md"), true);
|
|
4598
5871
|
assert.equal(isGenericRecallExcludedPath("artifacts/report.md"), true);
|
|
4599
5872
|
// And the wiring uses the search predicate, so an archived hit survives.
|
|
4600
|
-
const { runScopedMemorySearch } =
|
|
5873
|
+
const { runScopedMemorySearch } =
|
|
5874
|
+
await import("./access-memory-search-fanout.js");
|
|
4601
5875
|
const results = await runScopedMemorySearch({
|
|
4602
5876
|
query: "q",
|
|
4603
5877
|
budget: 5,
|
|
@@ -4605,17 +5879,22 @@ test("search keeps archived memories but still excludes the dedicated surfaces",
|
|
|
4605
5879
|
authorizeScope: () => {},
|
|
4606
5880
|
namespacesEnabled: false,
|
|
4607
5881
|
isExcluded: (memoryPath) => isSearchExcludedPath(memoryPath),
|
|
5882
|
+
filterPrivate: async (hits) => hits,
|
|
4608
5883
|
flatCorpus: async () => [
|
|
4609
5884
|
{ path: "archive/2026-01/fact-1.md", score: 0.9, snippet: "archived" },
|
|
4610
5885
|
{ path: "artifacts/report.md", score: 0.8, snippet: "artifact" },
|
|
4611
5886
|
],
|
|
4612
5887
|
namespaced: async () => [],
|
|
4613
5888
|
});
|
|
4614
|
-
assert.deepEqual(
|
|
5889
|
+
assert.deepEqual(
|
|
5890
|
+
results.map((hit) => hit.path),
|
|
5891
|
+
["archive/2026-01/fact-1.md"],
|
|
5892
|
+
);
|
|
4615
5893
|
});
|
|
4616
5894
|
|
|
4617
5895
|
test("a collection-qualified QMD path still hits the dedicated-surface exclusions", async () => {
|
|
4618
|
-
const { isSearchExcludedPath } =
|
|
5896
|
+
const { isSearchExcludedPath } =
|
|
5897
|
+
await import("./orchestration/generic-recall-paths.js");
|
|
4619
5898
|
// A flat-corpus QMD transport returns `qmd://<collection>/<path>` or
|
|
4620
5899
|
// `<collection>/<path>`. The activity predicate is root-aware, so an
|
|
4621
5900
|
// un-stripped prefix reads as a NESTED path and the digest leaks into
|
|
@@ -4633,12 +5912,20 @@ test("a collection-qualified QMD path still hits the dedicated-surface exclusion
|
|
|
4633
5912
|
// A namespaced search rewrites hits to absolute paths beneath the
|
|
4634
5913
|
// namespace's own storage root; the digest there must be excluded too.
|
|
4635
5914
|
assert.equal(
|
|
4636
|
-
isSearchExcludedPath(
|
|
5915
|
+
isSearchExcludedPath(
|
|
5916
|
+
"/memory/namespaces/team/activity/2026-08-02.md",
|
|
5917
|
+
policy,
|
|
5918
|
+
"qmd",
|
|
5919
|
+
),
|
|
4637
5920
|
true,
|
|
4638
5921
|
"a digest under a namespace root is still a digest",
|
|
4639
5922
|
);
|
|
4640
5923
|
assert.equal(
|
|
4641
|
-
isSearchExcludedPath(
|
|
5924
|
+
isSearchExcludedPath(
|
|
5925
|
+
"/memory/namespaces/team/facts/proj/activity/2026-08-02.md",
|
|
5926
|
+
policy,
|
|
5927
|
+
"qmd",
|
|
5928
|
+
),
|
|
4642
5929
|
false,
|
|
4643
5930
|
"and an ordinary nested memory under that root stays searchable",
|
|
4644
5931
|
);
|
|
@@ -4696,10 +5983,15 @@ test("search keeps paging while the backend page is full of excluded hits", asyn
|
|
|
4696
5983
|
// indistinguishable from "there is nothing else": a 1,000-row request whose
|
|
4697
5984
|
// first 4,000 hits are artifacts stopped at 4,000 and answered empty while
|
|
4698
5985
|
// valid memories sat at rank 4,001.
|
|
4699
|
-
const { searchWithGenericExclusion } =
|
|
5986
|
+
const { searchWithGenericExclusion } =
|
|
5987
|
+
await import("./access-memory-search-fanout.js");
|
|
4700
5988
|
const corpus = [
|
|
4701
|
-
...Array.from({ length: 4_000 }, (_, index) => ({
|
|
4702
|
-
|
|
5989
|
+
...Array.from({ length: 4_000 }, (_, index) => ({
|
|
5990
|
+
path: `artifacts/a-${index}.md`,
|
|
5991
|
+
})),
|
|
5992
|
+
...Array.from({ length: 50 }, (_, index) => ({
|
|
5993
|
+
path: `facts/f-${index}.md`,
|
|
5994
|
+
})),
|
|
4703
5995
|
];
|
|
4704
5996
|
const limits: Array<number | undefined> = [];
|
|
4705
5997
|
const results = await searchWithGenericExclusion({
|
|
@@ -4709,30 +6001,66 @@ test("search keeps paging while the backend page is full of excluded hits", asyn
|
|
|
4709
6001
|
limits.push(limit);
|
|
4710
6002
|
return corpus.slice(0, limit ?? corpus.length);
|
|
4711
6003
|
},
|
|
6004
|
+
filterPrivate: async (hits) => hits,
|
|
4712
6005
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4713
6006
|
});
|
|
4714
|
-
assert.equal(
|
|
4715
|
-
|
|
6007
|
+
assert.equal(
|
|
6008
|
+
results.length,
|
|
6009
|
+
50,
|
|
6010
|
+
"the memories behind the excluded block are returned",
|
|
6011
|
+
);
|
|
6012
|
+
assert.ok(
|
|
6013
|
+
limits.length > 1,
|
|
6014
|
+
"and it took more than the first page to reach them",
|
|
6015
|
+
);
|
|
4716
6016
|
assert.ok(
|
|
4717
6017
|
limits.every((limit) => (limit ?? 0) <= 25_000),
|
|
4718
6018
|
"while still respecting the absolute backend cap",
|
|
4719
6019
|
);
|
|
4720
6020
|
});
|
|
4721
6021
|
|
|
6022
|
+
test("search removes excluded paths before private-memory resolution", async () => {
|
|
6023
|
+
const { searchWithGenericExclusion } = await import("./access-memory-search-fanout.js");
|
|
6024
|
+
const resolved: string[] = [];
|
|
6025
|
+
const results = await searchWithGenericExclusion({
|
|
6026
|
+
budget: 1,
|
|
6027
|
+
sendInitialLimit: true,
|
|
6028
|
+
search: async () => [
|
|
6029
|
+
{ path: "artifacts/private.md" },
|
|
6030
|
+
{ path: "facts/visible.md" },
|
|
6031
|
+
],
|
|
6032
|
+
filterPrivate: async (hits) => {
|
|
6033
|
+
resolved.push(...hits.map((hit) => hit.path));
|
|
6034
|
+
return hits;
|
|
6035
|
+
},
|
|
6036
|
+
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
6037
|
+
});
|
|
6038
|
+
assert.deepEqual(resolved, ["facts/visible.md"]);
|
|
6039
|
+
assert.deepEqual(results, [{ path: "facts/visible.md" }]);
|
|
6040
|
+
});
|
|
6041
|
+
|
|
4722
6042
|
test("a budget above the backend cap still gets the rows it asked for", async () => {
|
|
4723
6043
|
// The cap protects the backend from an unbounded walk; it must never sit
|
|
4724
6044
|
// BELOW an explicit request, or a large search returns a short page for a
|
|
4725
6045
|
// count the operation deliberately supports.
|
|
4726
|
-
const { searchWithGenericExclusion } =
|
|
6046
|
+
const { searchWithGenericExclusion } =
|
|
6047
|
+
await import("./access-memory-search-fanout.js");
|
|
4727
6048
|
const corpus = [
|
|
4728
6049
|
{ path: "artifacts/excluded.md" },
|
|
4729
|
-
...Array.from({ length: 30_000 }, (_, index) => ({
|
|
6050
|
+
...Array.from({ length: 30_000 }, (_, index) => ({
|
|
6051
|
+
path: `facts/f-${index}.md`,
|
|
6052
|
+
})),
|
|
4730
6053
|
];
|
|
4731
6054
|
const results = await searchWithGenericExclusion({
|
|
4732
6055
|
budget: 30_000,
|
|
4733
6056
|
sendInitialLimit: true,
|
|
4734
6057
|
search: async (limit) => corpus.slice(0, limit ?? corpus.length),
|
|
6058
|
+
filterPrivate: async (hits) => hits,
|
|
4735
6059
|
isExcluded: (memoryPath) => memoryPath.startsWith("artifacts/"),
|
|
4736
6060
|
});
|
|
4737
|
-
assert.equal(
|
|
6061
|
+
assert.equal(
|
|
6062
|
+
results.length,
|
|
6063
|
+
30_000,
|
|
6064
|
+
"the excluded hit was replaced, not subtracted",
|
|
6065
|
+
);
|
|
4738
6066
|
});
|