@remnic/core 9.4.2 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +28 -28
- package/dist/access-boundary.d.ts +27 -8
- package/dist/access-boundary.js +33 -29
- package/dist/access-cli.js +83 -81
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +52 -6
- package/dist/access-http.js +40 -38
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +28 -28
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +28 -28
- package/dist/access-mcp.d.ts +16 -6
- package/dist/access-mcp.js +37 -35
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +28 -28
- package/dist/access-operations-batch.js +33 -32
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +28 -28
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +174 -0
- package/dist/access-token-capabilities.js +33 -0
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +9 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
- package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
- package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
- package/dist/chunk-4GNEDXDI.js.map +1 -0
- package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
- package/dist/chunk-4OKLXWDZ.js.map +1 -0
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
- package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
- package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
- package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
- package/dist/chunk-B3ABVQ36.js.map +1 -0
- package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
- package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/chunk-D7FOKIY5.js +173 -0
- package/dist/chunk-D7FOKIY5.js.map +1 -0
- package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
- package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
- package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
- package/dist/chunk-IC2TCZJ2.js +54 -0
- package/dist/chunk-IC2TCZJ2.js.map +1 -0
- package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
- package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
- package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
- package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
- package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
- package/dist/chunk-LADFWYHY.js.map +1 -0
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
- package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
- package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
- package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
- package/dist/chunk-PCLIWBGD.js.map +1 -0
- package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
- package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
- package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
- package/dist/chunk-UFUKHGZD.js.map +1 -0
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
- package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
- package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
- package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +61 -59
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +9 -9
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +9 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +271 -17
- package/dist/index.js +163 -99
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/mcp-read-only-tools.d.ts +16 -0
- package/dist/mcp-read-only-tools.js +8 -0
- package/dist/mcp-read-only-tools.js.map +1 -0
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +19 -19
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +25 -25
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +85 -83
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/document-scanner.js +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -3
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +9 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage-C1maCNbc.d.ts +1591 -0
- package/dist/storage.d.ts +5 -1239
- package/dist/storage.js +8 -8
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
- package/dist/tokens.d.ts +17 -2
- package/dist/tokens.js +3 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-boundary.test.ts +62 -0
- package/src/access-boundary.ts +165 -126
- package/src/access-http.test.ts +781 -0
- package/src/access-http.ts +334 -108
- package/src/access-mcp.test.ts +264 -0
- package/src/access-mcp.ts +45 -70
- package/src/access-operations-batch.ts +35 -12
- package/src/access-surface-catalog.test.ts +22 -14
- package/src/access-surface-catalog.ts +6 -3
- package/src/access-token-capabilities.test.ts +844 -0
- package/src/access-token-capabilities.ts +391 -0
- package/src/chat/chat-mcp.test.ts +232 -0
- package/src/chat/chat-session.ts +34 -0
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +14 -0
- package/src/mcp-read-only-tools.ts +61 -0
- package/src/storage.ts +16 -0
- package/src/tokens.ts +32 -3
- package/src/wearables/cli.test.ts +102 -0
- package/src/wearables/cli.ts +65 -0
- package/src/wearables/config.test.ts +32 -0
- package/src/wearables/config.ts +52 -0
- package/src/wearables/day-store.test.ts +246 -0
- package/src/wearables/day-store.ts +272 -3
- package/src/wearables/fusion/cluster.ts +230 -0
- package/src/wearables/fusion/fuse.ts +218 -0
- package/src/wearables/fusion/fusion.test.ts +2575 -0
- package/src/wearables/fusion/index.ts +50 -0
- package/src/wearables/fusion/reconcile.ts +831 -0
- package/src/wearables/fusion/reconstruct.ts +251 -0
- package/src/wearables/fusion/store.ts +480 -0
- package/src/wearables/fusion/types.ts +228 -0
- package/src/wearables/index.ts +41 -0
- package/src/wearables/service.test.ts +1030 -2
- package/src/wearables/service.ts +258 -16
- package/src/wearables/speakers.test.ts +128 -0
- package/src/wearables/speakers.ts +58 -4
- package/src/wearables/storage-io.test.ts +146 -0
- package/src/wearables/types.ts +31 -0
- package/dist/chunk-2FOD2YY3.js.map +0 -1
- package/dist/chunk-7QWMTOIF.js.map +0 -1
- package/dist/chunk-DR6PEJU3.js.map +0 -1
- package/dist/chunk-E2SPGGUI.js.map +0 -1
- package/dist/chunk-FZGGZAAU.js.map +0 -1
- package/dist/chunk-ILFJ7AVS.js.map +0 -1
- package/dist/chunk-M7GGMGMZ.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
- /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
- /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
- /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
- /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
- /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
- /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for per-token capabilities (issue #1837).
|
|
3
|
+
*
|
|
4
|
+
* Locks in EVERY semantic of the capability system:
|
|
5
|
+
* - legacy token (absent record) ⇒ full access
|
|
6
|
+
* - present ops allow-list ⇒ default-deny, listed-only
|
|
7
|
+
* - explicitly empty ops ⇒ deny all (fail-closed)
|
|
8
|
+
* - namespaces allow-list ⇒ in-scope only
|
|
9
|
+
* - monitor-scoped token ⇒ monitoring ops allowed, content + admin denied
|
|
10
|
+
* - malformed input at mint ⇒ rejected
|
|
11
|
+
* - new token without flags ⇒ explicit versioned unrestricted record
|
|
12
|
+
* - capabilities survive persist + reload round-trip
|
|
13
|
+
* - boundary run() enforces for every registered op (no bypass)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
OPERATION_NAMES,
|
|
25
|
+
type OperationContext,
|
|
26
|
+
__resetRegistryForTest,
|
|
27
|
+
defineOperation,
|
|
28
|
+
getOperation,
|
|
29
|
+
listRegisteredOperations,
|
|
30
|
+
} from "./access-boundary.js";
|
|
31
|
+
import { EngramAccessForbiddenError } from "./access-errors.js";
|
|
32
|
+
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
33
|
+
import {
|
|
34
|
+
TOKEN_CAPABILITIES_VERSION,
|
|
35
|
+
type TokenCapabilities,
|
|
36
|
+
assertFleetWideOperationAllowed,
|
|
37
|
+
assertNamespaceAllowed,
|
|
38
|
+
assertOperationAllowed,
|
|
39
|
+
capabilityAllowsNamespace,
|
|
40
|
+
capabilityAllowsOp,
|
|
41
|
+
enforceNamespaceAllowList,
|
|
42
|
+
resolveEffectiveNamespace,
|
|
43
|
+
isCapabilityRestricted,
|
|
44
|
+
isValidNamespaceValue,
|
|
45
|
+
normalizeCapabilities,
|
|
46
|
+
tokenCapabilityStore,
|
|
47
|
+
validateCapabilitiesForMint,
|
|
48
|
+
} from "./access-token-capabilities.js";
|
|
49
|
+
import { buildTokenEntry, generateToken, loadTokenStore } from "./tokens.js";
|
|
50
|
+
|
|
51
|
+
// Importing access-operations registers the boundary operations as a side
|
|
52
|
+
// effect — we enumerate them in the critical-coverage test below.
|
|
53
|
+
import "./access-operations.js";
|
|
54
|
+
import { readPair, writePair } from "./contradiction/contradiction-review.js";
|
|
55
|
+
import { parseConfig } from "./config.js";
|
|
56
|
+
import type { StorageManager } from "./storage.js";
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Test fixtures
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function mockService(): EngramAccessService {
|
|
63
|
+
return {} as unknown as EngramAccessService;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockCtx(): OperationContext {
|
|
67
|
+
return { service: mockService(), authenticatedPrincipal: undefined };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function makeTempTokenPath(): Promise<{ dir: string; tokensPath: string }> {
|
|
71
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-capabilities-"));
|
|
72
|
+
return { dir, tokensPath: path.join(dir, "tokens.json") };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
// 1. Legacy token (absent capabilities record) ⇒ full access
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
|
|
79
|
+
test("legacy token: absent record is unrestricted on every axis", () => {
|
|
80
|
+
const caps: TokenCapabilities | undefined = undefined;
|
|
81
|
+
assert.equal(capabilityAllowsOp(caps, "memory_store"), true);
|
|
82
|
+
assert.equal(capabilityAllowsOp(caps, "recall"), true);
|
|
83
|
+
assert.equal(capabilityAllowsOp(caps, "anything_at_all"), true);
|
|
84
|
+
assert.equal(capabilityAllowsNamespace(caps, "default"), true);
|
|
85
|
+
assert.equal(capabilityAllowsNamespace(caps, "other"), true);
|
|
86
|
+
assert.equal(isCapabilityRestricted(caps), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("legacy token: assertOperationAllowed / assertNamespaceAllowed are no-ops", () => {
|
|
90
|
+
assert.doesNotThrow(() => assertOperationAllowed(undefined, "memory_store"));
|
|
91
|
+
assert.doesNotThrow(() => assertNamespaceAllowed(undefined, "default"));
|
|
92
|
+
assert.doesNotThrow(() => assertNamespaceAllowed(undefined, undefined));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("legacy token: null record is also treated as unrestricted", () => {
|
|
96
|
+
assert.equal(capabilityAllowsOp(null, "recall"), true);
|
|
97
|
+
assert.equal(isCapabilityRestricted(null), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
// 2. Present ops allow-list ⇒ listed op permitted, unlisted rejected
|
|
102
|
+
// ===========================================================================
|
|
103
|
+
|
|
104
|
+
test("ops allow-list: validateCapabilitiesForMint accepts valid ops", () => {
|
|
105
|
+
const caps = validateCapabilitiesForMint({ ops: ["recall", "memory_get"] }, OPERATION_NAMES);
|
|
106
|
+
assert.equal(caps.version, TOKEN_CAPABILITIES_VERSION);
|
|
107
|
+
assert.deepEqual(caps.ops, ["memory_get", "recall"]); // deduped + sorted
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("ops allow-list: listed op permitted, unlisted rejected", () => {
|
|
111
|
+
const caps = validateCapabilitiesForMint({ ops: ["recall"] }, OPERATION_NAMES);
|
|
112
|
+
assert.equal(capabilityAllowsOp(caps, "recall"), true);
|
|
113
|
+
assert.equal(capabilityAllowsOp(caps, "memory_store"), false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("ops allow-list: assertOperationAllowed throws EngramAccessForbiddenError for unlisted op", () => {
|
|
117
|
+
const caps = validateCapabilitiesForMint({ ops: ["recall"] }, OPERATION_NAMES);
|
|
118
|
+
assert.doesNotThrow(() => assertOperationAllowed(caps, "recall"));
|
|
119
|
+
assert.throws(
|
|
120
|
+
() => assertOperationAllowed(caps, "memory_store"),
|
|
121
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /memory_store/.test(err.message)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("ops allow-list: isCapabilityRestricted is true", () => {
|
|
126
|
+
const caps = validateCapabilitiesForMint({ ops: ["recall"] }, OPERATION_NAMES);
|
|
127
|
+
assert.equal(isCapabilityRestricted(caps), true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ===========================================================================
|
|
131
|
+
// 3. Explicitly EMPTY ops array ⇒ deny ALL ops (fail-closed)
|
|
132
|
+
// ===========================================================================
|
|
133
|
+
|
|
134
|
+
test("empty ops: validateCapabilitiesForMint preserves empty array", () => {
|
|
135
|
+
const caps = validateCapabilitiesForMint({ ops: [] }, OPERATION_NAMES);
|
|
136
|
+
assert.deepEqual(caps.ops, []);
|
|
137
|
+
assert.equal(isCapabilityRestricted(caps), true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("empty ops: every op is denied (fail-closed, NOT unrestricted)", () => {
|
|
141
|
+
const caps = validateCapabilitiesForMint({ ops: [] }, OPERATION_NAMES);
|
|
142
|
+
// Sample across the catalog — every one must be denied.
|
|
143
|
+
for (const opName of ["recall", "memory_store", "memory_get", "wearables_status"]) {
|
|
144
|
+
assert.equal(capabilityAllowsOp(caps, opName), false, `${opName} must be denied`);
|
|
145
|
+
}
|
|
146
|
+
assert.throws(() => assertOperationAllowed(caps, "recall"), EngramAccessForbiddenError);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ===========================================================================
|
|
150
|
+
// 4. Namespaces allow-list ⇒ in-scope permitted, out-of-scope rejected
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
|
|
153
|
+
test("namespaces allow-list: validateCapabilitiesForMint accepts valid namespaces", () => {
|
|
154
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default", "project-x"] }, OPERATION_NAMES);
|
|
155
|
+
assert.deepEqual(caps.namespaces, ["default", "project-x"]); // sorted
|
|
156
|
+
assert.equal(caps.ops, undefined); // ops axis absent ⇒ unrestricted
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("namespaces allow-list: in-scope permitted, out-of-scope rejected", () => {
|
|
160
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
161
|
+
assert.equal(capabilityAllowsNamespace(caps, "default"), true);
|
|
162
|
+
assert.equal(capabilityAllowsNamespace(caps, "other"), false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("namespaces allow-list: scoped token rejects undefined namespace", () => {
|
|
166
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
167
|
+
assert.equal(capabilityAllowsNamespace(caps, undefined), false);
|
|
168
|
+
assert.throws(() => assertNamespaceAllowed(caps, undefined), EngramAccessForbiddenError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("namespaces allow-list: out-of-scope throws EngramAccessForbiddenError", () => {
|
|
172
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
173
|
+
assert.doesNotThrow(() => assertNamespaceAllowed(caps, "default"));
|
|
174
|
+
assert.throws(
|
|
175
|
+
() => assertNamespaceAllowed(caps, "secret"),
|
|
176
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /secret/.test(err.message)
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("empty namespaces: deny all (fail-closed)", () => {
|
|
181
|
+
const caps = validateCapabilitiesForMint({ namespaces: [] }, OPERATION_NAMES);
|
|
182
|
+
assert.deepEqual(caps.namespaces, []);
|
|
183
|
+
assert.equal(capabilityAllowsNamespace(caps, "default"), false);
|
|
184
|
+
assert.equal(capabilityAllowsNamespace(caps, undefined), false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// -------------------------------------------------------------------------
|
|
188
|
+
// 4b. Effective-namespace chokepoint (issue #1850) — the SINGLE function the
|
|
189
|
+
// HTTP query/body routes, MCP dispatch, and id-loaded contradiction routes
|
|
190
|
+
// all route through. Models undefined→default mapping, fail-closed, and
|
|
191
|
+
// no-op-for-unrestricted.
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
test("resolveEffectiveNamespace: present value wins; empty/undefined → default", () => {
|
|
195
|
+
assert.equal(resolveEffectiveNamespace("team", "default"), "team");
|
|
196
|
+
assert.equal(resolveEffectiveNamespace("", "default"), "default", "empty string is treated as absent");
|
|
197
|
+
assert.equal(resolveEffectiveNamespace(undefined, "default"), "default");
|
|
198
|
+
assert.equal(resolveEffectiveNamespace(undefined, undefined), undefined, "no value and no default → undefined");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("enforceNamespaceAllowList: no-op for unrestricted tokens (absent + explicit-unrestricted)", () => {
|
|
202
|
+
const scoped = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
203
|
+
// Legacy token (absent capabilities record) — unrestricted.
|
|
204
|
+
assert.doesNotThrow(() => enforceNamespaceAllowList(undefined, "anything", "default"));
|
|
205
|
+
assert.doesNotThrow(() => enforceNamespaceAllowList(null, undefined, "default"));
|
|
206
|
+
// Explicit-unrestricted record ({version:1}, no namespaces axis).
|
|
207
|
+
const explicitUnrestricted = validateCapabilitiesForMint(undefined, OPERATION_NAMES);
|
|
208
|
+
assert.doesNotThrow(() => enforceNamespaceAllowList(explicitUnrestricted, "anything", "default"));
|
|
209
|
+
// Sanity: the scoped token IS restricted (control for the tests below).
|
|
210
|
+
assert.equal(isCapabilityRestricted(scoped), true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("enforceNamespaceAllowList: scoped token — explicit allowed namespace passes", () => {
|
|
214
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["ns_a", "default"] }, OPERATION_NAMES);
|
|
215
|
+
assert.doesNotThrow(() => enforceNamespaceAllowList(caps, "ns_a", "default"));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("enforceNamespaceAllowList: scoped token — explicit unlisted namespace rejected (fail closed)", () => {
|
|
219
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["ns_a"] }, OPERATION_NAMES);
|
|
220
|
+
assert.throws(
|
|
221
|
+
() => enforceNamespaceAllowList(caps, "ns_b", "default"),
|
|
222
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /ns_b/.test(err.message),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("enforceNamespaceAllowList: undefined maps to default — allow-list INCLUDING default passes (legacy pair fix)", () => {
|
|
227
|
+
// The round-4 bug: a legacy pair carries namespace:undefined, which
|
|
228
|
+
// downstream storage maps to the server default. A scoped token whose
|
|
229
|
+
// allow-list INCLUDES the default must be permitted; the prior
|
|
230
|
+
// assertNamespaceAllowed(caps, undefined) wrongly denied it.
|
|
231
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
232
|
+
assert.doesNotThrow(() => enforceNamespaceAllowList(caps, undefined, "default"));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("enforceNamespaceAllowList: undefined maps to default — allow-list NOT including default rejected", () => {
|
|
236
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["ns_a"] }, OPERATION_NAMES);
|
|
237
|
+
assert.throws(
|
|
238
|
+
() => enforceNamespaceAllowList(caps, undefined, "default"),
|
|
239
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /server default namespace is not permitted/.test(err.message),
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("enforceNamespaceAllowList: absent server default + scoped token → fail closed", () => {
|
|
244
|
+
// No request namespace AND no configured default ⇒ an undefined effective
|
|
245
|
+
// namespace cannot satisfy any allow-list; fail closed rather than let an
|
|
246
|
+
// unconfigured server silently admit the default tenant.
|
|
247
|
+
const caps = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
248
|
+
assert.throws(
|
|
249
|
+
() => enforceNamespaceAllowList(caps, undefined, undefined),
|
|
250
|
+
EngramAccessForbiddenError,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
// 5. Monitor-scoped token — monitoring ops allowed, content + admin denied
|
|
256
|
+
// ===========================================================================
|
|
257
|
+
|
|
258
|
+
const MONITOR_OPS = ["maintenance_status", "quality_status", "procedural_stats", "recall_timings"];
|
|
259
|
+
|
|
260
|
+
test("monitor-scoped token: only monitoring ops are permitted", () => {
|
|
261
|
+
const caps = validateCapabilitiesForMint({ ops: MONITOR_OPS }, OPERATION_NAMES);
|
|
262
|
+
for (const op of MONITOR_OPS) {
|
|
263
|
+
assert.equal(capabilityAllowsOp(caps, op), true, `${op} must be allowed`);
|
|
264
|
+
}
|
|
265
|
+
// Memory-content ops must be denied.
|
|
266
|
+
for (const denied of ["memory_store", "memory_get", "memory_search", "recall", "observe"]) {
|
|
267
|
+
assert.equal(capabilityAllowsOp(caps, denied), false, `${denied} must be denied`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("monitor-scoped token: isCapabilityRestricted → true (operator gate would reject)", () => {
|
|
272
|
+
const caps = validateCapabilitiesForMint({ ops: MONITOR_OPS }, OPERATION_NAMES);
|
|
273
|
+
// requireOperatorToken() checks isCapabilityRestricted — a scoped token
|
|
274
|
+
// must be flagged so operator/admin routes deny it.
|
|
275
|
+
assert.equal(isCapabilityRestricted(caps), true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("monitor-scoped token: content op throws EngramAccessForbiddenError", () => {
|
|
279
|
+
const caps = validateCapabilitiesForMint({ ops: MONITOR_OPS }, OPERATION_NAMES);
|
|
280
|
+
assert.doesNotThrow(() => assertOperationAllowed(caps, "maintenance_status"));
|
|
281
|
+
assert.throws(() => assertOperationAllowed(caps, "memory_store"), EngramAccessForbiddenError);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ===========================================================================
|
|
285
|
+
// 6. Malformed input at mint ⇒ rejected (validateCapabilitiesForMint throws)
|
|
286
|
+
// ===========================================================================
|
|
287
|
+
|
|
288
|
+
test("malformed mint: unknown op name is rejected", () => {
|
|
289
|
+
assert.throws(() => validateCapabilitiesForMint({ ops: ["bogus_op"] }, OPERATION_NAMES), /unknown operation name/i);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("malformed mint: non-array ops is rejected", () => {
|
|
293
|
+
assert.throws(() => validateCapabilitiesForMint({ ops: "not-an-array" }, OPERATION_NAMES), /must be an array/i);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("malformed mint: non-string element in ops is rejected", () => {
|
|
297
|
+
assert.throws(
|
|
298
|
+
() => validateCapabilitiesForMint({ ops: [123] }, OPERATION_NAMES),
|
|
299
|
+
/must be an array of non-empty strings/i
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("malformed mint: traversal namespace (../x) is rejected", () => {
|
|
304
|
+
assert.throws(() => validateCapabilitiesForMint({ namespaces: ["../x"] }, OPERATION_NAMES), /malformed namespace/i);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("malformed mint: path-separator namespace (a/b) is rejected", () => {
|
|
308
|
+
assert.throws(() => validateCapabilitiesForMint({ namespaces: ["a/b"] }, OPERATION_NAMES), /malformed namespace/i);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("malformed mint: backslash namespace is rejected", () => {
|
|
312
|
+
assert.throws(() => validateCapabilitiesForMint({ namespaces: ["a\\b"] }, OPERATION_NAMES), /malformed namespace/i);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("malformed mint: non-object capabilities is rejected", () => {
|
|
316
|
+
assert.throws(() => validateCapabilitiesForMint("oops", OPERATION_NAMES), /must be an object/i);
|
|
317
|
+
assert.throws(() => validateCapabilitiesForMint([1, 2], OPERATION_NAMES), /must be an object/i);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("malformed mint: wrong version is rejected", () => {
|
|
321
|
+
assert.throws(
|
|
322
|
+
() => validateCapabilitiesForMint({ version: 99, ops: ["recall"] }, OPERATION_NAMES),
|
|
323
|
+
/version must be/i
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("isValidNamespaceValue: accepts simple identifiers, rejects traversal/separators", () => {
|
|
328
|
+
assert.equal(isValidNamespaceValue("default"), true);
|
|
329
|
+
assert.equal(isValidNamespaceValue("project-x"), true);
|
|
330
|
+
assert.equal(isValidNamespaceValue("a.b.c"), true); // dots allowed
|
|
331
|
+
assert.equal(isValidNamespaceValue("../x"), false);
|
|
332
|
+
assert.equal(isValidNamespaceValue("a/b"), false);
|
|
333
|
+
assert.equal(isValidNamespaceValue("a\\b"), false);
|
|
334
|
+
assert.equal(isValidNamespaceValue("has space"), false);
|
|
335
|
+
assert.equal(isValidNamespaceValue(""), false);
|
|
336
|
+
assert.equal(isValidNamespaceValue(42), false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
// 7. New token without flags ⇒ explicit versioned unrestricted record
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
|
|
343
|
+
test("validateCapabilitiesForMint(undefined) yields explicit {version:1} (not omitted)", () => {
|
|
344
|
+
const caps = validateCapabilitiesForMint(undefined, OPERATION_NAMES);
|
|
345
|
+
assert.equal(caps.version, TOKEN_CAPABILITIES_VERSION);
|
|
346
|
+
assert.equal(caps.ops, undefined); // unrestricted on ops axis
|
|
347
|
+
assert.equal(caps.namespaces, undefined); // unrestricted on namespaces axis
|
|
348
|
+
assert.equal(isCapabilityRestricted(caps), false);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("validateCapabilitiesForMint(null) yields explicit {version:1}", () => {
|
|
352
|
+
const caps = validateCapabilitiesForMint(null, OPERATION_NAMES);
|
|
353
|
+
assert.equal(caps.version, TOKEN_CAPABILITIES_VERSION);
|
|
354
|
+
assert.equal(isCapabilityRestricted(caps), false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("buildTokenEntry without capabilities records explicit {version:1}", () => {
|
|
358
|
+
const entry = buildTokenEntry("test-connector");
|
|
359
|
+
assert.ok(entry.capabilities, "new token must carry an explicit capabilities record");
|
|
360
|
+
assert.equal(entry.capabilities?.version, TOKEN_CAPABILITIES_VERSION);
|
|
361
|
+
assert.equal(entry.capabilities?.ops, undefined);
|
|
362
|
+
assert.equal(entry.capabilities?.namespaces, undefined);
|
|
363
|
+
assert.equal(isCapabilityRestricted(entry.capabilities), false);
|
|
364
|
+
// Distinguishable from legacy: legacy has capabilities === undefined,
|
|
365
|
+
// new-unrestricted has capabilities === {version:1}.
|
|
366
|
+
assert.notEqual(entry.capabilities, undefined);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("explicit-unrestricted token still has full access", () => {
|
|
370
|
+
const caps = validateCapabilitiesForMint(undefined, OPERATION_NAMES);
|
|
371
|
+
assert.equal(capabilityAllowsOp(caps, "memory_store"), true);
|
|
372
|
+
assert.equal(capabilityAllowsOp(caps, "recall"), true);
|
|
373
|
+
assert.equal(capabilityAllowsNamespace(caps, "anything"), true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ===========================================================================
|
|
377
|
+
// 8. Capabilities survive token-store PERSIST + RELOAD round-trip
|
|
378
|
+
// ===========================================================================
|
|
379
|
+
|
|
380
|
+
test("persist + reload: scoped capabilities survive round-trip", async () => {
|
|
381
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
382
|
+
try {
|
|
383
|
+
generateToken(
|
|
384
|
+
"scoped-rt",
|
|
385
|
+
tokensPath,
|
|
386
|
+
validateCapabilitiesForMint({ ops: ["recall", "memory_get"], namespaces: ["default"] }, OPERATION_NAMES)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const store = loadTokenStore(tokensPath);
|
|
390
|
+
const entry = store.tokens.find((t) => t.connector === "scoped-rt");
|
|
391
|
+
assert.ok(entry, "scoped token not found after reload");
|
|
392
|
+
assert.ok(entry?.capabilities, "capabilities record must survive reload");
|
|
393
|
+
assert.equal(entry?.capabilities?.version, TOKEN_CAPABILITIES_VERSION);
|
|
394
|
+
assert.deepEqual(entry?.capabilities?.ops, ["memory_get", "recall"]);
|
|
395
|
+
assert.deepEqual(entry?.capabilities?.namespaces, ["default"]);
|
|
396
|
+
} finally {
|
|
397
|
+
await rm(dir, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("persist + reload: explicit-unrestricted marker survives round-trip", async () => {
|
|
402
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
403
|
+
try {
|
|
404
|
+
generateToken("plain-rt", tokensPath);
|
|
405
|
+
|
|
406
|
+
const store = loadTokenStore(tokensPath);
|
|
407
|
+
const entry = store.tokens.find((t) => t.connector === "plain-rt");
|
|
408
|
+
assert.ok(entry);
|
|
409
|
+
assert.ok(entry?.capabilities, "explicit {version:1} must survive reload");
|
|
410
|
+
assert.equal(entry?.capabilities?.version, TOKEN_CAPABILITIES_VERSION);
|
|
411
|
+
assert.equal(entry?.capabilities?.ops, undefined);
|
|
412
|
+
} finally {
|
|
413
|
+
await rm(dir, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("persist + reload: legacy token (no capabilities field) stays undefined", async () => {
|
|
418
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
419
|
+
try {
|
|
420
|
+
await writeFile(
|
|
421
|
+
tokensPath,
|
|
422
|
+
JSON.stringify({
|
|
423
|
+
tokens: [{ token: "remnic_oc_legacy_xx", connector: "legacy", createdAt: "2024-01-01T00:00:00.000Z" }],
|
|
424
|
+
}),
|
|
425
|
+
"utf8"
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const store = loadTokenStore(tokensPath);
|
|
429
|
+
assert.equal(store.tokens.length, 1);
|
|
430
|
+
assert.equal(store.tokens[0].capabilities, undefined, "legacy entry must have NO capabilities record");
|
|
431
|
+
} finally {
|
|
432
|
+
await rm(dir, { recursive: true, force: true });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("persist + reload: deny-all (empty ops) survives round-trip", async () => {
|
|
437
|
+
const { dir, tokensPath } = await makeTempTokenPath();
|
|
438
|
+
try {
|
|
439
|
+
generateToken("denyall-rt", tokensPath, validateCapabilitiesForMint({ ops: [] }, OPERATION_NAMES));
|
|
440
|
+
|
|
441
|
+
const store = loadTokenStore(tokensPath);
|
|
442
|
+
const entry = store.tokens.find((t) => t.connector === "denyall-rt");
|
|
443
|
+
assert.ok(entry?.capabilities);
|
|
444
|
+
assert.deepEqual(entry?.capabilities?.ops, []); // still empty, NOT coerced to undefined
|
|
445
|
+
assert.equal(capabilityAllowsOp(entry?.capabilities, "recall"), false);
|
|
446
|
+
} finally {
|
|
447
|
+
await rm(dir, { recursive: true, force: true });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// 9. normalizeCapabilities (load-time normalization)
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
|
|
455
|
+
test("normalizeCapabilities: absent ⇒ undefined (legacy)", () => {
|
|
456
|
+
assert.equal(normalizeCapabilities(undefined), undefined);
|
|
457
|
+
assert.equal(normalizeCapabilities(null), undefined);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("normalizeCapabilities: {version:1} ⇒ explicit-unrestricted", () => {
|
|
461
|
+
const caps = normalizeCapabilities({ version: 1 });
|
|
462
|
+
assert.deepEqual(caps, { version: 1 });
|
|
463
|
+
assert.equal(isCapabilityRestricted(caps), false);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("normalizeCapabilities: preserves ops and namespaces", () => {
|
|
467
|
+
const caps = normalizeCapabilities({ version: 1, ops: ["recall"], namespaces: ["default"] });
|
|
468
|
+
assert.deepEqual(caps, { version: 1, ops: ["recall"], namespaces: ["default"] });
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("normalizeCapabilities: rejects bad version", () => {
|
|
472
|
+
assert.throws(() => normalizeCapabilities({ version: 2 }), /version must be/i);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("normalizeCapabilities: rejects non-object", () => {
|
|
476
|
+
assert.throws(() => normalizeCapabilities("bad"), /must be an object/i);
|
|
477
|
+
assert.throws(() => normalizeCapabilities([1]), /must be an object/i);
|
|
478
|
+
});
|
|
479
|
+
// ===========================================================================
|
|
480
|
+
// 9b. Mint/load version symmetry (issue #1837 r11) — validateCapabilitiesForMint
|
|
481
|
+
// and normalizeCapabilities MUST treat the version marker identically:
|
|
482
|
+
// an absent/undefined version coerces to the current default (1) in both,
|
|
483
|
+
// and an explicitly-wrong version is rejected by both. The prior asymmetry
|
|
484
|
+
// let {version: undefined} pass mint (coerced to {version:1}) yet THROW on
|
|
485
|
+
// load — a record that mint-validated had to also load-normalize.
|
|
486
|
+
// ===========================================================================
|
|
487
|
+
|
|
488
|
+
test("version symmetry: undefined version coerces to 1 in BOTH mint and load", () => {
|
|
489
|
+
const minted = validateCapabilitiesForMint({ version: undefined, ops: ["recall"] }, OPERATION_NAMES);
|
|
490
|
+
assert.deepEqual(minted, { version: TOKEN_CAPABILITIES_VERSION, ops: ["recall"] });
|
|
491
|
+
// The minted record must load-normalize without throwing (the original bug).
|
|
492
|
+
const loaded = normalizeCapabilities(minted);
|
|
493
|
+
assert.deepEqual(loaded, { version: TOKEN_CAPABILITIES_VERSION, ops: ["recall"] });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("version symmetry: absent version key coerces to 1 in BOTH mint and load", () => {
|
|
497
|
+
const minted = validateCapabilitiesForMint({ ops: ["recall"] }, OPERATION_NAMES);
|
|
498
|
+
assert.deepEqual(minted, { version: TOKEN_CAPABILITIES_VERSION, ops: ["recall"] });
|
|
499
|
+
// A present record with NO version key loads as version 1 (no throw).
|
|
500
|
+
const loaded = normalizeCapabilities({ ops: ["recall"] });
|
|
501
|
+
assert.deepEqual(loaded, { version: TOKEN_CAPABILITIES_VERSION, ops: ["recall"] });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("version symmetry: undefined-version record round-trips mint → load", () => {
|
|
505
|
+
// The mint output is what is persisted; feeding it back through the load
|
|
506
|
+
// normalizer must reproduce it exactly (mint ⇄ load equivalence).
|
|
507
|
+
const minted = validateCapabilitiesForMint(
|
|
508
|
+
{ version: undefined, namespaces: ["default"] },
|
|
509
|
+
OPERATION_NAMES,
|
|
510
|
+
);
|
|
511
|
+
const reloaded = normalizeCapabilities(minted);
|
|
512
|
+
assert.deepEqual(reloaded, minted);
|
|
513
|
+
assert.deepEqual(reloaded, { version: TOKEN_CAPABILITIES_VERSION, namespaces: ["default"] });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("version symmetry: explicitly-wrong versions (2, 'x', 0) rejected identically by mint AND load", () => {
|
|
517
|
+
for (const badVersion of [2, "x", 0]) {
|
|
518
|
+
assert.throws(
|
|
519
|
+
() => validateCapabilitiesForMint({ version: badVersion }, OPERATION_NAMES),
|
|
520
|
+
/version must be/i,
|
|
521
|
+
`mint must reject version ${JSON.stringify(badVersion)}`,
|
|
522
|
+
);
|
|
523
|
+
assert.throws(
|
|
524
|
+
() => normalizeCapabilities({ version: badVersion }),
|
|
525
|
+
/version must be/i,
|
|
526
|
+
`load must reject version ${JSON.stringify(badVersion)}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("version symmetry: version 1 is accepted identically by mint AND load", () => {
|
|
532
|
+
const minted = validateCapabilitiesForMint({ version: 1, ops: ["recall"] }, OPERATION_NAMES);
|
|
533
|
+
const loaded = normalizeCapabilities({ version: 1, ops: ["recall"] });
|
|
534
|
+
assert.deepEqual(minted, loaded);
|
|
535
|
+
assert.deepEqual(loaded, { version: TOKEN_CAPABILITIES_VERSION, ops: ["recall"] });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ===========================================================================
|
|
539
|
+
// 10. CRITICAL COVERAGE — boundary run() enforces for EVERY registered op
|
|
540
|
+
// These tests run FIRST in section 10, before any test that resets the
|
|
541
|
+
// registry, so the ops registered by the module-level import are present.
|
|
542
|
+
// ===========================================================================
|
|
543
|
+
|
|
544
|
+
test("CRITICAL: every registered op blocks handler under deny-all ALS (no bypass)", async () => {
|
|
545
|
+
// Enumerate every operation registered through the boundary. Under a
|
|
546
|
+
// deny-all capability ({version:1, ops:[]}), the handler must NEVER execute
|
|
547
|
+
// for ANY op — proving no op bypasses the capability chokepoint.
|
|
548
|
+
//
|
|
549
|
+
// Ops with permissive schemas (accept {}) pass zod and hit the capability
|
|
550
|
+
// gate → EngramAccessForbiddenError. Ops with strict schemas reject {} at
|
|
551
|
+
// zod → EngramAccessInputError. In BOTH cases the handler body is not
|
|
552
|
+
// reached. If ANY op's handler ran, that would be a security hole.
|
|
553
|
+
const denyAll: TokenCapabilities = { version: 1, ops: [] };
|
|
554
|
+
const registered = listRegisteredOperations();
|
|
555
|
+
assert.ok(registered.length > 10, `expected many registered ops; got ${registered.length}`);
|
|
556
|
+
|
|
557
|
+
const ctx = mockCtx();
|
|
558
|
+
const reachedGate: string[] = [];
|
|
559
|
+
const rejectedAtInput: string[] = [];
|
|
560
|
+
|
|
561
|
+
for (const opName of registered) {
|
|
562
|
+
const op = getOperation(opName);
|
|
563
|
+
assert.ok(op, `operation ${opName} not found in registry`);
|
|
564
|
+
|
|
565
|
+
await tokenCapabilityStore.run(denyAll, async () => {
|
|
566
|
+
try {
|
|
567
|
+
await op?.run({}, ctx);
|
|
568
|
+
// If we reach here, the handler executed under deny-all — SECURITY HOLE.
|
|
569
|
+
assert.fail(`SECURITY HOLE: op "${opName}" handler ran under deny-all capabilities`);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (err instanceof EngramAccessForbiddenError) {
|
|
572
|
+
reachedGate.push(opName);
|
|
573
|
+
} else if (err instanceof EngramAccessInputError) {
|
|
574
|
+
// Schema rejected {} before the gate — still safe, handler didn't run.
|
|
575
|
+
rejectedAtInput.push(opName);
|
|
576
|
+
} else {
|
|
577
|
+
assert.fail(
|
|
578
|
+
`op "${opName}": unexpected error type: ${(err as Error).constructor.name}: ${(err as Error).message}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// At least one op must have a permissive-enough schema to reach the gate,
|
|
586
|
+
// proving the capability check actually fires (not just that all ops fail
|
|
587
|
+
// at input validation).
|
|
588
|
+
assert.ok(
|
|
589
|
+
reachedGate.length > 0,
|
|
590
|
+
`expected at least one op to reach the capability gate under deny-all; reachedGate=[${reachedGate.join(", ")}] rejectedAtInput=[${rejectedAtInput.join(", ")}]`
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("CRITICAL: ops that reach the gate are individually denied under scoped ALS", async () => {
|
|
595
|
+
// For ops with permissive schemas (they accept {}), verify that under a
|
|
596
|
+
// scoped-to-different-ops ALS, they throw EngramAccessForbiddenError —
|
|
597
|
+
// proving each op is individually gated by its own name.
|
|
598
|
+
const scopedAway: TokenCapabilities = {
|
|
599
|
+
version: 1,
|
|
600
|
+
ops: ["nonexistent_placeholder_op"],
|
|
601
|
+
};
|
|
602
|
+
const ctx = mockCtx();
|
|
603
|
+
|
|
604
|
+
const registered = listRegisteredOperations();
|
|
605
|
+
assert.ok(registered.length > 0, "expected registered ops before structural tests clear the registry");
|
|
606
|
+
|
|
607
|
+
for (const opName of registered) {
|
|
608
|
+
const op = getOperation(opName);
|
|
609
|
+
assert.ok(op);
|
|
610
|
+
|
|
611
|
+
await tokenCapabilityStore.run(scopedAway, async () => {
|
|
612
|
+
try {
|
|
613
|
+
await op?.run({}, ctx);
|
|
614
|
+
assert.fail(`SECURITY HOLE: op "${opName}" ran under scoped-away ALS`);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
assert.ok(
|
|
617
|
+
err instanceof EngramAccessForbiddenError || err instanceof EngramAccessInputError,
|
|
618
|
+
`op "${opName}": unexpected error ${(err as Error).constructor.name}`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("CRITICAL: review_resolve enforces token namespace allow-list on the loaded pair (issue #1850 round 9)", async () => {
|
|
626
|
+
// Direct boundary-handler test (the "batch-ops" path). review_resolve selects
|
|
627
|
+
// its target BY pairId, so the pair's namespace comes from the record — NOT
|
|
628
|
+
// a request param. A namespace-scoped bearer must NOT mutate a pair in a
|
|
629
|
+
// namespace outside its allow-list. The handler must load the pair, assert
|
|
630
|
+
// its intrinsic namespace via enforceNamespaceAllowList, and fail closed
|
|
631
|
+
// BEFORE executeResolution. Runs before the registry reset below so the
|
|
632
|
+
// module-registered op is present.
|
|
633
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-batch-review-resolve-scoped-"));
|
|
634
|
+
const allowedPair = writePair(dir, {
|
|
635
|
+
namespace: "ns_a",
|
|
636
|
+
memoryIds: ["a-1", "a-2"],
|
|
637
|
+
verdict: "contradicts",
|
|
638
|
+
rationale: "pair in the allowed namespace",
|
|
639
|
+
confidence: 0.9,
|
|
640
|
+
detectedAt: new Date().toISOString(),
|
|
641
|
+
});
|
|
642
|
+
const deniedPair = writePair(dir, {
|
|
643
|
+
namespace: "ns_b",
|
|
644
|
+
memoryIds: ["b-1", "b-2"],
|
|
645
|
+
verdict: "contradicts",
|
|
646
|
+
rationale: "pair in a denied namespace",
|
|
647
|
+
confidence: 0.9,
|
|
648
|
+
detectedAt: new Date().toISOString(),
|
|
649
|
+
});
|
|
650
|
+
const storage = { dir } as unknown as StorageManager;
|
|
651
|
+
const ctx: OperationContext = {
|
|
652
|
+
service: {
|
|
653
|
+
configRef: parseConfig({ memoryDir: dir, namespacesEnabled: true, defaultNamespace: "default" }),
|
|
654
|
+
memoryDir: dir,
|
|
655
|
+
storageRef: storage,
|
|
656
|
+
getWritableStorageForNamespace: async (namespace: string | undefined) => ({
|
|
657
|
+
namespace: namespace ?? "default",
|
|
658
|
+
storage,
|
|
659
|
+
}),
|
|
660
|
+
} as unknown as EngramAccessService,
|
|
661
|
+
authenticatedPrincipal: "writer",
|
|
662
|
+
};
|
|
663
|
+
const op = getOperation("review_resolve");
|
|
664
|
+
if (!op) { assert.fail("review_resolve must be registered"); }
|
|
665
|
+
try {
|
|
666
|
+
// ── scoped to ns_a (ops axis absent ⇒ op-gate no-op): denied-namespace pair
|
|
667
|
+
// rejects EngramAccessForbiddenError BEFORE mutation ──
|
|
668
|
+
await assert.rejects(
|
|
669
|
+
() => tokenCapabilityStore.run({ version: 1, namespaces: ["ns_a"] }, () =>
|
|
670
|
+
op.run({ pairId: deniedPair.pairId, verb: "both-valid" }, ctx)),
|
|
671
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /ns_b/.test(err.message),
|
|
672
|
+
"scoped resolve: a pair in an unlisted namespace must be denied before mutation",
|
|
673
|
+
);
|
|
674
|
+
assert.notEqual(
|
|
675
|
+
readPair(dir, deniedPair.pairId)?.resolution,
|
|
676
|
+
"both-valid",
|
|
677
|
+
"the denied pair must remain unresolved (no mutation leak)",
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
// ── scoped to ns_a: allowed-namespace pair resolves and is mutated ──
|
|
681
|
+
await tokenCapabilityStore.run({ version: 1, namespaces: ["ns_a"] }, () =>
|
|
682
|
+
op.run({ pairId: allowedPair.pairId, verb: "both-valid" }, ctx));
|
|
683
|
+
assert.equal(
|
|
684
|
+
readPair(dir, allowedPair.pairId)?.resolution,
|
|
685
|
+
"both-valid",
|
|
686
|
+
"the allowed pair is marked resolved",
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
// ── unrestricted (namespaces axis absent): the denied-namespace pair is reachable ──
|
|
690
|
+
await tokenCapabilityStore.run({ version: 1 }, () =>
|
|
691
|
+
op.run({ pairId: deniedPair.pairId, verb: "both-valid" }, ctx));
|
|
692
|
+
assert.equal(
|
|
693
|
+
readPair(dir, deniedPair.pairId)?.resolution,
|
|
694
|
+
"both-valid",
|
|
695
|
+
"unrestricted token resolves the previously-denied pair",
|
|
696
|
+
);
|
|
697
|
+
} finally {
|
|
698
|
+
await rm(dir, { recursive: true, force: true });
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// -------------------------------------------------------------------------
|
|
703
|
+
// Structural enforcement tests — these reset the registry and register
|
|
704
|
+
// custom test ops, so they run AFTER the critical-coverage enumeration.
|
|
705
|
+
// -------------------------------------------------------------------------
|
|
706
|
+
|
|
707
|
+
test("defineOperation run(): under scoped ALS, listed op passes, unlisted rejected", async () => {
|
|
708
|
+
__resetRegistryForTest();
|
|
709
|
+
let handlerCalls = 0;
|
|
710
|
+
const op = defineOperation({
|
|
711
|
+
name: "memory_get",
|
|
712
|
+
description: "test op",
|
|
713
|
+
schema: z.object({}),
|
|
714
|
+
handler: async () => {
|
|
715
|
+
handlerCalls++;
|
|
716
|
+
return { ok: true };
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const ctx = mockCtx();
|
|
721
|
+
|
|
722
|
+
// Under a scoped ALS that DOES list the op → handler runs.
|
|
723
|
+
const allowsOp: TokenCapabilities = { version: 1, ops: ["memory_get"] };
|
|
724
|
+
const resultAllowed = await tokenCapabilityStore.run(allowsOp, () => op.run({}, ctx));
|
|
725
|
+
assert.deepEqual(resultAllowed, { ok: true });
|
|
726
|
+
assert.equal(handlerCalls, 1);
|
|
727
|
+
|
|
728
|
+
// Under a scoped ALS that does NOT list the op → EngramAccessForbiddenError.
|
|
729
|
+
const deniesOp: TokenCapabilities = { version: 1, ops: ["recall"] };
|
|
730
|
+
await assert.rejects(() => tokenCapabilityStore.run(deniesOp, () => op.run({}, ctx)), EngramAccessForbiddenError);
|
|
731
|
+
assert.equal(handlerCalls, 1, "handler must not run for denied op");
|
|
732
|
+
|
|
733
|
+
// Under empty-ops (deny-all) ALS → EngramAccessForbiddenError.
|
|
734
|
+
const denyAll: TokenCapabilities = { version: 1, ops: [] };
|
|
735
|
+
await assert.rejects(() => tokenCapabilityStore.run(denyAll, () => op.run({}, ctx)), EngramAccessForbiddenError);
|
|
736
|
+
assert.equal(handlerCalls, 1, "handler must not run under deny-all");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("defineOperation run(): legacy/unrestricted ALS → handler runs", async () => {
|
|
740
|
+
__resetRegistryForTest();
|
|
741
|
+
const op = defineOperation({
|
|
742
|
+
name: "memory_get",
|
|
743
|
+
description: "test op",
|
|
744
|
+
schema: z.object({}),
|
|
745
|
+
handler: async () => ({ ok: true }),
|
|
746
|
+
});
|
|
747
|
+
const ctx = mockCtx();
|
|
748
|
+
|
|
749
|
+
// No ALS set (undefined store) → unrestricted → handler runs.
|
|
750
|
+
const result = await op.run({}, ctx);
|
|
751
|
+
assert.deepEqual(result, { ok: true });
|
|
752
|
+
|
|
753
|
+
// Explicit-unrestricted capabilities ({version:1}) → handler runs.
|
|
754
|
+
const unrestricted: TokenCapabilities = { version: 1 };
|
|
755
|
+
const result2 = await tokenCapabilityStore.run(unrestricted, () => op.run({}, ctx));
|
|
756
|
+
assert.deepEqual(result2, { ok: true });
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("defineOperation run(): namespaces enforcement via assertNamespaceAllowed pattern", async () => {
|
|
760
|
+
// The boundary run() checks ops via the ALS. Namespace enforcement is
|
|
761
|
+
// additionally handled in the HTTP layer's resolveNamespace(). Here we
|
|
762
|
+
// verify the capability primitives the handler layer relies on.
|
|
763
|
+
const scopedNs = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
764
|
+
assert.doesNotThrow(() => assertNamespaceAllowed(scopedNs, "default"));
|
|
765
|
+
assert.throws(() => assertNamespaceAllowed(scopedNs, "other"), EngramAccessForbiddenError);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("CRITICAL: boundary enforcement reads from tokenCapabilityStore ALS", async () => {
|
|
769
|
+
// Prove the enforcement check reads the ALS value, not a static/closure
|
|
770
|
+
// variable. The same op must be denied under one ALS and allowed under
|
|
771
|
+
// another within the same process.
|
|
772
|
+
__resetRegistryForTest();
|
|
773
|
+
const op = defineOperation({
|
|
774
|
+
name: "memory_get",
|
|
775
|
+
description: "test",
|
|
776
|
+
schema: z.object({}),
|
|
777
|
+
handler: async () => ({ ran: true }),
|
|
778
|
+
});
|
|
779
|
+
const ctx = mockCtx();
|
|
780
|
+
|
|
781
|
+
// Denied under scoped ALS.
|
|
782
|
+
await assert.rejects(
|
|
783
|
+
() => tokenCapabilityStore.run({ version: 1, ops: ["recall"] }, () => op.run({}, ctx)),
|
|
784
|
+
EngramAccessForbiddenError
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
// Allowed under different scoped ALS that includes this op.
|
|
788
|
+
const result = await tokenCapabilityStore.run({ version: 1, ops: ["memory_get"] }, () => op.run({}, ctx));
|
|
789
|
+
assert.deepEqual(result, { ran: true });
|
|
790
|
+
|
|
791
|
+
// Allowed under no ALS (undefined).
|
|
792
|
+
assert.deepEqual(await op.run({}, ctx), { ran: true });
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// ===========================================================================
|
|
796
|
+
// assertFleetWideOperationAllowed — fleet-wide maintenance guard (issue #1850 round 10).
|
|
797
|
+
// Distinct class from the id-addressed routes (round 9) and param-namespace
|
|
798
|
+
// ops (round 4): maintenance ops that run ACROSS ALL namespaces carry no
|
|
799
|
+
// `namespace` arg, so the tools/call effective-namespace gate never fires. A
|
|
800
|
+
// namespace-scoped token must not trigger cross-namespace maintenance.
|
|
801
|
+
// ===========================================================================
|
|
802
|
+
|
|
803
|
+
test("assertFleetWideOperationAllowed: no-op for unrestricted / legacy tokens", () => {
|
|
804
|
+
// Legacy (absent record) — unrestricted.
|
|
805
|
+
assert.doesNotThrow(() => assertFleetWideOperationAllowed(undefined));
|
|
806
|
+
assert.doesNotThrow(() => assertFleetWideOperationAllowed(null));
|
|
807
|
+
// Explicit-unrestricted (version present, no namespaces axis).
|
|
808
|
+
const explicitUnrestricted = validateCapabilitiesForMint(undefined, OPERATION_NAMES);
|
|
809
|
+
assert.doesNotThrow(() => assertFleetWideOperationAllowed(explicitUnrestricted));
|
|
810
|
+
// Op-scoped but namespace-UNrestricted ⇒ still allowed: the fleet-wide
|
|
811
|
+
// concern is the namespaces axis; the ops axis is enforced separately by
|
|
812
|
+
// assertOperationAllowed. A token with only an ops allow-list may run the
|
|
813
|
+
// op (it cannot exceed its op scope, and has no namespace restriction).
|
|
814
|
+
const opScoped = validateCapabilitiesForMint({ ops: ["graph_edge_decay_run"] }, OPERATION_NAMES);
|
|
815
|
+
assert.doesNotThrow(() => assertFleetWideOperationAllowed(opScoped));
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("assertFleetWideOperationAllowed: namespace-SCOPED token is denied (fail closed)", () => {
|
|
819
|
+
const scoped = validateCapabilitiesForMint({ namespaces: ["ns_a"] }, OPERATION_NAMES);
|
|
820
|
+
assert.throws(
|
|
821
|
+
() => assertFleetWideOperationAllowed(scoped),
|
|
822
|
+
(err: unknown) => err instanceof EngramAccessForbiddenError && /across all namespaces/.test(err.message),
|
|
823
|
+
);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test("assertFleetWideOperationAllowed: scoped token whose allow-list INCLUDES the default is STILL denied (fleet-wide ≠ default)", () => {
|
|
827
|
+
// A fleet-wide op affects EVERY namespace — it is not equivalent to an op
|
|
828
|
+
// scoped to the default namespace. enforceNamespaceAllowList maps undefined
|
|
829
|
+
// → default and would wrongly PASS such a token; the fleet-wide guard must
|
|
830
|
+
// NOT (that is the whole point of a dedicated guard for this class).
|
|
831
|
+
const scopedWithDefault = validateCapabilitiesForMint({ namespaces: ["default"] }, OPERATION_NAMES);
|
|
832
|
+
assert.throws(
|
|
833
|
+
() => assertFleetWideOperationAllowed(scopedWithDefault),
|
|
834
|
+
EngramAccessForbiddenError,
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("assertFleetWideOperationAllowed: deny-all namespaces ([]) is denied", () => {
|
|
839
|
+
const denyAll = validateCapabilitiesForMint({ namespaces: [] }, OPERATION_NAMES);
|
|
840
|
+
assert.throws(
|
|
841
|
+
() => assertFleetWideOperationAllowed(denyAll),
|
|
842
|
+
EngramAccessForbiddenError,
|
|
843
|
+
);
|
|
844
|
+
});
|