@remnic/core 9.3.648 → 9.3.650
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 +4 -4
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +4 -4
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.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 +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-TWVRDGTX.js → chunk-23RYLGYA.js} +185 -55
- package/dist/chunk-23RYLGYA.js.map +1 -0
- package/dist/{chunk-CNRZ6WJU.js → chunk-3IJEQWQX.js} +4 -4
- package/dist/{chunk-XUGQQPGO.js → chunk-AGRPGAKR.js} +12 -1
- package/dist/chunk-AGRPGAKR.js.map +1 -0
- 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-6BNFVP7Y.js → chunk-RZOBQ23O.js} +2 -2
- package/dist/{chunk-AEIZEAP7.js → chunk-TUMH6EDV.js} +12 -15
- package/dist/chunk-TUMH6EDV.js.map +1 -0
- package/dist/{chunk-FUXV6HSO.js → chunk-TVOPSKOK.js} +3 -3
- package/dist/{chunk-5ETA6OAS.js → chunk-YAFSTKTH.js} +608 -80
- package/dist/chunk-YAFSTKTH.js.map +1 -0
- package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +7 -7
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +3 -3
- package/dist/resume-bundles.js +2 -2
- package/dist/transcript.d.ts +18 -1
- package/dist/transcript.js +5 -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/cli.ts +10 -12
- package/src/coding/coding-namespace.test.ts +44 -0
- package/src/coding/coding-namespace.ts +163 -0
- package/src/orchestrator.ts +335 -77
- package/src/transcript-day-range.test.ts +101 -0
- package/src/transcript.ts +26 -0
- package/dist/chunk-5ETA6OAS.js.map +0 -1
- package/dist/chunk-AEIZEAP7.js.map +0 -1
- package/dist/chunk-TWVRDGTX.js.map +0 -1
- package/dist/chunk-XUGQQPGO.js.map +0 -1
- /package/dist/{chunk-CNRZ6WJU.js.map → chunk-3IJEQWQX.js.map} +0 -0
- /package/dist/{chunk-6BNFVP7Y.js.map → chunk-RZOBQ23O.js.map} +0 -0
- /package/dist/{chunk-FUXV6HSO.js.map → chunk-TVOPSKOK.js.map} +0 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1495: `observe` must resolve EVERY memory-producing side effect through ONE
|
|
3
|
+
* effective scope plan, so observed turns and extracted memories land in the
|
|
4
|
+
* SAME namespace that same-session project-scoped recall searches.
|
|
5
|
+
*
|
|
6
|
+
* Before this change, `observe`:
|
|
7
|
+
* - applied the coding overlay to the objective-state snapshot target, but
|
|
8
|
+
* - keyed LCM archival (`lcmSessionKey`) and extraction replay turns off the
|
|
9
|
+
* EARLIER base namespace (`resolveWritableNamespace(undefined, …)` ==
|
|
10
|
+
* `config.defaultNamespace`), and
|
|
11
|
+
* - returned the base namespace in the response.
|
|
12
|
+
*
|
|
13
|
+
* The fix introduces an internal `MemoryScopePlan` resolver. `observe` consumes
|
|
14
|
+
* it so the LCM key, the extraction write target, the objective-state target,
|
|
15
|
+
* and the response `effectiveNamespace` all agree.
|
|
16
|
+
*
|
|
17
|
+
* Invariants verified here (rule 39 / 42 / 47 / 48 / 51):
|
|
18
|
+
* - Agreement: LCM key, extraction writeNamespaceOverride, objective-state
|
|
19
|
+
* target, and response.effectiveNamespace ALL == scope.writeNamespace, and
|
|
20
|
+
* that equals what a same-session project-scoped resolve produces.
|
|
21
|
+
* - Explicit namespace wins and is NOT silently overridden by project context.
|
|
22
|
+
* - No sessionKey ⇒ no overlay (observe requires a sessionKey, so this is the
|
|
23
|
+
* explicit-namespace / namespaces-disabled equivalents).
|
|
24
|
+
* - `codingMode.projectScope: false` ⇒ no overlay.
|
|
25
|
+
* - `namespacesEnabled: false` ⇒ single-store behavior preserved.
|
|
26
|
+
* - Unauthorized explicit namespace throws BEFORE any session-context mutation.
|
|
27
|
+
*/
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import { execFileSync } from "node:child_process";
|
|
30
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import test from "node:test";
|
|
34
|
+
|
|
35
|
+
import { EngramAccessService } from "./access-service.js";
|
|
36
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
37
|
+
import type { EngramAccessObserveRequest } from "./access-service.js";
|
|
38
|
+
import {
|
|
39
|
+
combineNamespaces,
|
|
40
|
+
lcmSessionKeyForNamespace,
|
|
41
|
+
projectNamespaceName,
|
|
42
|
+
projectTagProjectId,
|
|
43
|
+
} from "./coding/coding-namespace.js";
|
|
44
|
+
import { resolveGitContext } from "./coding/git-context.js";
|
|
45
|
+
import type { CodingContext, PluginConfig } from "./types.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Encode the expected namespaced LCM `session_id` via the SAME shared helper
|
|
49
|
+
* production uses, so these assertions stay shape-agnostic after the #1495 P1
|
|
50
|
+
* fix made the namespaced encoding sentinel-framed and unforgeable (rule 22).
|
|
51
|
+
*/
|
|
52
|
+
function encodeNs(namespace: string, sessionKey: string): string {
|
|
53
|
+
return lcmSessionKeyForNamespace(namespace, sessionKey, "default") ?? sessionKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ObserveProbe {
|
|
57
|
+
orch: Orchestrator;
|
|
58
|
+
contexts: Map<string, CodingContext>;
|
|
59
|
+
lcmCalls: Array<{ sessionKey: string }>;
|
|
60
|
+
extractionCalls: Array<{
|
|
61
|
+
sessionKeys: string[];
|
|
62
|
+
writeNamespaceOverride?: string;
|
|
63
|
+
principalOverride?: string;
|
|
64
|
+
}>;
|
|
65
|
+
objectiveStateNamespaces: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build an orchestrator stub wired to record every namespace-bearing side
|
|
70
|
+
* effect `observe` produces: LCM enqueue, extraction replay, and the storage
|
|
71
|
+
* router lookup that the objective-state snapshot writer goes through.
|
|
72
|
+
*/
|
|
73
|
+
function makeObserveProbe(overrides: Partial<PluginConfig> = {}): ObserveProbe {
|
|
74
|
+
const contexts = new Map<string, CodingContext>();
|
|
75
|
+
const lcmCalls: ObserveProbe["lcmCalls"] = [];
|
|
76
|
+
const extractionCalls: ObserveProbe["extractionCalls"] = [];
|
|
77
|
+
const objectiveStateNamespaces: string[] = [];
|
|
78
|
+
|
|
79
|
+
const config = {
|
|
80
|
+
namespacesEnabled: true,
|
|
81
|
+
defaultNamespace: "default",
|
|
82
|
+
sharedNamespace: "shared",
|
|
83
|
+
namespacePolicies: [],
|
|
84
|
+
codingMode: { projectScope: true },
|
|
85
|
+
memoryDir: "/synthetic/remnic-observe-scope",
|
|
86
|
+
objectiveStateMemoryEnabled: true,
|
|
87
|
+
objectiveStateSnapshotWritesEnabled: true,
|
|
88
|
+
principalFromSessionKeyMode: "prefix",
|
|
89
|
+
principalFromSessionKeyRules: [],
|
|
90
|
+
recallCrossNamespaceBudgetEnabled: false,
|
|
91
|
+
recallCrossNamespaceBudgetWindowMs: 60_000,
|
|
92
|
+
recallCrossNamespaceBudgetSoftLimit: 10,
|
|
93
|
+
recallCrossNamespaceBudgetHardLimit: 30,
|
|
94
|
+
...overrides,
|
|
95
|
+
} as unknown as PluginConfig;
|
|
96
|
+
|
|
97
|
+
const orch = {
|
|
98
|
+
config,
|
|
99
|
+
getCodingContextForSession: (sk: string | undefined) =>
|
|
100
|
+
(sk ? contexts.get(sk) : null) ?? null,
|
|
101
|
+
setCodingContextForSession: (sk: string, ctx: CodingContext | null) => {
|
|
102
|
+
if (ctx === null) contexts.delete(sk);
|
|
103
|
+
else contexts.set(sk, ctx);
|
|
104
|
+
},
|
|
105
|
+
applyCodingNamespaceOverlay: (sk: string | undefined, base: string) =>
|
|
106
|
+
Orchestrator.prototype.applyCodingNamespaceOverlay.call(orch, sk, base),
|
|
107
|
+
// The objective-state snapshot writer goes through getStorage(namespace).
|
|
108
|
+
// Capturing the namespace it resolves lets us assert the objective-state
|
|
109
|
+
// target without touching the filesystem.
|
|
110
|
+
getStorage: async (ns: string) => {
|
|
111
|
+
objectiveStateNamespaces.push(ns);
|
|
112
|
+
return { dir: `/synthetic/storage/${ns}` };
|
|
113
|
+
},
|
|
114
|
+
lcmEngine: {
|
|
115
|
+
enabled: true,
|
|
116
|
+
enqueueObserveMessages: (sessionKey: string) => {
|
|
117
|
+
lcmCalls.push({ sessionKey });
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
ingestReplayBatch: async (
|
|
121
|
+
turns: Array<{ sessionKey: string }>,
|
|
122
|
+
options: { writeNamespaceOverride?: string; principalOverride?: string } = {},
|
|
123
|
+
) => {
|
|
124
|
+
extractionCalls.push({
|
|
125
|
+
sessionKeys: turns.map((t) => t.sessionKey),
|
|
126
|
+
writeNamespaceOverride: options.writeNamespaceOverride,
|
|
127
|
+
principalOverride: options.principalOverride,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
} as unknown as Orchestrator;
|
|
131
|
+
|
|
132
|
+
return { orch, contexts, lcmCalls, extractionCalls, objectiveStateNamespaces };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function observeRequest(
|
|
136
|
+
overrides: Partial<EngramAccessObserveRequest>,
|
|
137
|
+
): EngramAccessObserveRequest {
|
|
138
|
+
return {
|
|
139
|
+
sessionKey: "sess-observe",
|
|
140
|
+
messages: [
|
|
141
|
+
{ role: "user", content: "what database are we using?" },
|
|
142
|
+
{ role: "assistant", content: "we use postgres for the primary store" },
|
|
143
|
+
],
|
|
144
|
+
...overrides,
|
|
145
|
+
} as EngramAccessObserveRequest;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A principal whose self namespace exists, so the overlay base is non-default. */
|
|
149
|
+
function withSelfPolicyPrefix(principal: string): Partial<PluginConfig> {
|
|
150
|
+
return {
|
|
151
|
+
namespacePolicies: [
|
|
152
|
+
{ name: principal, readPrincipals: [principal], writePrincipals: [principal] },
|
|
153
|
+
],
|
|
154
|
+
principalFromSessionKeyMode: "prefix",
|
|
155
|
+
principalFromSessionKeyRules: [{ match: `${principal}:`, principal }],
|
|
156
|
+
} as Partial<PluginConfig>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test("#1495 projectTag: LCM, extraction, objective-state, and response all agree on the effective namespace", async () => {
|
|
160
|
+
const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
|
|
161
|
+
const service = new EngramAccessService(probe.orch);
|
|
162
|
+
|
|
163
|
+
const res = await service.observe(
|
|
164
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Effective write namespace == principal self base overlaid with the project,
|
|
168
|
+
// EXACTLY what a same-session project-scoped recall/store resolves.
|
|
169
|
+
const expected = combineNamespaces(
|
|
170
|
+
"pi-geek",
|
|
171
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
assert.equal(res.effectiveNamespace, expected, "response effectiveNamespace");
|
|
175
|
+
assert.notEqual(expected, "default", "overlay must change the namespace");
|
|
176
|
+
|
|
177
|
+
// LCM archival key carries the effective namespace prefix.
|
|
178
|
+
assert.equal(probe.lcmCalls.length, 1);
|
|
179
|
+
assert.equal(
|
|
180
|
+
probe.lcmCalls[0].sessionKey,
|
|
181
|
+
encodeNs(expected, "pi-geek:abc123"),
|
|
182
|
+
"LCM key must be prefixed with the EFFECTIVE write namespace",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// #1505 thread 1 (identity-vs-routing separation): extraction replay turns
|
|
186
|
+
// carry the ORIGINAL, un-prefixed session key so provenance principal
|
|
187
|
+
// resolution and conversation threading see the real identity — NOT the
|
|
188
|
+
// namespace-prefixed key (which `resolvePrincipal` would collapse to
|
|
189
|
+
// `default`). Storage routing is pinned independently via
|
|
190
|
+
// writeNamespaceOverride, and the authenticated principal via principalOverride.
|
|
191
|
+
assert.equal(probe.extractionCalls.length, 1);
|
|
192
|
+
assert.deepEqual(
|
|
193
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
194
|
+
new Set(["pi-geek:abc123"]),
|
|
195
|
+
"extraction replay turns must carry the ORIGINAL session key (identity), not the namespace-prefixed key",
|
|
196
|
+
);
|
|
197
|
+
assert.equal(
|
|
198
|
+
probe.extractionCalls[0].writeNamespaceOverride,
|
|
199
|
+
expected,
|
|
200
|
+
"extraction must pin the write (routing) to the effective namespace",
|
|
201
|
+
);
|
|
202
|
+
assert.equal(
|
|
203
|
+
probe.extractionCalls[0].principalOverride,
|
|
204
|
+
"pi-geek",
|
|
205
|
+
"extraction must pin provenance to the resolved principal, not a default parsed from a prefixed key",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Objective-state snapshot target == effective namespace.
|
|
209
|
+
assert.ok(
|
|
210
|
+
probe.objectiveStateNamespaces.every((ns) => ns === expected),
|
|
211
|
+
`objective-state target must be the effective namespace, got ${JSON.stringify(probe.objectiveStateNamespaces)}`,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("#1495 cwd (git repo): every observe side effect agrees on the effective namespace", async () => {
|
|
216
|
+
const repoDir = mkdtempSync(join(tmpdir(), "remnic-observe-git-"));
|
|
217
|
+
// A real (synthetic) git repo so resolveGitContext can read rev-parse output.
|
|
218
|
+
// No remote/commit needed — projectId derives from the resolved root path.
|
|
219
|
+
const git = (...args: string[]) =>
|
|
220
|
+
execFileSync("git", args, { cwd: repoDir, stdio: "pipe" });
|
|
221
|
+
git("init", "-q");
|
|
222
|
+
git("config", "user.email", "test@example.com");
|
|
223
|
+
git("config", "user.name", "Test");
|
|
224
|
+
try {
|
|
225
|
+
const gitCtx = await resolveGitContext(repoDir);
|
|
226
|
+
assert.ok(gitCtx, "synthetic repo must resolve a git context");
|
|
227
|
+
|
|
228
|
+
const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
|
|
229
|
+
const service = new EngramAccessService(probe.orch);
|
|
230
|
+
|
|
231
|
+
const res = await service.observe(
|
|
232
|
+
observeRequest({ sessionKey: "pi-geek:cwd1", cwd: repoDir }),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const expected = combineNamespaces(
|
|
236
|
+
"pi-geek",
|
|
237
|
+
projectNamespaceName(gitCtx!.projectId),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
assert.equal(res.effectiveNamespace, expected);
|
|
241
|
+
assert.equal(probe.lcmCalls[0].sessionKey, encodeNs(expected, "pi-geek:cwd1"));
|
|
242
|
+
// #1505 thread 1: extraction turns carry the ORIGINAL session key (identity);
|
|
243
|
+
// routing + provenance are pinned via the override options.
|
|
244
|
+
assert.deepEqual(
|
|
245
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
246
|
+
new Set(["pi-geek:cwd1"]),
|
|
247
|
+
);
|
|
248
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expected);
|
|
249
|
+
assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
|
|
250
|
+
assert.ok(probe.objectiveStateNamespaces.every((ns) => ns === expected));
|
|
251
|
+
} finally {
|
|
252
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("#1495 explicit namespace wins and project context does NOT silently override it", async () => {
|
|
257
|
+
const probe = makeObserveProbe({
|
|
258
|
+
namespacePolicies: [
|
|
259
|
+
{ name: "team", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
|
|
260
|
+
],
|
|
261
|
+
principalFromSessionKeyMode: "prefix",
|
|
262
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
263
|
+
} as Partial<PluginConfig>);
|
|
264
|
+
const service = new EngramAccessService(probe.orch);
|
|
265
|
+
|
|
266
|
+
const res = await service.observe(
|
|
267
|
+
observeRequest({
|
|
268
|
+
sessionKey: "pi-geek:abc123",
|
|
269
|
+
namespace: "team",
|
|
270
|
+
projectTag: "Blend/Supply",
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
assert.equal(res.effectiveNamespace, "team", "explicit namespace must win");
|
|
275
|
+
assert.equal(probe.lcmCalls[0].sessionKey, encodeNs("team", "pi-geek:abc123"));
|
|
276
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, "team");
|
|
277
|
+
// #1505 thread 1: extraction turns carry the ORIGINAL session key (identity),
|
|
278
|
+
// even with an explicit namespace; routing is pinned via writeNamespaceOverride.
|
|
279
|
+
assert.deepEqual(
|
|
280
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
281
|
+
new Set(["pi-geek:abc123"]),
|
|
282
|
+
);
|
|
283
|
+
assert.equal(probe.extractionCalls[0].principalOverride, "pi-geek");
|
|
284
|
+
assert.ok(probe.objectiveStateNamespaces.every((ns) => ns === "team"));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("#1495 projectScope:false ⇒ no overlay (unqualified write stays on config.defaultNamespace)", async () => {
|
|
288
|
+
// With projectScope off there is NO coding overlay, so an unqualified observe
|
|
289
|
+
// stays on config.defaultNamespace — exactly the pre-#1434 / memory_store
|
|
290
|
+
// behavior for an unqualified write (rule 39: identical across paths). It must
|
|
291
|
+
// NOT be silently moved to the principal self namespace. lcmSessionKey carries
|
|
292
|
+
// no prefix (effective == default).
|
|
293
|
+
const probe = makeObserveProbe({
|
|
294
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
295
|
+
codingMode: { projectScope: false },
|
|
296
|
+
} as Partial<PluginConfig>);
|
|
297
|
+
const service = new EngramAccessService(probe.orch);
|
|
298
|
+
|
|
299
|
+
const res = await service.observe(
|
|
300
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
assert.equal(res.effectiveNamespace, "default");
|
|
304
|
+
assert.equal(res.scopeDebug!.codingOverlayApplied, false);
|
|
305
|
+
assert.equal(probe.lcmCalls[0].sessionKey, "pi-geek:abc123");
|
|
306
|
+
// #1505 round 3 (codex "Pin default-store extraction writes too"): with
|
|
307
|
+
// namespaces ENABLED, extraction must be pinned to the resolved writeNamespace
|
|
308
|
+
// (config.defaultNamespace here) even though it equals the default store —
|
|
309
|
+
// otherwise an unpinned runExtraction would fall back to
|
|
310
|
+
// defaultNamespaceForPrincipal("pi-geek") == "pi-geek" (the SELF namespace),
|
|
311
|
+
// diverging from where LCM/objective-state/response wrote ("default"). Pinning
|
|
312
|
+
// forces every side effect onto the one scope-plan namespace.
|
|
313
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, "default");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("#1495 namespacesEnabled:false ⇒ single-store behavior preserved", async () => {
|
|
317
|
+
const probe = makeObserveProbe({ namespacesEnabled: false } as Partial<PluginConfig>);
|
|
318
|
+
const service = new EngramAccessService(probe.orch);
|
|
319
|
+
|
|
320
|
+
const res = await service.observe(
|
|
321
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
assert.equal(res.effectiveNamespace, "default");
|
|
325
|
+
assert.equal(res.namespace, "default");
|
|
326
|
+
// No namespace prefix on the LCM key when the effective ns is the default.
|
|
327
|
+
assert.equal(probe.lcmCalls[0].sessionKey, "pi-geek:abc123");
|
|
328
|
+
// No override needed when there is only one store.
|
|
329
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, undefined);
|
|
330
|
+
assert.deepEqual(
|
|
331
|
+
new Set(probe.extractionCalls[0].sessionKeys),
|
|
332
|
+
new Set(["pi-geek:abc123"]),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("#1495 unauthorized explicit namespace throws BEFORE session context is attached", async () => {
|
|
337
|
+
const probe = makeObserveProbe();
|
|
338
|
+
const service = new EngramAccessService(probe.orch);
|
|
339
|
+
|
|
340
|
+
await assert.rejects(
|
|
341
|
+
service.observe(
|
|
342
|
+
observeRequest({
|
|
343
|
+
sessionKey: "pi-geek:abc123",
|
|
344
|
+
namespace: "victim-secret",
|
|
345
|
+
projectTag: "Blend/Supply",
|
|
346
|
+
}),
|
|
347
|
+
),
|
|
348
|
+
/not writable/,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// No orphaned coding context, no side effects after the auth failure.
|
|
352
|
+
assert.equal(probe.contexts.get("pi-geek:abc123"), undefined);
|
|
353
|
+
assert.equal(probe.lcmCalls.length, 0);
|
|
354
|
+
assert.equal(probe.extractionCalls.length, 0);
|
|
355
|
+
assert.equal(probe.objectiveStateNamespaces.length, 0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("#1505 thread 1/3: unauthorized OVERLAY self-base throws BEFORE coding context is attached (no orphan context)", async () => {
|
|
359
|
+
// Threads 1 & 3 (cursor / codex): a project-scoped observe with NO explicit
|
|
360
|
+
// namespace. Step 1 (resolveWritableNamespace(undefined)) authorizes the
|
|
361
|
+
// DEFAULT namespace and PASSES. The overlay self-base auth only runs inside
|
|
362
|
+
// the scope plan. If the principal has a self namespace policy that EXISTS but
|
|
363
|
+
// is NOT writable, the scope plan throws — and before this fix that happened
|
|
364
|
+
// AFTER maybeAttachCodingContext mutated the session, leaving a project
|
|
365
|
+
// binding from a rejected op. The invariant: an observe that throws leaves NO
|
|
366
|
+
// coding context on the session, matching memory_store's resolve-before-mutate
|
|
367
|
+
// ordering.
|
|
368
|
+
const probe = makeObserveProbe({
|
|
369
|
+
namespacePolicies: [
|
|
370
|
+
// Self namespace exists (so defaultNamespaceForPrincipal → "pi-geek")
|
|
371
|
+
// but pi-geek may NOT write it — only some other principal can.
|
|
372
|
+
{ name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["other"] },
|
|
373
|
+
],
|
|
374
|
+
principalFromSessionKeyMode: "prefix",
|
|
375
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
376
|
+
} as Partial<PluginConfig>);
|
|
377
|
+
const service = new EngramAccessService(probe.orch);
|
|
378
|
+
|
|
379
|
+
await assert.rejects(
|
|
380
|
+
service.observe(
|
|
381
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
382
|
+
),
|
|
383
|
+
/not writable/,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// No orphaned coding context, no side effects after the overlay auth failure.
|
|
387
|
+
assert.equal(
|
|
388
|
+
probe.contexts.get("pi-geek:abc123"),
|
|
389
|
+
undefined,
|
|
390
|
+
"a rejected observe must NOT leave a project binding on the session",
|
|
391
|
+
);
|
|
392
|
+
assert.equal(probe.lcmCalls.length, 0);
|
|
393
|
+
assert.equal(probe.extractionCalls.length, 0);
|
|
394
|
+
assert.equal(probe.objectiveStateNamespaces.length, 0);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("#1505 thread jvO: restrictive default-namespace write policy does NOT reject a valid project-scoped observe via the legacy-response path (no orphan binding)", async () => {
|
|
398
|
+
// The legacy `namespace` response field was previously a SECOND
|
|
399
|
+
// `resolveWritableNamespace(request.namespace, …)` call. For an implicit
|
|
400
|
+
// (no explicit namespace) project-scoped observe that re-authorized
|
|
401
|
+
// `undefined ⇒ config.defaultNamespace`. Under a deployment that restricts
|
|
402
|
+
// WRITE to the default namespace while still allowing the principal to write
|
|
403
|
+
// its own self/project namespace, that second auth REJECTED an observe whose
|
|
404
|
+
// effective self/project write target the scope plan had ALREADY authorized
|
|
405
|
+
// (the same target memory_store/suggestion_submit accept). Worse, the scope
|
|
406
|
+
// plan had already SEEDED the coding context to compute the overlay, so the
|
|
407
|
+
// post-plan rejection left an orphaned project binding on the session.
|
|
408
|
+
//
|
|
409
|
+
// After the fix the legacy field is DERIVED from the resolved scope plan, so
|
|
410
|
+
// there is no second authorization: the observe succeeds, and the legacy
|
|
411
|
+
// `namespace` stays byte-for-byte `config.defaultNamespace` (overlay-agnostic
|
|
412
|
+
// pre-#1495 semantics) while every side effect uses the overlay write target.
|
|
413
|
+
const probe = makeObserveProbe({
|
|
414
|
+
namespacePolicies: [
|
|
415
|
+
// Restrictive DEFAULT namespace: only `admin` may write it, NOT pi-geek.
|
|
416
|
+
{ name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
|
|
417
|
+
// pi-geek may write its own self (and thus its `pi-geek-project-*`) base.
|
|
418
|
+
{ name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
|
|
419
|
+
],
|
|
420
|
+
principalFromSessionKeyMode: "prefix",
|
|
421
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
422
|
+
} as Partial<PluginConfig>);
|
|
423
|
+
const service = new EngramAccessService(probe.orch);
|
|
424
|
+
|
|
425
|
+
const expectedOverlay = combineNamespaces(
|
|
426
|
+
"pi-geek",
|
|
427
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// FAIL-BEFORE: this threw `namespace is not writable: default`. PASS-AFTER:
|
|
431
|
+
// the observe is accepted exactly like memory_store/suggestion_submit would.
|
|
432
|
+
const res = await service.observe(
|
|
433
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Every side effect uses the authorized overlay write target.
|
|
437
|
+
assert.equal(res.effectiveNamespace, expectedOverlay);
|
|
438
|
+
assert.equal(res.scopeDebug!.codingOverlayApplied, true);
|
|
439
|
+
assert.equal(probe.extractionCalls[0].writeNamespaceOverride, expectedOverlay);
|
|
440
|
+
// The legacy `namespace` field stays byte-for-byte pre-#1495: overlay-agnostic,
|
|
441
|
+
// so config.defaultNamespace for an unqualified write — NOT a re-auth result.
|
|
442
|
+
assert.equal(res.namespace, "default");
|
|
443
|
+
// The seeded coding context IS retained on success (the happy path re-binds
|
|
444
|
+
// the identical context after auth passes) — that is correct, not an orphan.
|
|
445
|
+
assert.ok(
|
|
446
|
+
probe.contexts.get("pi-geek:abc123"),
|
|
447
|
+
"a SUCCESSFUL scoped observe binds the project context for later recall",
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("#1505 thread jvO: a genuine reject (unwritable self base) under a restrictive default policy still leaves NO orphan binding", async () => {
|
|
452
|
+
// Companion to the jvO fix: when the observe SHOULD reject (the principal
|
|
453
|
+
// cannot write its own self base), the rejection must still come from the
|
|
454
|
+
// scope plan (resolve-before-mutate) and leave NO session binding — never
|
|
455
|
+
// from a post-plan legacy-response re-auth that fires after seeding.
|
|
456
|
+
const probe = makeObserveProbe({
|
|
457
|
+
namespacePolicies: [
|
|
458
|
+
{ name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
|
|
459
|
+
// pi-geek's self base EXISTS but is NOT writable by pi-geek.
|
|
460
|
+
{ name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["other"] },
|
|
461
|
+
],
|
|
462
|
+
principalFromSessionKeyMode: "prefix",
|
|
463
|
+
principalFromSessionKeyRules: [{ match: "pi-geek:", principal: "pi-geek" }],
|
|
464
|
+
} as Partial<PluginConfig>);
|
|
465
|
+
const service = new EngramAccessService(probe.orch);
|
|
466
|
+
|
|
467
|
+
await assert.rejects(
|
|
468
|
+
service.observe(
|
|
469
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
470
|
+
),
|
|
471
|
+
/not writable/,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
assert.equal(
|
|
475
|
+
probe.contexts.get("pi-geek:abc123"),
|
|
476
|
+
undefined,
|
|
477
|
+
"a rejected observe must NOT leave a project binding (resolve-before-mutate)",
|
|
478
|
+
);
|
|
479
|
+
assert.equal(probe.lcmCalls.length, 0);
|
|
480
|
+
assert.equal(probe.extractionCalls.length, 0);
|
|
481
|
+
assert.equal(probe.objectiveStateNamespaces.length, 0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("#1495 scopeDebug exposes the resolved plan for callers/tests", async () => {
|
|
485
|
+
const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
|
|
486
|
+
const service = new EngramAccessService(probe.orch);
|
|
487
|
+
|
|
488
|
+
const res = await service.observe(
|
|
489
|
+
observeRequest({ sessionKey: "pi-geek:abc123", projectTag: "Blend/Supply" }),
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const expected = combineNamespaces(
|
|
493
|
+
"pi-geek",
|
|
494
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
495
|
+
);
|
|
496
|
+
assert.ok(res.scopeDebug, "scopeDebug must be present");
|
|
497
|
+
assert.equal(res.scopeDebug!.principal, "pi-geek");
|
|
498
|
+
assert.equal(res.scopeDebug!.baseNamespace, "pi-geek");
|
|
499
|
+
assert.equal(res.scopeDebug!.writeNamespace, expected);
|
|
500
|
+
assert.equal(res.scopeDebug!.codingOverlayApplied, true);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("#1505 (cursor hAp) scopeDebug.baseNamespace reports the principal self base on the implicit no-overlay path", async () => {
|
|
504
|
+
// Regression for the round-4 cursor "Wrong scopeDebug base namespace" thread.
|
|
505
|
+
// Implicit (no explicit namespace) + projectScope OFF ⇒ the no-overlay branch
|
|
506
|
+
// of resolveMemoryScopePlan runs: the general write namespace collapses to
|
|
507
|
+
// config.defaultNamespace ("default") for memory_store parity (rule 39), but
|
|
508
|
+
// the plan's diagnostic baseNamespace must report the principal SELF base
|
|
509
|
+
// ("pi-geek" via defaultNamespaceForPrincipal) — the same base
|
|
510
|
+
// objectiveStateNamespace already targets — NOT the write namespace. Before the
|
|
511
|
+
// fix, scopeDebug.baseNamespace misstated the self base as "default".
|
|
512
|
+
const probe = makeObserveProbe({
|
|
513
|
+
...withSelfPolicyPrefix("pi-geek"),
|
|
514
|
+
codingMode: { projectScope: false },
|
|
515
|
+
} as Partial<PluginConfig>);
|
|
516
|
+
const service = new EngramAccessService(probe.orch);
|
|
517
|
+
|
|
518
|
+
const res = await service.observe(
|
|
519
|
+
observeRequest({ sessionKey: "pi-geek:abc123" }),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
assert.ok(res.scopeDebug, "scopeDebug must be present");
|
|
523
|
+
assert.equal(
|
|
524
|
+
res.scopeDebug!.codingOverlayApplied,
|
|
525
|
+
false,
|
|
526
|
+
"no overlay on this path",
|
|
527
|
+
);
|
|
528
|
+
// Write/effective namespace collapses to the default store (memory_store parity)…
|
|
529
|
+
assert.equal(res.scopeDebug!.writeNamespace, "default");
|
|
530
|
+
assert.equal(res.effectiveNamespace, "default");
|
|
531
|
+
// …but the diagnostic base must be the principal SELF base, not the write ns.
|
|
532
|
+
assert.equal(
|
|
533
|
+
res.scopeDebug!.baseNamespace,
|
|
534
|
+
"pi-geek",
|
|
535
|
+
"scopeDebug.baseNamespace must be the principal self base on the implicit no-overlay path",
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("#1495 the scope plan's writeNamespace matches resolveCodingScopedWriteNamespace (memory_store / suggestion_submit parity, rule 39)", async () => {
|
|
540
|
+
// Regression guard: observe's effective scope MUST be identical to what the
|
|
541
|
+
// explicit-write tools (memory_store / suggestion_submit) resolve via
|
|
542
|
+
// resolveCodingScopedWriteNamespace. If these ever diverge, observed turns and
|
|
543
|
+
// explicit writes on the same session/project would land in different stores.
|
|
544
|
+
const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
|
|
545
|
+
// Bind a session coding context so both resolvers see the same project.
|
|
546
|
+
probe.contexts.set("pi-geek:abc123", {
|
|
547
|
+
projectId: projectTagProjectId("Blend/Supply"),
|
|
548
|
+
branch: null,
|
|
549
|
+
rootPath: projectTagProjectId("Blend/Supply"),
|
|
550
|
+
defaultBranch: null,
|
|
551
|
+
});
|
|
552
|
+
const service = new EngramAccessService(probe.orch);
|
|
553
|
+
|
|
554
|
+
const internals = service as unknown as {
|
|
555
|
+
resolveMemoryScopePlan: (r: unknown) => Promise<{ writeNamespace: string }>;
|
|
556
|
+
resolveCodingScopedWriteNamespace: (r: unknown) => Promise<string>;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
for (const req of [
|
|
560
|
+
{ sessionKey: "pi-geek:abc123", authenticatedPrincipal: "pi-geek" },
|
|
561
|
+
{
|
|
562
|
+
sessionKey: "pi-geek:abc123",
|
|
563
|
+
authenticatedPrincipal: "pi-geek",
|
|
564
|
+
namespace: "pi-geek",
|
|
565
|
+
},
|
|
566
|
+
]) {
|
|
567
|
+
const plan = await internals.resolveMemoryScopePlan.call(service, req);
|
|
568
|
+
const explicit = await internals.resolveCodingScopedWriteNamespace.call(
|
|
569
|
+
service,
|
|
570
|
+
req,
|
|
571
|
+
);
|
|
572
|
+
assert.equal(
|
|
573
|
+
plan.writeNamespace,
|
|
574
|
+
explicit,
|
|
575
|
+
`observe and explicit-write resolvers must agree for ${JSON.stringify(req)}`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("#1495 skipExtraction does not enqueue extraction but still archives LCM under the effective namespace", async () => {
|
|
581
|
+
const probe = makeObserveProbe(withSelfPolicyPrefix("pi-geek"));
|
|
582
|
+
const service = new EngramAccessService(probe.orch);
|
|
583
|
+
|
|
584
|
+
const res = await service.observe(
|
|
585
|
+
observeRequest({
|
|
586
|
+
sessionKey: "pi-geek:abc123",
|
|
587
|
+
projectTag: "Blend/Supply",
|
|
588
|
+
skipExtraction: true,
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const expected = combineNamespaces(
|
|
593
|
+
"pi-geek",
|
|
594
|
+
projectNamespaceName(projectTagProjectId("Blend/Supply")),
|
|
595
|
+
);
|
|
596
|
+
assert.equal(res.extractionQueued, false);
|
|
597
|
+
assert.equal(probe.extractionCalls.length, 0);
|
|
598
|
+
assert.equal(probe.lcmCalls[0].sessionKey, encodeNs(expected, "pi-geek:abc123"));
|
|
599
|
+
});
|