@remnic/core 9.3.649 → 9.3.651
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-cli.js +36 -35
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +16 -16
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +15 -15
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +13 -13
- package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
- package/dist/bootstrap.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/causal-consolidation.js +6 -6
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
- package/dist/chunk-3XGWCZ63.js.map +1 -0
- package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
- package/dist/{chunk-NMIOW7XG.js → chunk-4PTKFBST.js} +2 -2
- package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
- package/dist/chunk-5FOCXX5E.js +34 -0
- package/dist/chunk-5FOCXX5E.js.map +1 -0
- package/dist/{chunk-XUGVP7ZU.js → chunk-5WSDHTBO.js} +166 -47
- package/dist/chunk-5WSDHTBO.js.map +1 -0
- package/dist/{chunk-WPCCNSWO.js → chunk-6UKL6IXM.js} +4 -4
- package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
- package/dist/chunk-7LWRCOP7.js.map +1 -0
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
- package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
- package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
- package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
- package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
- package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
- package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
- package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
- package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
- package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
- package/dist/{chunk-ZT6R3WR3.js → chunk-LFTLXOFX.js} +4 -4
- package/dist/{chunk-CNRZ6WJU.js → chunk-MF32AL7N.js} +5 -5
- package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
- package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
- package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
- package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
- package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
- package/dist/chunk-PS3SYNHP.js.map +1 -0
- package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
- package/dist/{chunk-FQYFMIKG.js → chunk-RKN5J4RO.js} +26 -26
- package/dist/{chunk-FUXV6HSO.js → chunk-RSS2KWN6.js} +5 -5
- package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
- package/dist/{chunk-5ETA6OAS.js → chunk-SLYD3AH4.js} +617 -89
- package/dist/chunk-SLYD3AH4.js.map +1 -0
- package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
- package/dist/chunk-UU6MVCJ6.js.map +1 -0
- package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
- package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
- package/dist/{chunk-RRRCNIPK.js → chunk-WJK75OCH.js} +4 -4
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
- package/dist/chunk-XBIACVCO.js.map +1 -0
- package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
- package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
- package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
- package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +31 -31
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/event-order-recall.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/explicit-cue-recall.d.ts +7 -0
- package/dist/explicit-cue-recall.js +2 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.js +2 -2
- package/dist/focused-list-recall.d.ts +6 -0
- package/dist/focused-list-recall.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +84 -83
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/lcm-fallback-read.d.ts +71 -0
- package/dist/lcm-fallback-read.js +10 -0
- package/dist/lcm-fallback-read.js.map +1 -0
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/namespaces/migrate.js +7 -7
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-toolkit.js +9 -9
- package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +30 -29
- package/dist/recall-planner-llm.js +2 -2
- package/dist/response-guidance-recall.d.ts +6 -0
- package/dist/response-guidance-recall.js +2 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.js +2 -2
- package/dist/summarizer.js +3 -3
- package/dist/targeted-fact-recall.d.ts +6 -0
- package/dist/targeted-fact-recall.js +2 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-service-lcm-forgery.test.ts +410 -0
- package/src/access-service-observe-lcm-parity.test.ts +1397 -0
- package/src/access-service-observe-scope.test.ts +599 -0
- package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
- package/src/access-service.ts +1270 -113
- package/src/coding/coding-namespace.test.ts +44 -0
- package/src/coding/coding-namespace.ts +163 -0
- package/src/event-order-recall.ts +8 -0
- package/src/explicit-cue-recall.ts +70 -29
- package/src/focused-list-recall.ts +23 -1
- package/src/lcm-fallback-read.ts +113 -0
- package/src/orchestrator.ts +331 -26
- package/src/response-guidance-recall.ts +21 -1
- package/src/targeted-fact-recall.ts +24 -3
- package/dist/chunk-5ETA6OAS.js.map +0 -1
- package/dist/chunk-6NKAQ74D.js.map +0 -1
- package/dist/chunk-DB5A3NHS.js.map +0 -1
- package/dist/chunk-OAZ5MFUB.js.map +0 -1
- package/dist/chunk-OZKZ2TRP.js.map +0 -1
- package/dist/chunk-VWT3F4IV.js.map +0 -1
- package/dist/chunk-XUGVP7ZU.js.map +0 -1
- /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
- /package/dist/{chunk-NMIOW7XG.js.map → chunk-4PTKFBST.js.map} +0 -0
- /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
- /package/dist/{chunk-WPCCNSWO.js.map → chunk-6UKL6IXM.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
- /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
- /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
- /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
- /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
- /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
- /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
- /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
- /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
- /package/dist/{chunk-ZT6R3WR3.js.map → chunk-LFTLXOFX.js.map} +0 -0
- /package/dist/{chunk-CNRZ6WJU.js.map → chunk-MF32AL7N.js.map} +0 -0
- /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
- /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
- /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
- /package/dist/{chunk-FQYFMIKG.js.map → chunk-RKN5J4RO.js.map} +0 -0
- /package/dist/{chunk-FUXV6HSO.js.map → chunk-RSS2KWN6.js.map} +0 -0
- /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
- /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
- /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
- /package/dist/{chunk-RRRCNIPK.js.map → chunk-WJK75OCH.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
- /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
- /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1505 thread 2 (P1): LCM read/write/compaction key PARITY.
|
|
3
|
+
*
|
|
4
|
+
* `observe` archives LCM / structured history under
|
|
5
|
+
* `${effectiveNamespace}:${sessionKey}` (the scope-plan write namespace). The
|
|
6
|
+
* LCM archive filters strictly by `session_id`, so a same-session reader and
|
|
7
|
+
* the compaction flush/record path MUST derive the EXACT same key, or a
|
|
8
|
+
* project-scoped session misses its own compressed-history / structured /
|
|
9
|
+
* targeted-fact evidence (and compaction flushes the wrong queue).
|
|
10
|
+
*
|
|
11
|
+
* This suite proves the key the WRITER (`observe`) produces equals:
|
|
12
|
+
* 1. the key the orchestrator recall READERS compute
|
|
13
|
+
* (`resolveSelfNamespace(sessionKey)` → `lcmSessionKeyForNamespace`), and
|
|
14
|
+
* 2. the key the compaction flush/record path computes.
|
|
15
|
+
* across the scenario matrix:
|
|
16
|
+
* (a) explicit namespace
|
|
17
|
+
* (b) auto-scoped via cwd (git repo)
|
|
18
|
+
* (c) auto-scoped via projectTag
|
|
19
|
+
* (d) no overlay (projectScope:false / namespacesEnabled:false) — must stay
|
|
20
|
+
* byte-for-byte the raw sessionKey (single-user regression guard).
|
|
21
|
+
*
|
|
22
|
+
* It also unit-tests the shared `lcmSessionKeyForNamespace` encoder so the
|
|
23
|
+
* write/read contract is pinned at the boundary.
|
|
24
|
+
*/
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
28
|
+
import { tmpdir } from "node:os";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import test from "node:test";
|
|
31
|
+
|
|
32
|
+
import { EngramAccessService } from "./access-service.js";
|
|
33
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
34
|
+
import type { EngramAccessObserveRequest } from "./access-service.js";
|
|
35
|
+
import {
|
|
36
|
+
combineNamespaces,
|
|
37
|
+
lcmSessionKeyForNamespace,
|
|
38
|
+
projectNamespaceName,
|
|
39
|
+
projectTagProjectId,
|
|
40
|
+
} from "./coding/coding-namespace.js";
|
|
41
|
+
import { resolveGitContext } from "./coding/git-context.js";
|
|
42
|
+
import { defaultNamespaceForPrincipal } from "./namespaces/principal.js";
|
|
43
|
+
import type { CodingContext, PluginConfig } from "./types.js";
|
|
44
|
+
|
|
45
|
+
interface ParityProbe {
|
|
46
|
+
orch: Orchestrator;
|
|
47
|
+
contexts: Map<string, CodingContext>;
|
|
48
|
+
lcmWriteKeys: string[];
|
|
49
|
+
compactionFlushKeys: string[];
|
|
50
|
+
compactionRecordKeys: string[];
|
|
51
|
+
searchSessionIds: Array<string | undefined>;
|
|
52
|
+
searchSessionPrefixes: Array<string | undefined>;
|
|
53
|
+
extractionCalls: Array<{
|
|
54
|
+
sessionKeys: string[];
|
|
55
|
+
writeNamespaceOverride?: string;
|
|
56
|
+
principalOverride?: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build an orchestrator stub that (a) records the LCM archival key `observe`
|
|
62
|
+
* writes under, (b) records the LCM key the compaction flush/record path
|
|
63
|
+
* targets, and (c) delegates `resolveSelfNamespace` / `resolvePrincipal` /
|
|
64
|
+
* `applyCodingNamespaceOverlay` to the REAL Orchestrator prototype so the
|
|
65
|
+
* reader-side namespace resolution is exercised exactly as production does it.
|
|
66
|
+
*/
|
|
67
|
+
function makeParityProbe(overrides: Partial<PluginConfig> = {}): ParityProbe {
|
|
68
|
+
const contexts = new Map<string, CodingContext>();
|
|
69
|
+
const lcmWriteKeys: string[] = [];
|
|
70
|
+
const compactionFlushKeys: string[] = [];
|
|
71
|
+
const compactionRecordKeys: string[] = [];
|
|
72
|
+
const searchSessionIds: Array<string | undefined> = [];
|
|
73
|
+
const searchSessionPrefixes: Array<string | undefined> = [];
|
|
74
|
+
const extractionCalls: ParityProbe["extractionCalls"] = [];
|
|
75
|
+
|
|
76
|
+
const config = {
|
|
77
|
+
namespacesEnabled: true,
|
|
78
|
+
defaultNamespace: "default",
|
|
79
|
+
sharedNamespace: "shared",
|
|
80
|
+
namespacePolicies: [],
|
|
81
|
+
// Production default. The #1505 round-3 read-authorization gate consults
|
|
82
|
+
// `recallNamespacesForPrincipal`, which reads `defaultRecallNamespaces`;
|
|
83
|
+
// omitting it would throw. Per-test overrides can still narrow it.
|
|
84
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
85
|
+
codingMode: { projectScope: true },
|
|
86
|
+
memoryDir: "/synthetic/remnic-observe-lcm-parity",
|
|
87
|
+
// LCM-only test: keep objective-state off so the storage router is not hit.
|
|
88
|
+
objectiveStateMemoryEnabled: false,
|
|
89
|
+
objectiveStateSnapshotWritesEnabled: false,
|
|
90
|
+
principalFromSessionKeyMode: "prefix",
|
|
91
|
+
principalFromSessionKeyRules: [],
|
|
92
|
+
recallCrossNamespaceBudgetEnabled: false,
|
|
93
|
+
recallCrossNamespaceBudgetWindowMs: 60_000,
|
|
94
|
+
recallCrossNamespaceBudgetSoftLimit: 10,
|
|
95
|
+
recallCrossNamespaceBudgetHardLimit: 30,
|
|
96
|
+
...overrides,
|
|
97
|
+
} as unknown as PluginConfig;
|
|
98
|
+
|
|
99
|
+
const orch = {
|
|
100
|
+
config,
|
|
101
|
+
getCodingContextForSession: (sk: string | undefined) =>
|
|
102
|
+
(sk ? contexts.get(sk) : null) ?? null,
|
|
103
|
+
setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
|
|
104
|
+
if (ctx === null) contexts.delete(sk);
|
|
105
|
+
else contexts.set(sk, ctx);
|
|
106
|
+
},
|
|
107
|
+
applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
|
|
108
|
+
Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
|
|
109
|
+
resolvePrincipal: (sk?: string) =>
|
|
110
|
+
Orchestrator.prototype.resolvePrincipal.call(orch, sk),
|
|
111
|
+
resolveSelfNamespace: (sk?: string) =>
|
|
112
|
+
Orchestrator.prototype.resolveSelfNamespace.call(orch, sk),
|
|
113
|
+
lcmEngine: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
enqueueObserveMessages: (sessionKey: string) => {
|
|
116
|
+
lcmWriteKeys.push(sessionKey);
|
|
117
|
+
},
|
|
118
|
+
waitForSessionObserveIdle: async (_sessionKey: string) => {},
|
|
119
|
+
preCompactionFlush: async (sessionKey: string) => {
|
|
120
|
+
compactionFlushKeys.push(sessionKey);
|
|
121
|
+
},
|
|
122
|
+
recordCompaction: async (
|
|
123
|
+
sessionKey: string,
|
|
124
|
+
_before: number,
|
|
125
|
+
_after: number,
|
|
126
|
+
) => {
|
|
127
|
+
compactionRecordKeys.push(sessionKey);
|
|
128
|
+
},
|
|
129
|
+
searchContextFull: async (
|
|
130
|
+
_query: string,
|
|
131
|
+
_limit: number,
|
|
132
|
+
sessionId?: string,
|
|
133
|
+
sessionPrefix?: string,
|
|
134
|
+
) => {
|
|
135
|
+
searchSessionIds.push(sessionId);
|
|
136
|
+
searchSessionPrefixes.push(sessionPrefix);
|
|
137
|
+
return [];
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
// Capture extraction routing/identity so the provenance-principal tests can
|
|
141
|
+
// assert what `observe` threads into `ingestReplayBatch`.
|
|
142
|
+
ingestReplayBatch: async (
|
|
143
|
+
turns: Array<{ sessionKey: string }>,
|
|
144
|
+
options: { writeNamespaceOverride?: string; principalOverride?: string } = {},
|
|
145
|
+
) => {
|
|
146
|
+
extractionCalls.push({
|
|
147
|
+
sessionKeys: turns.map((t) => t.sessionKey),
|
|
148
|
+
writeNamespaceOverride: options.writeNamespaceOverride,
|
|
149
|
+
principalOverride: options.principalOverride,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
} as unknown as Orchestrator;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
orch,
|
|
156
|
+
contexts,
|
|
157
|
+
lcmWriteKeys,
|
|
158
|
+
compactionFlushKeys,
|
|
159
|
+
compactionRecordKeys,
|
|
160
|
+
searchSessionIds,
|
|
161
|
+
searchSessionPrefixes,
|
|
162
|
+
extractionCalls,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function observeRequest(
|
|
167
|
+
overrides: Partial<EngramAccessObserveRequest>,
|
|
168
|
+
): EngramAccessObserveRequest {
|
|
169
|
+
return {
|
|
170
|
+
sessionKey: "pi-geek:abc123",
|
|
171
|
+
// skipExtraction keeps this an LCM-only round-trip.
|
|
172
|
+
skipExtraction: true,
|
|
173
|
+
messages: [
|
|
174
|
+
{ role: "user", content: "what database are we using?" },
|
|
175
|
+
{ role: "assistant", content: "we use postgres for the primary store" },
|
|
176
|
+
],
|
|
177
|
+
...overrides,
|
|
178
|
+
} as EngramAccessObserveRequest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function withSelfPolicyPrefix(principal: string): Partial<PluginConfig> {
|
|
182
|
+
return {
|
|
183
|
+
namespacePolicies: [
|
|
184
|
+
{ name: principal, readPrincipals: [principal], writePrincipals: [principal] },
|
|
185
|
+
],
|
|
186
|
+
principalFromSessionKeyMode: "prefix",
|
|
187
|
+
principalFromSessionKeyRules: [{ match: `${principal}:`, principal }],
|
|
188
|
+
} as Partial<PluginConfig>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reproduce EXACTLY what `orchestrator.recallInternal` computes for the LCM
|
|
193
|
+
* reader session_id of a same-session bare recall (no explicit namespace
|
|
194
|
+
* override): the coding-overlay namespace when one applies, else the default
|
|
195
|
+
* store (NOT the principal self base — that would prefix a namespace an
|
|
196
|
+
* unqualified observe never wrote to), then encode through the shared helper.
|
|
197
|
+
* This is the same rule as the orchestrator's private
|
|
198
|
+
* `lcmReadNamespaceForSession`.
|
|
199
|
+
*/
|
|
200
|
+
function readerLcmKey(probe: ParityProbe, sessionKey: string): string {
|
|
201
|
+
const principal = (
|
|
202
|
+
probe.orch as unknown as { resolvePrincipal: (sk?: string) => string | undefined }
|
|
203
|
+
).resolvePrincipal(sessionKey);
|
|
204
|
+
const base = defaultNamespaceForPrincipal(principal, probe.orch.config);
|
|
205
|
+
const overlaid = (
|
|
206
|
+
probe.orch as unknown as {
|
|
207
|
+
applyCodingNamespaceOverlay: (sk: string | undefined, base: string) => string;
|
|
208
|
+
}
|
|
209
|
+
).applyCodingNamespaceOverlay(sessionKey, base);
|
|
210
|
+
const effectiveNamespace =
|
|
211
|
+
overlaid !== base ? overlaid : probe.orch.config.defaultNamespace;
|
|
212
|
+
return (
|
|
213
|
+
lcmSessionKeyForNamespace(
|
|
214
|
+
effectiveNamespace,
|
|
215
|
+
sessionKey,
|
|
216
|
+
probe.orch.config.defaultNamespace,
|
|
217
|
+
) ?? sessionKey
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// #1495 P1: reserved structural sentinel framing the namespaced LCM key
|
|
222
|
+
// (`\x1f<namespace>\x1f<sessionKey>`). Kept in sync with
|
|
223
|
+
// `coding-namespace.ts:LCM_NS_SENTINEL`. U+001F cannot occur in a route
|
|
224
|
+
// namespace (`[A-Za-z0-9._-]`) nor any legitimate session key, so the
|
|
225
|
+
// namespaced and default key-spaces are provably disjoint and an overlay id is
|
|
226
|
+
// unforgeable from a caller-controlled raw default-store sessionKey.
|
|
227
|
+
const LCM_NS_SENTINEL = "\u001f";
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Encode the expected namespaced LCM `session_id` exactly as production does —
|
|
231
|
+
* via the shared `lcmSessionKeyForNamespace` helper — so these parity
|
|
232
|
+
* assertions stay shape-agnostic and never re-hardcode the `:`-join the #1495
|
|
233
|
+
* P1 fix removed (CLAUDE.md rule 22: never fork the encoding).
|
|
234
|
+
*/
|
|
235
|
+
function encodeNs(namespace: string, sessionKey: string): string {
|
|
236
|
+
return lcmSessionKeyForNamespace(namespace, sessionKey, "default") ?? sessionKey;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Assert a namespaced LCM write key (new #1495 P1 encoding
|
|
241
|
+
* `\x1f<overlayNs>\x1f<sessionKey>`) was archived under an overlay namespace
|
|
242
|
+
* whose name begins with `overlayPrefix` (e.g. `alice-`, `pi-geek-`). Replaces
|
|
243
|
+
* the pre-#1495 `writeKey.startsWith("<principal>-")`, which no longer holds now
|
|
244
|
+
* that the key is sentinel-framed.
|
|
245
|
+
*/
|
|
246
|
+
function assertOverlayWriteKey(writeKey: string, overlayPrefix: string): void {
|
|
247
|
+
assert.ok(
|
|
248
|
+
writeKey.startsWith(LCM_NS_SENTINEL),
|
|
249
|
+
`write key must be sentinel-framed, got ${JSON.stringify(writeKey)}`,
|
|
250
|
+
);
|
|
251
|
+
const overlayNs = writeKey.slice(LCM_NS_SENTINEL.length).split(LCM_NS_SENTINEL)[0]!;
|
|
252
|
+
assert.ok(
|
|
253
|
+
overlayNs.startsWith(overlayPrefix),
|
|
254
|
+
`overlay namespace must start with ${overlayPrefix}, got ${overlayNs} (key ${JSON.stringify(writeKey)})`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
test("#1505 thread 2 helper: write/read encoding agrees and collapses to raw key on the default store", () => {
|
|
259
|
+
// Non-default namespace ⇒ sentinel-framed, NOT `${ns}:${sessionKey}` (#1495 P1
|
|
260
|
+
// unforgeable encoding).
|
|
261
|
+
assert.equal(
|
|
262
|
+
lcmSessionKeyForNamespace("acme", "sk", "default"),
|
|
263
|
+
`${LCM_NS_SENTINEL}acme${LCM_NS_SENTINEL}sk`,
|
|
264
|
+
);
|
|
265
|
+
// Default namespace ⇒ raw key (single-store byte-for-byte).
|
|
266
|
+
assert.equal(lcmSessionKeyForNamespace("default", "sk", "default"), "sk");
|
|
267
|
+
// Undefined namespace ⇒ raw key.
|
|
268
|
+
assert.equal(lcmSessionKeyForNamespace(undefined, "sk", "default"), "sk");
|
|
269
|
+
// Empty namespace ⇒ raw key.
|
|
270
|
+
assert.equal(lcmSessionKeyForNamespace("", "sk", "default"), "sk");
|
|
271
|
+
// Missing sessionKey is passed through unchanged (recall's `?? "default"`
|
|
272
|
+
// fallback handles the undefined case downstream).
|
|
273
|
+
assert.equal(lcmSessionKeyForNamespace("acme", undefined, "default"), undefined);
|
|
274
|
+
|
|
275
|
+
// #1495 P1 UNFORGEABILITY: a default-store raw sessionKey can NEVER reproduce
|
|
276
|
+
// another namespace's encoded id, so a forged default read cannot collide with
|
|
277
|
+
// an overlay write key.
|
|
278
|
+
const overlayKey = lcmSessionKeyForNamespace("acme", "sk", "default");
|
|
279
|
+
// The classic forgery: a default-store caller passing the OLD `${ns}:${sk}`
|
|
280
|
+
// string. Under the new encoding the default path returns it verbatim (it does
|
|
281
|
+
// not start with the sentinel), which is disjoint from the overlay key-space.
|
|
282
|
+
const forgedDefaultKey = lcmSessionKeyForNamespace("default", "acme:sk", "default");
|
|
283
|
+
assert.equal(forgedDefaultKey, "acme:sk");
|
|
284
|
+
assert.notEqual(
|
|
285
|
+
forgedDefaultKey,
|
|
286
|
+
overlayKey,
|
|
287
|
+
"a forged `${ns}:${sk}` default key must NOT equal the overlay encoded id",
|
|
288
|
+
);
|
|
289
|
+
// Even a default sessionKey that itself begins with the sentinel is escaped so
|
|
290
|
+
// it can never equal an overlay key (whose 2nd char is a namespace char).
|
|
291
|
+
const sentinelLeadingDefault = lcmSessionKeyForNamespace(
|
|
292
|
+
"default",
|
|
293
|
+
`${LCM_NS_SENTINEL}acme${LCM_NS_SENTINEL}sk`,
|
|
294
|
+
"default",
|
|
295
|
+
);
|
|
296
|
+
assert.notEqual(
|
|
297
|
+
sentinelLeadingDefault,
|
|
298
|
+
overlayKey,
|
|
299
|
+
"a sentinel-leading default key must be escaped disjoint from the overlay key-space",
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("#1505 thread 2 (c) projectTag: observe LCM write key == recall reader key == compaction keys", async () => {
|
|
304
|
+
const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
|
|
305
|
+
const service = new EngramAccessService(probe.orch);
|
|
306
|
+
|
|
307
|
+
const res = await service.observe(
|
|
308
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const expectedNs = combineNamespaces(
|
|
312
|
+
"pi-geek",
|
|
313
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
314
|
+
);
|
|
315
|
+
const expectedKey = encodeNs(expectedNs, "pi-geek:abc123");
|
|
316
|
+
assert.equal(res.effectiveNamespace, expectedNs);
|
|
317
|
+
assert.notEqual(expectedNs, "default", "overlay must change the namespace");
|
|
318
|
+
|
|
319
|
+
// WRITE key.
|
|
320
|
+
assert.equal(probe.lcmWriteKeys.length, 1);
|
|
321
|
+
assert.equal(probe.lcmWriteKeys[0], expectedKey, "LCM write key");
|
|
322
|
+
|
|
323
|
+
// READ key (observe attached the coding context, so the reader resolves the
|
|
324
|
+
// SAME overlay namespace as the writer).
|
|
325
|
+
assert.equal(
|
|
326
|
+
readerLcmKey(probe, "pi-geek:abc123"),
|
|
327
|
+
expectedKey,
|
|
328
|
+
"recall reader LCM key must equal the observe write key",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// COMPACTION keys (no explicit namespace ⇒ overlay applied from session ctx).
|
|
332
|
+
await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
|
|
333
|
+
await service.lcmCompactionRecord({
|
|
334
|
+
sessionKey: "pi-geek:abc123",
|
|
335
|
+
tokensBefore: 100,
|
|
336
|
+
tokensAfter: 10,
|
|
337
|
+
});
|
|
338
|
+
assert.equal(probe.compactionFlushKeys[0], expectedKey, "flush key");
|
|
339
|
+
assert.equal(probe.compactionRecordKeys[0], expectedKey, "record key");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("#1505 thread 2 (b) cwd git repo: observe LCM write key == recall reader key == compaction keys", async () => {
|
|
343
|
+
const repoDir = mkdtempSync(join(tmpdir(), "remnic-lcm-parity-git-"));
|
|
344
|
+
const git = (...args: string[]) =>
|
|
345
|
+
execFileSync("git", args, { cwd: repoDir, stdio: "pipe" });
|
|
346
|
+
git("init", "-q");
|
|
347
|
+
git("config", "user.email", "test@example.com");
|
|
348
|
+
git("config", "user.name", "Test");
|
|
349
|
+
try {
|
|
350
|
+
const gitCtx = await resolveGitContext(repoDir);
|
|
351
|
+
assert.ok(gitCtx, "synthetic repo must resolve a git context");
|
|
352
|
+
|
|
353
|
+
const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
|
|
354
|
+
const service = new EngramAccessService(probe.orch);
|
|
355
|
+
|
|
356
|
+
const res = await service.observe(
|
|
357
|
+
observeRequest({ sessionKey: "pi-geek:cwd1", cwd: repoDir }),
|
|
358
|
+
);
|
|
359
|
+
const expectedNs = combineNamespaces(
|
|
360
|
+
"pi-geek",
|
|
361
|
+
projectNamespaceName(gitCtx!.projectId),
|
|
362
|
+
);
|
|
363
|
+
const expectedKey = encodeNs(expectedNs, "pi-geek:cwd1");
|
|
364
|
+
|
|
365
|
+
assert.equal(res.effectiveNamespace, expectedNs);
|
|
366
|
+
assert.equal(probe.lcmWriteKeys[0], expectedKey, "LCM write key");
|
|
367
|
+
assert.equal(
|
|
368
|
+
readerLcmKey(probe, "pi-geek:cwd1"),
|
|
369
|
+
expectedKey,
|
|
370
|
+
"recall reader LCM key must equal the observe write key",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
await service.lcmCompactionFlush({ sessionKey: "pi-geek:cwd1" });
|
|
374
|
+
await service.lcmCompactionRecord({
|
|
375
|
+
sessionKey: "pi-geek:cwd1",
|
|
376
|
+
tokensBefore: 100,
|
|
377
|
+
tokensAfter: 10,
|
|
378
|
+
});
|
|
379
|
+
assert.equal(probe.compactionFlushKeys[0], expectedKey, "flush key");
|
|
380
|
+
assert.equal(probe.compactionRecordKeys[0], expectedKey, "record key");
|
|
381
|
+
} finally {
|
|
382
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("#1505 thread 2 (a) explicit namespace: write key prefixed; compaction agrees", async () => {
|
|
387
|
+
const probe = makeParityProbe({
|
|
388
|
+
namespacePolicies: [
|
|
389
|
+
{ name: "team", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
|
|
390
|
+
],
|
|
391
|
+
principalFromSessionKeyMode: "prefix",
|
|
392
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
393
|
+
} as Partial<PluginConfig>);
|
|
394
|
+
const service = new EngramAccessService(probe.orch);
|
|
395
|
+
|
|
396
|
+
const res = await service.observe(
|
|
397
|
+
observeRequest({ sessionKey: "pi-geek:abc123", namespace: "team" }),
|
|
398
|
+
);
|
|
399
|
+
assert.equal(res.effectiveNamespace, "team");
|
|
400
|
+
assert.equal(
|
|
401
|
+
probe.lcmWriteKeys[0],
|
|
402
|
+
encodeNs("team", "pi-geek:abc123"),
|
|
403
|
+
"LCM write key",
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// A recall that wants the `team` namespace passes namespace=team; its reader
|
|
407
|
+
// key is built from that override and agrees with the write key.
|
|
408
|
+
assert.equal(
|
|
409
|
+
lcmSessionKeyForNamespace("team", "pi-geek:abc123", "default"),
|
|
410
|
+
encodeNs("team", "pi-geek:abc123"),
|
|
411
|
+
"explicit-namespace recall reader key matches the write key",
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Compaction with the same explicit namespace agrees.
|
|
415
|
+
await service.lcmCompactionFlush({
|
|
416
|
+
sessionKey: "pi-geek:abc123",
|
|
417
|
+
namespace: "team",
|
|
418
|
+
});
|
|
419
|
+
await service.lcmCompactionRecord({
|
|
420
|
+
sessionKey: "pi-geek:abc123",
|
|
421
|
+
namespace: "team",
|
|
422
|
+
tokensBefore: 100,
|
|
423
|
+
tokensAfter: 10,
|
|
424
|
+
});
|
|
425
|
+
assert.equal(
|
|
426
|
+
probe.compactionFlushKeys[0],
|
|
427
|
+
encodeNs("team", "pi-geek:abc123"),
|
|
428
|
+
"flush key",
|
|
429
|
+
);
|
|
430
|
+
assert.equal(
|
|
431
|
+
probe.compactionRecordKeys[0],
|
|
432
|
+
encodeNs("team", "pi-geek:abc123"),
|
|
433
|
+
"record key",
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("#1505 thread 2 (d) projectScope:false ⇒ raw sessionKey everywhere (single-user regression guard)", async () => {
|
|
438
|
+
const probe = makeParityProbe({
|
|
439
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
440
|
+
codingMode: { projectScope: false },
|
|
441
|
+
} as Partial<PluginConfig>);
|
|
442
|
+
const service = new EngramAccessService(probe.orch);
|
|
443
|
+
|
|
444
|
+
const res = await service.observe(
|
|
445
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
446
|
+
);
|
|
447
|
+
// No overlay ⇒ effective namespace is the default store ⇒ raw key.
|
|
448
|
+
assert.equal(res.effectiveNamespace, "default");
|
|
449
|
+
assert.equal(probe.lcmWriteKeys[0], "pi-geek:abc123", "raw LCM write key");
|
|
450
|
+
assert.equal(
|
|
451
|
+
readerLcmKey(probe, "pi-geek:abc123"),
|
|
452
|
+
"pi-geek:abc123",
|
|
453
|
+
"reader key must be the raw sessionKey",
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
|
|
457
|
+
await service.lcmCompactionRecord({
|
|
458
|
+
sessionKey: "pi-geek:abc123",
|
|
459
|
+
tokensBefore: 100,
|
|
460
|
+
tokensAfter: 10,
|
|
461
|
+
});
|
|
462
|
+
assert.equal(probe.compactionFlushKeys[0], "pi-geek:abc123", "raw flush key");
|
|
463
|
+
assert.equal(probe.compactionRecordKeys[0], "pi-geek:abc123", "raw record key");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("#1505 thread 2 (d) namespacesEnabled:false ⇒ raw sessionKey everywhere", async () => {
|
|
467
|
+
const probe = makeParityProbe({
|
|
468
|
+
namespacesEnabled: false,
|
|
469
|
+
} as Partial<PluginConfig>);
|
|
470
|
+
const service = new EngramAccessService(probe.orch);
|
|
471
|
+
|
|
472
|
+
const res = await service.observe(
|
|
473
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
474
|
+
);
|
|
475
|
+
assert.equal(res.effectiveNamespace, "default");
|
|
476
|
+
assert.equal(probe.lcmWriteKeys[0], "pi-geek:abc123", "raw LCM write key");
|
|
477
|
+
assert.equal(readerLcmKey(probe, "pi-geek:abc123"), "pi-geek:abc123");
|
|
478
|
+
|
|
479
|
+
await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
|
|
480
|
+
assert.equal(probe.compactionFlushKeys[0], "pi-geek:abc123", "raw flush key");
|
|
481
|
+
|
|
482
|
+
// Defensive: even without a defaultNamespaceForPrincipal policy, the base is
|
|
483
|
+
// the default store when namespaces are disabled.
|
|
484
|
+
assert.equal(
|
|
485
|
+
defaultNamespaceForPrincipal("pi-geek", probe.orch.config),
|
|
486
|
+
"default",
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("#1505 thread 1: extraction provenance principal is the resolved principal (NOT default) for a project-scoped observe with an encoded-principal key", async () => {
|
|
491
|
+
// Identity-vs-routing separation. A project-scoped observe prefixes the LCM
|
|
492
|
+
// key with the overlay namespace (`pi-geek-project-...:pi-geek:abc123`). Before
|
|
493
|
+
// this fix, that prefixed key was ALSO fed to extraction as the turn
|
|
494
|
+
// sessionKey, so `resolvePrincipal` parsed `pi-geek-project-...` → no prefix
|
|
495
|
+
// rule match → `default`, mis-attributing provenance. The fix passes the
|
|
496
|
+
// ORIGINAL sessionKey (identity) plus principalOverride (the resolved
|
|
497
|
+
// principal) and writeNamespaceOverride (routing).
|
|
498
|
+
const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
|
|
499
|
+
const service = new EngramAccessService(probe.orch);
|
|
500
|
+
|
|
501
|
+
await service.observe(
|
|
502
|
+
observeRequest({
|
|
503
|
+
sessionKey: "pi-geek:abc123",
|
|
504
|
+
projectTag: "Blend/Supply",
|
|
505
|
+
skipExtraction: false, // exercise the extraction path
|
|
506
|
+
}),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const expectedNs = combineNamespaces(
|
|
510
|
+
"pi-geek",
|
|
511
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
512
|
+
);
|
|
513
|
+
assert.equal(probe.extractionCalls.length, 1);
|
|
514
|
+
// Provenance identity: the resolved principal, never a default parsed from the
|
|
515
|
+
// prefixed key.
|
|
516
|
+
assert.equal(
|
|
517
|
+
probe.extractionCalls[0].principalOverride,
|
|
518
|
+
"pi-geek",
|
|
519
|
+
"provenance principal must be the resolved principal, not default",
|
|
520
|
+
);
|
|
521
|
+
// The extraction turns carry the ORIGINAL session key (identity / threading).
|
|
522
|
+
assert.deepEqual(
|
|
523
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
524
|
+
new Set(["pi-geek:abc123"]),
|
|
525
|
+
"extraction turns must carry the ORIGINAL un-prefixed session key",
|
|
526
|
+
);
|
|
527
|
+
// Storage routing is pinned to the effective overlay namespace.
|
|
528
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expectedNs);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("#1505 thread 1: provenance principal honors authenticatedPrincipal not encoded in the session key", async () => {
|
|
532
|
+
// alice authenticates at the transport layer but the raw sessionKey ("sess-1")
|
|
533
|
+
// encodes no principal. The scope plan resolves principal=alice (auth
|
|
534
|
+
// precedence), so extraction provenance must be pinned to alice — independent
|
|
535
|
+
// of what `resolvePrincipal("sess-1")` would parse (default).
|
|
536
|
+
const probe = makeParityProbe({
|
|
537
|
+
namespacePolicies: [
|
|
538
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
539
|
+
],
|
|
540
|
+
principalFromSessionKeyMode: "prefix",
|
|
541
|
+
principalFromSessionKeyRules: [],
|
|
542
|
+
} as Partial<PluginConfig>);
|
|
543
|
+
const service = new EngramAccessService(probe.orch);
|
|
544
|
+
|
|
545
|
+
await service.observe(
|
|
546
|
+
observeRequest({
|
|
547
|
+
sessionKey: "sess-1",
|
|
548
|
+
authenticatedPrincipal: "alice",
|
|
549
|
+
skipExtraction: false,
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
assert.equal(probe.extractionCalls.length, 1);
|
|
554
|
+
assert.equal(
|
|
555
|
+
probe.extractionCalls[0].principalOverride,
|
|
556
|
+
"alice",
|
|
557
|
+
"provenance principal must honor the authenticated principal",
|
|
558
|
+
);
|
|
559
|
+
assert.deepEqual(
|
|
560
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
561
|
+
new Set(["sess-1"]),
|
|
562
|
+
"extraction turns carry the original session key",
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("#1505 thread 2: LCM read namespace honors authenticatedPrincipal (write-under-alice ⇒ read-under-alice)", async () => {
|
|
567
|
+
// alice authenticates at the transport layer but is NOT encoded in the raw
|
|
568
|
+
// sessionKey ("sess-1"). With a PROJECT overlay, observe archives LCM under
|
|
569
|
+
// `combineNamespaces("alice", project):sess-1`. A same-session recall that
|
|
570
|
+
// supplies the SAME authenticated principal (principalOverride) must derive the
|
|
571
|
+
// base = alice so the overlay namespace — and thus the LCM read key — matches
|
|
572
|
+
// the write. Without the override, `lcmReadNamespaceForSession` derives the
|
|
573
|
+
// base from `resolvePrincipal("sess-1")` → default, so the overlay would be
|
|
574
|
+
// `combineNamespaces("default", project)` and the reader would MISS alice's
|
|
575
|
+
// evidence (the thread-2 bug).
|
|
576
|
+
// makeParityProbe defaults codingMode to { projectScope: true }; alice is NOT
|
|
577
|
+
// encoded in any prefix rule, so resolvePrincipal("sess-1") → default.
|
|
578
|
+
const probe = makeParityProbe({
|
|
579
|
+
namespacePolicies: [
|
|
580
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
581
|
+
],
|
|
582
|
+
principalFromSessionKeyMode: "prefix",
|
|
583
|
+
principalFromSessionKeyRules: [],
|
|
584
|
+
} as Partial<PluginConfig>);
|
|
585
|
+
const service = new EngramAccessService(probe.orch);
|
|
586
|
+
|
|
587
|
+
// WRITE: alice observes sess-1 with a project tag → overlay applies on alice's
|
|
588
|
+
// self base. (No explicit namespace.)
|
|
589
|
+
await service.observe(
|
|
590
|
+
observeRequest({
|
|
591
|
+
sessionKey: "sess-1",
|
|
592
|
+
authenticatedPrincipal: "alice",
|
|
593
|
+
projectTag: "Blend/Supply",
|
|
594
|
+
}),
|
|
595
|
+
);
|
|
596
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
597
|
+
assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay namespace, got ${writeKey}
|
|
598
|
+
|
|
599
|
+
// The orchestrator's real lcmReadNamespaceForSession (the stub delegates
|
|
600
|
+
// resolvePrincipal/overlay to the prototype).
|
|
601
|
+
const lcmReadNamespaceForSession = Orchestrator.prototype[
|
|
602
|
+
"lcmReadNamespaceForSession" as keyof Orchestrator
|
|
603
|
+
] as unknown as (
|
|
604
|
+
this: Orchestrator,
|
|
605
|
+
sk?: string,
|
|
606
|
+
principalOverride?: string,
|
|
607
|
+
) => string;
|
|
608
|
+
|
|
609
|
+
// Without the override, the base derives from resolvePrincipal("sess-1") →
|
|
610
|
+
// default, so the read namespace is the DEFAULT-based overlay (the bug).
|
|
611
|
+
const withoutOverride = lcmReadNamespaceForSession.call(probe.orch, "sess-1");
|
|
612
|
+
assert.ok(
|
|
613
|
+
!withoutOverride.startsWith("alice-"),
|
|
614
|
+
`without override the read base must NOT be alice (demonstrates the bug), got ${withoutOverride}`,
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// With the authenticated principal override, the read base is alice → the read
|
|
618
|
+
// namespace matches the write namespace, so the read key matches the write key.
|
|
619
|
+
const withOverride = lcmReadNamespaceForSession.call(
|
|
620
|
+
probe.orch,
|
|
621
|
+
"sess-1",
|
|
622
|
+
"alice",
|
|
623
|
+
);
|
|
624
|
+
assert.equal(
|
|
625
|
+
encodeNs(withOverride, "sess-1"),
|
|
626
|
+
writeKey,
|
|
627
|
+
"authenticated principal override ⇒ read key matches the alice-prefixed write key",
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("#1505 thread 2 compaction regression: flush/record overlay-derived key matches observe (pre-fix used base only)", async () => {
|
|
632
|
+
// Before the fix, compaction flush/record built `${resolveWritableNamespace(
|
|
633
|
+
// request.namespace)}:${sessionKey}` which, with NO explicit namespace, is
|
|
634
|
+
// the BASE (config.defaultNamespace) — never the coding overlay. So an
|
|
635
|
+
// auto-scoped session flushed `pi-geek:abc123` (no overlay) while observe
|
|
636
|
+
// archived under `pi-geek-project-...:pi-geek:abc123`. This pins that they
|
|
637
|
+
// now agree.
|
|
638
|
+
const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
|
|
639
|
+
const service = new EngramAccessService(probe.orch);
|
|
640
|
+
|
|
641
|
+
await service.observe(
|
|
642
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
643
|
+
);
|
|
644
|
+
await service.lcmCompactionFlush({ sessionKey: "pi-geek:abc123" });
|
|
645
|
+
|
|
646
|
+
const observedWriteKey = probe.lcmWriteKeys[0];
|
|
647
|
+
assertOverlayWriteKey(observedWriteKey, "pi-geek-"); // observe must archive under the overlay namespace, got ${observedWriteKey}
|
|
648
|
+
assert.equal(
|
|
649
|
+
probe.compactionFlushKeys[0],
|
|
650
|
+
observedWriteKey,
|
|
651
|
+
"compaction flush must target the overlay key, not the base",
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("#1505 round 3: access lcmSearch routes the session_id through the SCOPED (overlay) key", async () => {
|
|
656
|
+
// cursor "LCM search misses overlay keys" / codex "Route access LCM search
|
|
657
|
+
// through the scoped key". A project-scoped observe (no explicit namespace)
|
|
658
|
+
// archives under `${overlayNs}:${sessionKey}` and binds the coding context to
|
|
659
|
+
// the session. A subsequent lcmSearch({ sessionKey }) with NO explicit
|
|
660
|
+
// namespace must search under the SAME overlay key — not the raw sessionKey —
|
|
661
|
+
// or it misses the turns just archived.
|
|
662
|
+
const probe = makeParityProbe(withSelfPolicyPrefix("pi-geek"));
|
|
663
|
+
const service = new EngramAccessService(probe.orch);
|
|
664
|
+
|
|
665
|
+
await service.observe(
|
|
666
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
667
|
+
);
|
|
668
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
669
|
+
// New #1495 P1 encoding: `\x1f<overlayNs>\x1f<sessionKey>`. Parse the overlay
|
|
670
|
+
// namespace out of the sentinel frame (the namespace is `\x1f`-free).
|
|
671
|
+
assert.ok(
|
|
672
|
+
writeKey.startsWith(LCM_NS_SENTINEL),
|
|
673
|
+
`observe must archive under the sentinel-framed overlay key, got ${JSON.stringify(writeKey)}`,
|
|
674
|
+
);
|
|
675
|
+
const overlayNs = writeKey.slice(LCM_NS_SENTINEL.length).split(LCM_NS_SENTINEL)[0]!;
|
|
676
|
+
assert.ok(
|
|
677
|
+
overlayNs.startsWith("pi-geek-"),
|
|
678
|
+
`overlay namespace must be pi-geek's project overlay, got ${overlayNs}`,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
await service.lcmSearch({
|
|
682
|
+
query: "what database are we using?",
|
|
683
|
+
sessionKey: "pi-geek:abc123",
|
|
684
|
+
sessionPrefix: "pi-geek:",
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
assert.equal(
|
|
688
|
+
probe.searchSessionIds[0],
|
|
689
|
+
writeKey,
|
|
690
|
+
"lcmSearch session_id must be the overlay-scoped key, matching the write key",
|
|
691
|
+
);
|
|
692
|
+
assert.ok(
|
|
693
|
+
!probe.searchSessionIds.includes("pi-geek:abc123"),
|
|
694
|
+
`lcmSearch must NOT query the raw sessionKey; queried: ${JSON.stringify(probe.searchSessionIds)}`,
|
|
695
|
+
);
|
|
696
|
+
// The sessionPrefix is framed with the same overlay namespace (so it stays a
|
|
697
|
+
// valid LIKE-prefix of the overlay full keys).
|
|
698
|
+
assert.equal(
|
|
699
|
+
probe.searchSessionPrefixes[0],
|
|
700
|
+
encodeNs(overlayNs, "pi-geek:"),
|
|
701
|
+
"lcmSearch sessionPrefix must carry the overlay namespace too",
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("#1505 round 3: lcmSearch on a single-store / no-overlay session keeps the raw key (regression guard)", async () => {
|
|
706
|
+
const probe = makeParityProbe({
|
|
707
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
708
|
+
codingMode: { projectScope: false },
|
|
709
|
+
} as Partial<PluginConfig>);
|
|
710
|
+
const service = new EngramAccessService(probe.orch);
|
|
711
|
+
|
|
712
|
+
await service.lcmSearch({
|
|
713
|
+
query: "anything",
|
|
714
|
+
sessionKey: "pi-geek:abc123",
|
|
715
|
+
});
|
|
716
|
+
assert.equal(
|
|
717
|
+
probe.searchSessionIds[0],
|
|
718
|
+
"pi-geek:abc123",
|
|
719
|
+
"no overlay ⇒ lcmSearch searches the raw sessionKey (byte-for-byte preserved)",
|
|
720
|
+
);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("#1505 round 3: extraction is pinned to the resolved writeNamespace even when it equals the default store", async () => {
|
|
724
|
+
// codex "Pin default-store extraction writes too". An unqualified observe by a
|
|
725
|
+
// principal that HAS a self namespace resolves writeNamespace ==
|
|
726
|
+
// config.defaultNamespace (general path) but, left unpinned, runExtraction
|
|
727
|
+
// would fall back to defaultNamespaceForPrincipal(principal) == the SELF
|
|
728
|
+
// namespace — diverging from LCM/objective-state/response. With namespaces
|
|
729
|
+
// enabled, observe must pin writeNamespaceOverride = writeNamespace (= default)
|
|
730
|
+
// so every side effect lands in ONE namespace.
|
|
731
|
+
const probe = makeParityProbe({
|
|
732
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
733
|
+
// No overlay so writeNamespace collapses to config.defaultNamespace.
|
|
734
|
+
codingMode: { projectScope: false },
|
|
735
|
+
} as Partial<PluginConfig>);
|
|
736
|
+
const service = new EngramAccessService(probe.orch);
|
|
737
|
+
|
|
738
|
+
await service.observe(
|
|
739
|
+
observeRequest({
|
|
740
|
+
sessionKey: "pi-geek:abc123",
|
|
741
|
+
skipExtraction: false, // exercise extraction
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
assert.equal(probe.extractionCalls.length, 1);
|
|
746
|
+
assert.equal(
|
|
747
|
+
probe.extractionCalls[0].writeNamespaceOverride,
|
|
748
|
+
"default",
|
|
749
|
+
"extraction must be pinned to the default store, not left to fall back to the principal self namespace",
|
|
750
|
+
);
|
|
751
|
+
// Identity is still the original key + resolved principal.
|
|
752
|
+
assert.deepEqual(
|
|
753
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
754
|
+
new Set(["pi-geek:abc123"]),
|
|
755
|
+
);
|
|
756
|
+
assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
760
|
+
// #1505 round 3 (codex P2): READ-AUTHORIZATION gating of the overlay LCM read
|
|
761
|
+
// key. The round-2 parity fix made LCM READS always substitute the principal
|
|
762
|
+
// self-overlay namespace. That bypassed the read-authorization / readable-
|
|
763
|
+
// recall-namespace gating the rest of recall honors, so a principal who can
|
|
764
|
+
// WRITE but NOT READ its self namespace (or whose `defaultRecallNamespaces`
|
|
765
|
+
// omits `self`) would have `<principal>-project-*` overlay rows injected into
|
|
766
|
+
// recall / returned by `lcmSearch` even though QMD/file recall excludes them
|
|
767
|
+
// (cross-tenant read leak). Both sites must gate the overlay substitution by
|
|
768
|
+
// the readable recall namespace set (rule 42 read/write parity; rule 48
|
|
769
|
+
// least-privilege).
|
|
770
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
771
|
+
|
|
772
|
+
test("#1505 round 3 thread 1: orchestrator LCM read key falls back to default when the principal can WRITE but not READ its self namespace", async () => {
|
|
773
|
+
// alice may WRITE her self namespace but NOT read it (readPrincipals omits
|
|
774
|
+
// alice). A project-scoped observe archives LCM under
|
|
775
|
+
// `alice-project-*:sess-1`, but a no-namespace recall by alice may NOT inject
|
|
776
|
+
// those overlay rows — QMD/file recall would exclude `alice` (unreadable), so
|
|
777
|
+
// the LCM read key MUST collapse to the default store too.
|
|
778
|
+
const probe = makeParityProbe({
|
|
779
|
+
namespacePolicies: [
|
|
780
|
+
// WRITE-only self policy: alice can write `alice` but cannot read it.
|
|
781
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
782
|
+
],
|
|
783
|
+
principalFromSessionKeyMode: "prefix",
|
|
784
|
+
principalFromSessionKeyRules: [],
|
|
785
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
786
|
+
} as Partial<PluginConfig>);
|
|
787
|
+
|
|
788
|
+
// Bind a coding context to sess-1 so the overlay would apply on alice's base.
|
|
789
|
+
probe.contexts.set("sess-1", {
|
|
790
|
+
projectId: "blend-supply",
|
|
791
|
+
projectName: "Blend/Supply",
|
|
792
|
+
} as unknown as CodingContext);
|
|
793
|
+
|
|
794
|
+
const lcmReadNamespaceForSession = Orchestrator.prototype[
|
|
795
|
+
"lcmReadNamespaceForSession" as keyof Orchestrator
|
|
796
|
+
] as unknown as (
|
|
797
|
+
this: Orchestrator,
|
|
798
|
+
sk?: string,
|
|
799
|
+
principalOverride?: string,
|
|
800
|
+
) => string;
|
|
801
|
+
|
|
802
|
+
// With the authenticated principal = alice (NOT encoded in sess-1), the
|
|
803
|
+
// overlay base is `alice`. alice cannot READ `alice`, so the read namespace
|
|
804
|
+
// MUST fall back to the default store (NOT `alice-project-*`).
|
|
805
|
+
const readNs = lcmReadNamespaceForSession.call(probe.orch, "sess-1", "alice");
|
|
806
|
+
assert.equal(
|
|
807
|
+
readNs,
|
|
808
|
+
"default",
|
|
809
|
+
`unreadable self base ⇒ LCM read key must fall back to the default store, got ${readNs}`,
|
|
810
|
+
);
|
|
811
|
+
assert.ok(
|
|
812
|
+
!readNs.startsWith("alice-"),
|
|
813
|
+
`LCM read key must NOT inject alice's overlay rows when alice cannot read her self namespace, got ${readNs}`,
|
|
814
|
+
);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("#1505 round 3 thread 1: orchestrator LCM read key falls back to default when defaultRecallNamespaces omits self", async () => {
|
|
818
|
+
// alice CAN read her self namespace, but `defaultRecallNamespaces` omits
|
|
819
|
+
// `self`, so QMD/file recall never includes `alice` for a no-namespace
|
|
820
|
+
// recall. The overlay LCM read key must mirror that exclusion.
|
|
821
|
+
const probe = makeParityProbe({
|
|
822
|
+
namespacePolicies: [
|
|
823
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
824
|
+
],
|
|
825
|
+
principalFromSessionKeyMode: "prefix",
|
|
826
|
+
principalFromSessionKeyRules: [],
|
|
827
|
+
// Self deliberately omitted from the recall set.
|
|
828
|
+
defaultRecallNamespaces: ["shared"],
|
|
829
|
+
} as Partial<PluginConfig>);
|
|
830
|
+
|
|
831
|
+
probe.contexts.set("sess-1", {
|
|
832
|
+
projectId: "blend-supply",
|
|
833
|
+
projectName: "Blend/Supply",
|
|
834
|
+
} as unknown as CodingContext);
|
|
835
|
+
|
|
836
|
+
const lcmReadNamespaceForSession = Orchestrator.prototype[
|
|
837
|
+
"lcmReadNamespaceForSession" as keyof Orchestrator
|
|
838
|
+
] as unknown as (
|
|
839
|
+
this: Orchestrator,
|
|
840
|
+
sk?: string,
|
|
841
|
+
principalOverride?: string,
|
|
842
|
+
) => string;
|
|
843
|
+
|
|
844
|
+
const readNs = lcmReadNamespaceForSession.call(probe.orch, "sess-1", "alice");
|
|
845
|
+
assert.equal(
|
|
846
|
+
readNs,
|
|
847
|
+
"default",
|
|
848
|
+
`self omitted from defaultRecallNamespaces ⇒ overlay LCM read key must fall back to default, got ${readNs}`,
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("#1505 round 3 thread 1: orchestrator LCM read key still uses the overlay when self IS readable (round-2 positive case preserved)", async () => {
|
|
853
|
+
// pi-geek can read AND write its self namespace, and `self` is in the recall
|
|
854
|
+
// set, so the overlay LCM read key must STILL be the project-scoped overlay
|
|
855
|
+
// (the round-2 parity behavior must stay green).
|
|
856
|
+
const probe = makeParityProbe({
|
|
857
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
858
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
859
|
+
} as Partial<PluginConfig>);
|
|
860
|
+
|
|
861
|
+
probe.contexts.set("pi-geek:abc123", {
|
|
862
|
+
projectId: "blend-supply",
|
|
863
|
+
projectName: "Blend/Supply",
|
|
864
|
+
} as unknown as CodingContext);
|
|
865
|
+
|
|
866
|
+
const lcmReadNamespaceForSession = Orchestrator.prototype[
|
|
867
|
+
"lcmReadNamespaceForSession" as keyof Orchestrator
|
|
868
|
+
] as unknown as (this: Orchestrator, sk?: string) => string;
|
|
869
|
+
|
|
870
|
+
const readNs = lcmReadNamespaceForSession.call(probe.orch, "pi-geek:abc123");
|
|
871
|
+
assert.ok(
|
|
872
|
+
readNs.startsWith("pi-geek-"),
|
|
873
|
+
`readable self ⇒ overlay LCM read key must be the project overlay, got ${readNs}`,
|
|
874
|
+
);
|
|
875
|
+
assert.notEqual(readNs, "default");
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("#1505 round 3 thread 2: lcmSearch returns NO overlay rows when the principal cannot read its self base (authorized fallback)", async () => {
|
|
879
|
+
// alice authenticates and passes the `default` read check, but her policy does
|
|
880
|
+
// NOT permit reading her self/overlay base (readPrincipals omits alice). A
|
|
881
|
+
// coding context is bound to the session, so the overlay WOULD apply — but the
|
|
882
|
+
// read-authorization gate must keep the just-authorized (default) namespace,
|
|
883
|
+
// so lcmSearch queries the RAW sessionKey, NOT `alice-project-*`.
|
|
884
|
+
const probe = makeParityProbe({
|
|
885
|
+
namespacePolicies: [
|
|
886
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
887
|
+
],
|
|
888
|
+
principalFromSessionKeyMode: "prefix",
|
|
889
|
+
principalFromSessionKeyRules: [],
|
|
890
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
891
|
+
} as Partial<PluginConfig>);
|
|
892
|
+
const service = new EngramAccessService(probe.orch);
|
|
893
|
+
|
|
894
|
+
probe.contexts.set("sess-1", {
|
|
895
|
+
projectId: "blend-supply",
|
|
896
|
+
projectName: "Blend/Supply",
|
|
897
|
+
} as unknown as CodingContext);
|
|
898
|
+
|
|
899
|
+
await service.lcmSearch({
|
|
900
|
+
query: "what database are we using?",
|
|
901
|
+
sessionKey: "sess-1",
|
|
902
|
+
sessionPrefix: "alice:",
|
|
903
|
+
authenticatedPrincipal: "alice",
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
assert.equal(
|
|
907
|
+
probe.searchSessionIds[0],
|
|
908
|
+
"sess-1",
|
|
909
|
+
`unauthorized overlay base ⇒ lcmSearch must query the raw sessionKey, got ${String(
|
|
910
|
+
probe.searchSessionIds[0],
|
|
911
|
+
)}`,
|
|
912
|
+
);
|
|
913
|
+
assert.ok(
|
|
914
|
+
!String(probe.searchSessionIds[0] ?? "").includes("alice-"),
|
|
915
|
+
`lcmSearch must NOT return alice-project-* rows to a caller who cannot read the alice namespace; queried: ${JSON.stringify(
|
|
916
|
+
probe.searchSessionIds,
|
|
917
|
+
)}`,
|
|
918
|
+
);
|
|
919
|
+
// The prefix must NOT carry the overlay namespace either.
|
|
920
|
+
assert.ok(
|
|
921
|
+
!String(probe.searchSessionPrefixes[0] ?? "").includes("alice-"),
|
|
922
|
+
`lcmSearch sessionPrefix must NOT carry the alice overlay namespace; got ${String(
|
|
923
|
+
probe.searchSessionPrefixes[0],
|
|
924
|
+
)}`,
|
|
925
|
+
);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("#1505 round 4 thread (codex P2): compaction flush/record target the OVERLAY key even when the principal can WRITE but not READ its self base", async () => {
|
|
929
|
+
// The round-3 read-authorization gate is SHARED by lcmCompactionFlush/Record,
|
|
930
|
+
// but compaction is a WRITE/maintenance op on the queue observe just wrote.
|
|
931
|
+
// alice can WRITE her self namespace but NOT read it. observe archives under
|
|
932
|
+
// `alice-project-*:sess-1`; compaction must flush/record that SAME overlay key
|
|
933
|
+
// (gated by WRITE authorization), NOT fall back to the default/raw key — else
|
|
934
|
+
// the project-scoped LCM queue is never flushed/recorded.
|
|
935
|
+
const probe = makeParityProbe({
|
|
936
|
+
namespacePolicies: [
|
|
937
|
+
// Write-only self policy: alice can write `alice` but cannot read it.
|
|
938
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
939
|
+
],
|
|
940
|
+
principalFromSessionKeyMode: "prefix",
|
|
941
|
+
principalFromSessionKeyRules: [],
|
|
942
|
+
// self deliberately omitted from the recall set too — read gate would fall
|
|
943
|
+
// back, but the WRITE gate must still honour the overlay.
|
|
944
|
+
defaultRecallNamespaces: ["shared"],
|
|
945
|
+
} as Partial<PluginConfig>);
|
|
946
|
+
const service = new EngramAccessService(probe.orch);
|
|
947
|
+
|
|
948
|
+
// WRITE: alice observes sess-1 with a project tag → overlay applies on alice's
|
|
949
|
+
// write-authorized self base.
|
|
950
|
+
await service.observe(
|
|
951
|
+
observeRequest({
|
|
952
|
+
sessionKey: "sess-1",
|
|
953
|
+
authenticatedPrincipal: "alice",
|
|
954
|
+
projectTag: "Blend/Supply",
|
|
955
|
+
}),
|
|
956
|
+
);
|
|
957
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
958
|
+
assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay key, got ${writeKey}
|
|
959
|
+
|
|
960
|
+
await service.lcmCompactionFlush({
|
|
961
|
+
sessionKey: "sess-1",
|
|
962
|
+
authenticatedPrincipal: "alice",
|
|
963
|
+
});
|
|
964
|
+
await service.lcmCompactionRecord({
|
|
965
|
+
sessionKey: "sess-1",
|
|
966
|
+
authenticatedPrincipal: "alice",
|
|
967
|
+
tokensBefore: 100,
|
|
968
|
+
tokensAfter: 10,
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
assert.equal(
|
|
972
|
+
probe.compactionFlushKeys[0],
|
|
973
|
+
writeKey,
|
|
974
|
+
"compaction flush must target the overlay key (write-authorized), not the default/raw key",
|
|
975
|
+
);
|
|
976
|
+
assert.equal(
|
|
977
|
+
probe.compactionRecordKeys[0],
|
|
978
|
+
writeKey,
|
|
979
|
+
"compaction record must target the overlay key (write-authorized), not the default/raw key",
|
|
980
|
+
);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test("#1505 round 4 thread (codex P2): a read-only lcmSearch by the SAME write-only principal still does NOT leak the overlay rows (read vs write gate divergence)", async () => {
|
|
984
|
+
// Companion to the compaction-write-gate test: the SAME write-only, read-denied
|
|
985
|
+
// principal must STILL get the authorized fallback (raw key) on lcmSearch — the
|
|
986
|
+
// read gate and the write gate diverge by design.
|
|
987
|
+
const probe = makeParityProbe({
|
|
988
|
+
namespacePolicies: [
|
|
989
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
990
|
+
],
|
|
991
|
+
principalFromSessionKeyMode: "prefix",
|
|
992
|
+
principalFromSessionKeyRules: [],
|
|
993
|
+
defaultRecallNamespaces: ["shared"],
|
|
994
|
+
} as Partial<PluginConfig>);
|
|
995
|
+
const service = new EngramAccessService(probe.orch);
|
|
996
|
+
|
|
997
|
+
probe.contexts.set("sess-1", {
|
|
998
|
+
projectId: "blend-supply",
|
|
999
|
+
projectName: "Blend/Supply",
|
|
1000
|
+
} as unknown as CodingContext);
|
|
1001
|
+
|
|
1002
|
+
await service.lcmSearch({
|
|
1003
|
+
query: "anything",
|
|
1004
|
+
sessionKey: "sess-1",
|
|
1005
|
+
authenticatedPrincipal: "alice",
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
assert.equal(
|
|
1009
|
+
probe.searchSessionIds[0],
|
|
1010
|
+
"sess-1",
|
|
1011
|
+
"read gate: write-only/read-denied principal must NOT receive alice-project-* rows on lcmSearch",
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test("#1505 round 3 thread 2: lcmSearch still routes through the overlay key when the principal CAN read its self base (round-2 positive case preserved)", async () => {
|
|
1016
|
+
// alice can read AND write her self namespace, so the authorized overlay LCM
|
|
1017
|
+
// read key is honored — lcmSearch routes the session_id through the overlay.
|
|
1018
|
+
const probe = makeParityProbe({
|
|
1019
|
+
namespacePolicies: [
|
|
1020
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
1021
|
+
],
|
|
1022
|
+
principalFromSessionKeyMode: "prefix",
|
|
1023
|
+
principalFromSessionKeyRules: [],
|
|
1024
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1025
|
+
} as Partial<PluginConfig>);
|
|
1026
|
+
const service = new EngramAccessService(probe.orch);
|
|
1027
|
+
|
|
1028
|
+
// Archive under alice's overlay (binds the coding context to the session).
|
|
1029
|
+
const writeRes = await service.observe(
|
|
1030
|
+
observeRequest({
|
|
1031
|
+
sessionKey: "sess-1",
|
|
1032
|
+
authenticatedPrincipal: "alice",
|
|
1033
|
+
projectTag: "Blend/Supply",
|
|
1034
|
+
}),
|
|
1035
|
+
);
|
|
1036
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
1037
|
+
assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's overlay key, got ${writeKey}
|
|
1038
|
+
|
|
1039
|
+
await service.lcmSearch({
|
|
1040
|
+
query: "what database are we using?",
|
|
1041
|
+
sessionKey: "sess-1",
|
|
1042
|
+
authenticatedPrincipal: "alice",
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
assert.equal(
|
|
1046
|
+
probe.searchSessionIds[0],
|
|
1047
|
+
writeKey,
|
|
1048
|
+
"readable self ⇒ lcmSearch session_id must be the overlay-scoped key matching the write key",
|
|
1049
|
+
);
|
|
1050
|
+
assert.equal(
|
|
1051
|
+
writeRes.effectiveNamespace?.startsWith("alice-"),
|
|
1052
|
+
true,
|
|
1053
|
+
"observe effectiveNamespace must be the alice overlay (sanity)",
|
|
1054
|
+
);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
test("#1505 thread NBHWs (codex P2): restrictive `default` WRITE policy + writable self ⇒ compaction flush/record the OVERLAY queue (no `not writable: default` throw)", async () => {
|
|
1058
|
+
// The root defect: `lcmCompactionFlush`/`Record` PRE-authorized
|
|
1059
|
+
// `undefined ⇒ config.defaultNamespace` via `resolveWritableNamespace` BEFORE
|
|
1060
|
+
// the scoped write key was computed. Under a deployment whose `default`
|
|
1061
|
+
// namespace has a RESTRICTIVE write policy (alice may NOT write `default`) but
|
|
1062
|
+
// where alice CAN write her self/project overlay, `observe({ projectTag })`
|
|
1063
|
+
// succeeds and archives LCM under `alice-project-*:sess-1` — yet compaction
|
|
1064
|
+
// threw `namespace is not writable: default`, so the queue observe just wrote
|
|
1065
|
+
// could never be flushed or recorded.
|
|
1066
|
+
//
|
|
1067
|
+
// FAIL-BEFORE: both compaction calls throw `namespace is not writable:
|
|
1068
|
+
// default`. PASS-AFTER: compaction derives the namespace through the SAME
|
|
1069
|
+
// write-scoped plan/gate observe uses (`resolveMemoryScopePlan`), authorizes
|
|
1070
|
+
// the writable self base, and flushes/records the overlay key.
|
|
1071
|
+
const probe = makeParityProbe({
|
|
1072
|
+
namespacePolicies: [
|
|
1073
|
+
// RESTRICTIVE default: NO principal may write (or read) `default`.
|
|
1074
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1075
|
+
// alice CAN write (and read) her self namespace.
|
|
1076
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
1077
|
+
],
|
|
1078
|
+
principalFromSessionKeyMode: "prefix",
|
|
1079
|
+
principalFromSessionKeyRules: [],
|
|
1080
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1081
|
+
} as Partial<PluginConfig>);
|
|
1082
|
+
const service = new EngramAccessService(probe.orch);
|
|
1083
|
+
|
|
1084
|
+
// observe succeeds under alice's writable self/project overlay even though
|
|
1085
|
+
// `default` is not writable.
|
|
1086
|
+
const observeRes = await service.observe(
|
|
1087
|
+
observeRequest({
|
|
1088
|
+
sessionKey: "sess-1",
|
|
1089
|
+
authenticatedPrincipal: "alice",
|
|
1090
|
+
projectTag: "Blend/Supply",
|
|
1091
|
+
}),
|
|
1092
|
+
);
|
|
1093
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
1094
|
+
assertOverlayWriteKey(writeKey, "alice-"); // observe must archive under alice's writable overlay key, got ${writeKey}
|
|
1095
|
+
assert.ok(
|
|
1096
|
+
observeRes.effectiveNamespace?.startsWith("alice-"),
|
|
1097
|
+
"observe effectiveNamespace must be the alice overlay (sanity)",
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
// Compaction must NOT throw `not writable: default` and must target the
|
|
1101
|
+
// overlay queue observe wrote.
|
|
1102
|
+
const flushRes = await service.lcmCompactionFlush({
|
|
1103
|
+
sessionKey: "sess-1",
|
|
1104
|
+
authenticatedPrincipal: "alice",
|
|
1105
|
+
});
|
|
1106
|
+
const recordRes = await service.lcmCompactionRecord({
|
|
1107
|
+
sessionKey: "sess-1",
|
|
1108
|
+
authenticatedPrincipal: "alice",
|
|
1109
|
+
tokensBefore: 100,
|
|
1110
|
+
tokensAfter: 10,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
assert.equal(flushRes.flushed, true, "flush must succeed");
|
|
1114
|
+
assert.equal(recordRes.recorded, true, "record must succeed");
|
|
1115
|
+
assert.equal(
|
|
1116
|
+
probe.compactionFlushKeys[0],
|
|
1117
|
+
writeKey,
|
|
1118
|
+
"compaction flush must target the overlay key observe wrote, not the (unwritable) default key",
|
|
1119
|
+
);
|
|
1120
|
+
assert.equal(
|
|
1121
|
+
probe.compactionRecordKeys[0],
|
|
1122
|
+
writeKey,
|
|
1123
|
+
"compaction record must target the overlay key observe wrote, not the (unwritable) default key",
|
|
1124
|
+
);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test("#1505 thread NBHWz (sweep): restrictive `default` READ policy + readable self ⇒ lcmSearch reads the self/recall-authorized overlay (no `not readable: default` throw)", async () => {
|
|
1128
|
+
// Convergence sweep: `lcmSearch` is the SAME defect class as the raw-excerpt
|
|
1129
|
+
// path — it pre-authorized `undefined ⇒ config.defaultNamespace` via
|
|
1130
|
+
// `resolveReadableNamespace` BEFORE deriving the scoped read namespace. Under a
|
|
1131
|
+
// restrictive `default` READ policy where pi-geek's self namespace IS readable,
|
|
1132
|
+
// normal recall succeeds via `recallNamespacesForPrincipal`, so `lcmSearch`
|
|
1133
|
+
// must too. FAIL-BEFORE: throws `namespace is not readable: default`.
|
|
1134
|
+
// PASS-AFTER: routes through the readable self overlay.
|
|
1135
|
+
const probe = makeParityProbe({
|
|
1136
|
+
namespacePolicies: [
|
|
1137
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1138
|
+
{ name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
|
|
1139
|
+
],
|
|
1140
|
+
principalFromSessionKeyMode: "prefix",
|
|
1141
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
1142
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1143
|
+
} as Partial<PluginConfig>);
|
|
1144
|
+
const service = new EngramAccessService(probe.orch);
|
|
1145
|
+
|
|
1146
|
+
// observe archives under pi-geek's readable+writable project overlay.
|
|
1147
|
+
await service.observe(
|
|
1148
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
1149
|
+
);
|
|
1150
|
+
const writeKey = probe.lcmWriteKeys[0];
|
|
1151
|
+
assertOverlayWriteKey(writeKey, "pi-geek-"); // observe must archive under pi-geek's overlay key, got ${writeKey}
|
|
1152
|
+
|
|
1153
|
+
// lcmSearch with NO explicit namespace must NOT throw `not readable: default`
|
|
1154
|
+
// and must route the session_id through the readable overlay key.
|
|
1155
|
+
const res = await service.lcmSearch({
|
|
1156
|
+
query: "what database are we using?",
|
|
1157
|
+
sessionKey: "pi-geek:abc123",
|
|
1158
|
+
});
|
|
1159
|
+
assert.equal(res.lcmEnabled, true);
|
|
1160
|
+
assert.equal(
|
|
1161
|
+
probe.searchSessionIds[0],
|
|
1162
|
+
writeKey,
|
|
1163
|
+
"lcmSearch must route through the readable self overlay key, not pre-authorize the denied default",
|
|
1164
|
+
);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test("#1505 codex P1 'Don't treat any readable namespace as default LCM access': self EXCLUDED from recall + `shared` readable + `default` denied ⇒ lcmSearch SUPPRESSES (never the denied default store, never `shared:`)", async () => {
|
|
1168
|
+
// codex P1 on the round-7 head: my first NBHWz fix treated ANY readable recall
|
|
1169
|
+
// namespace (e.g. `shared`) as license to read the DEFAULT LCM store. But the
|
|
1170
|
+
// implicit LCM read can ONLY target the coding overlay (when the SELF base is
|
|
1171
|
+
// readable-in-recall) or the default store — never `shared`. So when the self
|
|
1172
|
+
// base is NOT readable-in-recall AND `default` is denied, there is NO
|
|
1173
|
+
// authorized LCM target: lcmSearch must SUPPRESS (empty), NOT fall back to the
|
|
1174
|
+
// denied default store (which, sessionless, would scan the whole archive).
|
|
1175
|
+
//
|
|
1176
|
+
// alice can WRITE her self base (so observe archives under the overlay) but
|
|
1177
|
+
// CANNOT read it; `self` is omitted from the recall set; only `shared` is
|
|
1178
|
+
// readable; `default` is restrictively unreadable.
|
|
1179
|
+
const probe = makeParityProbe({
|
|
1180
|
+
namespacePolicies: [
|
|
1181
|
+
// Restrictive default: alice may NOT read `default`.
|
|
1182
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1183
|
+
// alice can WRITE but NOT read her self base.
|
|
1184
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
1185
|
+
],
|
|
1186
|
+
principalFromSessionKeyMode: "prefix",
|
|
1187
|
+
principalFromSessionKeyRules: [],
|
|
1188
|
+
// `shared` is readable + in the recall set; `self` is omitted.
|
|
1189
|
+
defaultRecallNamespaces: ["shared"],
|
|
1190
|
+
} as Partial<PluginConfig>);
|
|
1191
|
+
const service = new EngramAccessService(probe.orch);
|
|
1192
|
+
|
|
1193
|
+
// Bind a coding context so the overlay WOULD apply on alice's base.
|
|
1194
|
+
probe.contexts.set("sess-1", {
|
|
1195
|
+
projectId: "blend-supply",
|
|
1196
|
+
projectName: "Blend/Supply",
|
|
1197
|
+
} as unknown as CodingContext);
|
|
1198
|
+
|
|
1199
|
+
const res = await service.lcmSearch({
|
|
1200
|
+
query: "what database are we using?",
|
|
1201
|
+
sessionKey: "sess-1",
|
|
1202
|
+
authenticatedPrincipal: "alice",
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
assert.equal(res.lcmEnabled, true);
|
|
1206
|
+
assert.equal(res.count, 0, "no authorized LCM target ⇒ empty results");
|
|
1207
|
+
// SUPPRESS: self unreadable-in-recall AND default denied ⇒ no authorized LCM
|
|
1208
|
+
// target. `searchContextFull` must NEVER run — not against the denied default
|
|
1209
|
+
// store (raw key), not against `shared`, not against alice's unreadable
|
|
1210
|
+
// overlay.
|
|
1211
|
+
assert.equal(
|
|
1212
|
+
probe.searchSessionIds.length,
|
|
1213
|
+
0,
|
|
1214
|
+
"lcmSearch must SUPPRESS (no searchContextFull) — never read the denied default store via a `shared`-only authorization",
|
|
1215
|
+
);
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test("#1505 cursor 'LCM read gate wrong fallback' (positive): self READABLE-in-recall but overlay-self gate denies ⇒ lcmSearch uses the raw sessionKey (default store), never `shared:`", async () => {
|
|
1219
|
+
// The complementary case to the codex P1 suppress: when the principal self base
|
|
1220
|
+
// IS readable-in-recall (so PROCEED is authorized) but `default` is restrictively
|
|
1221
|
+
// unreadable, the implicit LCM read still collapses to the DEFAULT STORE raw key
|
|
1222
|
+
// for a session whose overlay does not apply — EXACTLY like the orchestrator's
|
|
1223
|
+
// `lcmReadNamespaceForSession` — and NEVER an arbitrary readable recall namespace
|
|
1224
|
+
// (e.g. `shared`). Here NO coding context is bound, so no overlay applies and the
|
|
1225
|
+
// key is the raw sessionKey.
|
|
1226
|
+
const probe = makeParityProbe({
|
|
1227
|
+
namespacePolicies: [
|
|
1228
|
+
// Restrictive default: alice may NOT read `default`.
|
|
1229
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1230
|
+
// alice CAN read AND write her self base.
|
|
1231
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
1232
|
+
],
|
|
1233
|
+
principalFromSessionKeyMode: "prefix",
|
|
1234
|
+
principalFromSessionKeyRules: [],
|
|
1235
|
+
// self IS readable + in the recall set, so PROCEED is authorized.
|
|
1236
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1237
|
+
} as Partial<PluginConfig>);
|
|
1238
|
+
const service = new EngramAccessService(probe.orch);
|
|
1239
|
+
|
|
1240
|
+
// No coding context bound ⇒ no overlay ⇒ the LCM key is the raw sessionKey.
|
|
1241
|
+
const res = await service.lcmSearch({
|
|
1242
|
+
query: "what database are we using?",
|
|
1243
|
+
sessionKey: "sess-1",
|
|
1244
|
+
authenticatedPrincipal: "alice",
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
assert.equal(res.lcmEnabled, true);
|
|
1248
|
+
assert.equal(
|
|
1249
|
+
probe.searchSessionIds[0],
|
|
1250
|
+
"sess-1",
|
|
1251
|
+
"self-readable PROCEED + no overlay ⇒ lcmSearch queries the raw sessionKey (default store), matching the orchestrator",
|
|
1252
|
+
);
|
|
1253
|
+
for (const id of probe.searchSessionIds) {
|
|
1254
|
+
assert.ok(
|
|
1255
|
+
!String(id ?? "").startsWith("shared"),
|
|
1256
|
+
`lcmSearch must NOT prefix with the shared recall namespace; got ${String(id)}`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test("#1505 thread NBHWz (sweep): no readable LCM namespace ⇒ lcmSearch returns EMPTY (no `not readable: default` throw)", async () => {
|
|
1262
|
+
// Companion: when NO readable LCM namespace exists for an implicit lcmSearch
|
|
1263
|
+
// (restrictive default READ + unreadable self + self omitted from the recall
|
|
1264
|
+
// set), the search returns EMPTY rather than throwing — `searchContextFull` is
|
|
1265
|
+
// never called.
|
|
1266
|
+
const probe = makeParityProbe({
|
|
1267
|
+
namespacePolicies: [
|
|
1268
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1269
|
+
{ name: "alice", readPrincipals: [], writePrincipals: ["alice"] },
|
|
1270
|
+
],
|
|
1271
|
+
principalFromSessionKeyMode: "prefix",
|
|
1272
|
+
principalFromSessionKeyRules: [],
|
|
1273
|
+
defaultRecallNamespaces: [],
|
|
1274
|
+
} as Partial<PluginConfig>);
|
|
1275
|
+
const service = new EngramAccessService(probe.orch);
|
|
1276
|
+
|
|
1277
|
+
const res = await service.lcmSearch({
|
|
1278
|
+
query: "anything",
|
|
1279
|
+
sessionKey: "sess-1",
|
|
1280
|
+
authenticatedPrincipal: "alice",
|
|
1281
|
+
});
|
|
1282
|
+
assert.equal(res.lcmEnabled, true);
|
|
1283
|
+
assert.equal(res.count, 0, "no readable LCM namespace ⇒ empty results");
|
|
1284
|
+
assert.equal(
|
|
1285
|
+
probe.searchSessionIds.length,
|
|
1286
|
+
0,
|
|
1287
|
+
"searchContextFull must NOT be called when no readable LCM namespace exists",
|
|
1288
|
+
);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("#1505 codex P1 (sessionless archive-scan guard): restrictive `default` READ + readable self but NO sessionKey/sessionPrefix ⇒ lcmSearch SUPPRESSES (no unbounded archive scan)", async () => {
|
|
1292
|
+
// codex P1 defense-in-depth: a sessionless, prefixless `lcmSearch` issues
|
|
1293
|
+
// `searchContextFull(query, limit, undefined, undefined)`, scanning the ENTIRE
|
|
1294
|
+
// LCM archive across every session/namespace. Under a restrictive `default`
|
|
1295
|
+
// READ policy (alice cannot read `default`), that scan exposes the denied
|
|
1296
|
+
// default store's rows. Even though alice's SELF base is readable (so the
|
|
1297
|
+
// implicit fallback PROCEEDs), with NO sessionKey the overlay cannot apply and
|
|
1298
|
+
// the read collapses to the default store — so the sessionless scan must be
|
|
1299
|
+
// SUPPRESSED.
|
|
1300
|
+
const probe = makeParityProbe({
|
|
1301
|
+
namespacePolicies: [
|
|
1302
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1303
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
1304
|
+
],
|
|
1305
|
+
principalFromSessionKeyMode: "prefix",
|
|
1306
|
+
principalFromSessionKeyRules: [],
|
|
1307
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1308
|
+
} as Partial<PluginConfig>);
|
|
1309
|
+
const service = new EngramAccessService(probe.orch);
|
|
1310
|
+
|
|
1311
|
+
// NO sessionKey, NO sessionPrefix → would otherwise scan the whole archive.
|
|
1312
|
+
const res = await service.lcmSearch({
|
|
1313
|
+
query: "what database are we using?",
|
|
1314
|
+
authenticatedPrincipal: "alice",
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
assert.equal(res.lcmEnabled, true);
|
|
1318
|
+
assert.equal(res.count, 0, "sessionless + denied default ⇒ empty results");
|
|
1319
|
+
assert.equal(
|
|
1320
|
+
probe.searchSessionIds.length,
|
|
1321
|
+
0,
|
|
1322
|
+
"searchContextFull must NOT run an unbounded archive scan against the denied default store",
|
|
1323
|
+
);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
test("#1505 codex P1 (sessionless regression): namespaces DISABLED (single-store) + no sessionKey ⇒ lcmSearch still scans the archive (byte-for-byte prior behavior)", async () => {
|
|
1327
|
+
// Regression guard: in a single-store deployment (namespaces disabled, default
|
|
1328
|
+
// always readable), a sessionless `lcmSearch` keeps its prior unbounded
|
|
1329
|
+
// behavior — the P1 guard only fires when the principal cannot read the default
|
|
1330
|
+
// store (i.e. namespaces enabled + a restrictive default policy / unauthenticated
|
|
1331
|
+
// caller).
|
|
1332
|
+
const probe = makeParityProbe({
|
|
1333
|
+
namespacesEnabled: false,
|
|
1334
|
+
} as Partial<PluginConfig>);
|
|
1335
|
+
const service = new EngramAccessService(probe.orch);
|
|
1336
|
+
|
|
1337
|
+
await service.lcmSearch({ query: "anything" });
|
|
1338
|
+
|
|
1339
|
+
assert.equal(
|
|
1340
|
+
probe.searchSessionIds.length,
|
|
1341
|
+
1,
|
|
1342
|
+
"namespaces disabled + sessionless ⇒ the archive search still runs (byte-for-byte prior behavior)",
|
|
1343
|
+
);
|
|
1344
|
+
assert.equal(
|
|
1345
|
+
probe.searchSessionIds[0],
|
|
1346
|
+
undefined,
|
|
1347
|
+
"sessionless search passes no session_id filter (archive-wide), unchanged",
|
|
1348
|
+
);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
test("#1505 thread NBHWs regression: restrictive `default` WRITE policy + no overlay (writable self) ⇒ compaction still authorizes the self base, no `not writable: default`", async () => {
|
|
1352
|
+
// Companion: even with projectScope OFF (no overlay), an implicit observe by a
|
|
1353
|
+
// principal that can write its self base archives under the default store ONLY
|
|
1354
|
+
// when objective-state writes are off; with objective-state writes enabled the
|
|
1355
|
+
// scope plan authorizes the self base. Here we keep objective-state off (the
|
|
1356
|
+
// probe default) and projectScope off, so the write namespace collapses to the
|
|
1357
|
+
// default store — but `default` is NOT writable. The scope plan's no-overlay
|
|
1358
|
+
// branch still collapses to `config.defaultNamespace` via
|
|
1359
|
+
// `resolveWritableNamespace(undefined)`, which DOES throw when default is
|
|
1360
|
+
// unwritable — matching observe EXACTLY (if observe can't write, there is no
|
|
1361
|
+
// queue to flush). This pins that compaction and observe agree on the throw.
|
|
1362
|
+
const probe = makeParityProbe({
|
|
1363
|
+
namespacePolicies: [
|
|
1364
|
+
{ name: "default", readPrincipals: [], writePrincipals: [] },
|
|
1365
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
1366
|
+
],
|
|
1367
|
+
principalFromSessionKeyMode: "prefix",
|
|
1368
|
+
principalFromSessionKeyRules: [],
|
|
1369
|
+
defaultRecallNamespaces: ["self", "shared"],
|
|
1370
|
+
codingMode: { projectScope: false, branchScope: false, globalFallback: true },
|
|
1371
|
+
} as Partial<PluginConfig>);
|
|
1372
|
+
const service = new EngramAccessService(probe.orch);
|
|
1373
|
+
|
|
1374
|
+
// No overlay ⇒ implicit write collapses to the (unwritable) default store, so
|
|
1375
|
+
// observe itself rejects. Compaction must reject identically (parity), NOT
|
|
1376
|
+
// succeed against a phantom queue.
|
|
1377
|
+
await assert.rejects(
|
|
1378
|
+
() =>
|
|
1379
|
+
service.observe(
|
|
1380
|
+
observeRequest({
|
|
1381
|
+
sessionKey: "sess-1",
|
|
1382
|
+
authenticatedPrincipal: "alice",
|
|
1383
|
+
}),
|
|
1384
|
+
),
|
|
1385
|
+
/not writable: default/,
|
|
1386
|
+
"no-overlay implicit observe must reject on the unwritable default store",
|
|
1387
|
+
);
|
|
1388
|
+
await assert.rejects(
|
|
1389
|
+
() =>
|
|
1390
|
+
service.lcmCompactionFlush({
|
|
1391
|
+
sessionKey: "sess-1",
|
|
1392
|
+
authenticatedPrincipal: "alice",
|
|
1393
|
+
}),
|
|
1394
|
+
/not writable: default/,
|
|
1395
|
+
"compaction must reject identically to observe when the effective write target is the unwritable default store (parity)",
|
|
1396
|
+
);
|
|
1397
|
+
});
|