@remnic/core 9.3.614 → 9.3.616
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 +3 -3
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +5 -5
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +4 -4
- package/dist/access-schema.d.ts +48 -36
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-DGG_2xPK.d.ts → access-service-CBNEKjzN.d.ts} +70 -5
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +2 -2
- package/dist/{chunk-B6FDZPCF.js → chunk-5OHHEORR.js} +50 -15
- package/dist/chunk-5OHHEORR.js.map +1 -0
- package/dist/{chunk-T5XWMMU2.js → chunk-EXUAP5LH.js} +2 -2
- package/dist/{chunk-EUML3N6B.js → chunk-IMA6GU4Y.js} +3 -3
- package/dist/chunk-IMA6GU4Y.js.map +1 -0
- package/dist/{chunk-7YQFWOF7.js → chunk-KGLPJROV.js} +4 -4
- package/dist/{chunk-VPGUMLBA.js → chunk-NM5NQYJE.js} +16 -16
- package/dist/chunk-NM5NQYJE.js.map +1 -0
- package/dist/{chunk-QEMCQFDW.js → chunk-WD2W4234.js} +8 -2
- package/dist/chunk-WD2W4234.js.map +1 -0
- package/dist/{chunk-ADNZVFXG.js → chunk-ZK32E74R.js} +142 -31
- package/dist/chunk-ZK32E74R.js.map +1 -0
- package/dist/{cli-DWeu7eTY.d.ts → cli-Cw729yLf.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +6 -6
- package/dist/explicit-capture.d.ts +10 -0
- package/dist/explicit-capture.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -7
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/orchestrator.js +2 -2
- package/dist/schemas.d.ts +64 -64
- package/dist/shared-context/manager.d.ts +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/access-http.ts +21 -10
- package/src/access-mcp.test.ts +109 -0
- package/src/access-mcp.ts +46 -2
- package/src/access-schema.ts +11 -0
- package/src/access-service-coding-write.test.ts +478 -0
- package/src/access-service.ts +237 -32
- package/src/explicit-capture.ts +19 -2
- package/dist/chunk-ADNZVFXG.js.map +0 -1
- package/dist/chunk-B6FDZPCF.js.map +0 -1
- package/dist/chunk-EUML3N6B.js.map +0 -1
- package/dist/chunk-QEMCQFDW.js.map +0 -1
- package/dist/chunk-VPGUMLBA.js.map +0 -1
- /package/dist/{chunk-T5XWMMU2.js.map → chunk-EXUAP5LH.js.map} +0 -0
- /package/dist/{chunk-7YQFWOF7.js.map → chunk-KGLPJROV.js.map} +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1434: explicit-write tools (memory_store / suggestion_submit) must resolve
|
|
3
|
+
* their write namespace through the SAME project-scope overlay the read path
|
|
4
|
+
* uses, so a memory stored with a client-injected `cwd`/`projectTag` is
|
|
5
|
+
* discoverable by project-scoped recall (rule 42 symmetry). Previously these
|
|
6
|
+
* tools ignored coding context and always wrote to the base namespace.
|
|
7
|
+
*
|
|
8
|
+
* Invariants verified here (review hardening on PR #1444):
|
|
9
|
+
* - Symmetry: a `projectTag`/`cwd` (or an existing session context) overlays
|
|
10
|
+
* the project namespace onto the principal self base — the SAME namespace
|
|
11
|
+
* recall/observe/buffer use — so scoped stores are found by scoped recall.
|
|
12
|
+
* - Base: the principal self namespace (defaultNamespaceForPrincipal), which
|
|
13
|
+
* collapses to `config.defaultNamespace` when namespaces are disabled or the
|
|
14
|
+
* principal has no self policy (the common deployment is unchanged).
|
|
15
|
+
* - Read-only: the resolver NEVER mutates session coding context, so
|
|
16
|
+
* idempotency peeks / dryRun preflights are side-effect free (Codex review).
|
|
17
|
+
* - Persist: a pre-resolved project namespace reaches storage instead of being
|
|
18
|
+
* rejected by the static policy allow-list (Codex P1 / Cursor High).
|
|
19
|
+
* - Precedence: explicit `namespace` wins; namespaces-disabled is a no-op.
|
|
20
|
+
*/
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
|
|
24
|
+
import { EngramAccessService } from "./access-service.js";
|
|
25
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
26
|
+
import { persistExplicitCapture } from "./explicit-capture.js";
|
|
27
|
+
import type { ValidExplicitCapture } from "./explicit-capture.js";
|
|
28
|
+
import {
|
|
29
|
+
combineNamespaces,
|
|
30
|
+
projectNamespaceName,
|
|
31
|
+
projectTagProjectId,
|
|
32
|
+
} from "./coding/coding-namespace.js";
|
|
33
|
+
import type { CodingContext, PluginConfig } from "./types.js";
|
|
34
|
+
|
|
35
|
+
function makeOrchestratorStub(overrides: Partial<PluginConfig> = {}): Orchestrator {
|
|
36
|
+
const orch = Object.create(Orchestrator.prototype) as Orchestrator;
|
|
37
|
+
const internals = orch as unknown as {
|
|
38
|
+
config: PluginConfig;
|
|
39
|
+
_codingContextBySession: Map<string, CodingContext>;
|
|
40
|
+
};
|
|
41
|
+
internals.config = {
|
|
42
|
+
namespacesEnabled: true,
|
|
43
|
+
defaultNamespace: "default",
|
|
44
|
+
sharedNamespace: "shared",
|
|
45
|
+
namespacePolicies: [],
|
|
46
|
+
codingMode: { projectScope: true },
|
|
47
|
+
memoryDir: "/synthetic/remnic-coding-write",
|
|
48
|
+
recallCrossNamespaceBudgetEnabled: false,
|
|
49
|
+
recallCrossNamespaceBudgetWindowMs: 60_000,
|
|
50
|
+
recallCrossNamespaceBudgetSoftLimit: 10,
|
|
51
|
+
recallCrossNamespaceBudgetHardLimit: 30,
|
|
52
|
+
...overrides,
|
|
53
|
+
} as unknown as PluginConfig;
|
|
54
|
+
internals._codingContextBySession = new Map();
|
|
55
|
+
return orch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolver(service: EngramAccessService) {
|
|
59
|
+
return (req: unknown) =>
|
|
60
|
+
(
|
|
61
|
+
service as unknown as {
|
|
62
|
+
resolveCodingScopedWriteNamespace: (r: unknown) => Promise<string>;
|
|
63
|
+
}
|
|
64
|
+
).resolveCodingScopedWriteNamespace(req);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function projectNamespaceFor(tag: string): string {
|
|
68
|
+
// projectScope (no branch scope) overlay namespace == projectNamespaceName.
|
|
69
|
+
return combineNamespaces("default", projectNamespaceName(projectTagProjectId(tag)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("#1434 projectTag scopes the write to the project namespace, read-only", async () => {
|
|
73
|
+
const orch = makeOrchestratorStub();
|
|
74
|
+
const service = new EngramAccessService(orch);
|
|
75
|
+
|
|
76
|
+
const resolved = await resolver(service)({
|
|
77
|
+
sessionKey: "sess-1",
|
|
78
|
+
authenticatedPrincipal: "alice",
|
|
79
|
+
projectTag: "Blend/Supply",
|
|
80
|
+
content: "x",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.equal(resolved, projectNamespaceFor("Blend/Supply"));
|
|
84
|
+
assert.notEqual(resolved, "default", "project context must change the namespace");
|
|
85
|
+
// Read-only: resolving must NOT persist coding context on the session.
|
|
86
|
+
assert.equal(
|
|
87
|
+
orch.getCodingContextForSession("sess-1"),
|
|
88
|
+
null,
|
|
89
|
+
"resolver must not mutate session coding context (peek/dryRun safety)",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("#1434 a sessionless write with projectTag stays on the base namespace (recall symmetry)", async () => {
|
|
94
|
+
// Without a sessionKey the recall path can't attach or look up coding context
|
|
95
|
+
// (maybeAttachCodingContext / applyCodingNamespaceOverlay both no-op), so a
|
|
96
|
+
// sessionless recall searches the base namespace. A sessionless write must
|
|
97
|
+
// therefore also stay on the base — else the store would be hidden from the
|
|
98
|
+
// same client's recall (Codex review).
|
|
99
|
+
const orch = makeOrchestratorStub();
|
|
100
|
+
const service = new EngramAccessService(orch);
|
|
101
|
+
const resolved = await resolver(service)({
|
|
102
|
+
authenticatedPrincipal: "alice",
|
|
103
|
+
projectTag: "Blend/Supply",
|
|
104
|
+
content: "x",
|
|
105
|
+
});
|
|
106
|
+
assert.equal(resolved, "default");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("#1434 an existing session coding context scopes the write (recall-then-store flow)", async () => {
|
|
110
|
+
const orch = makeOrchestratorStub();
|
|
111
|
+
orch.setCodingContextForSession("sess-ctx", {
|
|
112
|
+
projectId: projectTagProjectId("Blend/Supply"),
|
|
113
|
+
branch: null,
|
|
114
|
+
rootPath: projectTagProjectId("Blend/Supply"),
|
|
115
|
+
defaultBranch: null,
|
|
116
|
+
});
|
|
117
|
+
const service = new EngramAccessService(orch);
|
|
118
|
+
|
|
119
|
+
const resolved = await resolver(service)({
|
|
120
|
+
sessionKey: "sess-ctx",
|
|
121
|
+
authenticatedPrincipal: "alice",
|
|
122
|
+
content: "x",
|
|
123
|
+
});
|
|
124
|
+
assert.equal(resolved, projectNamespaceFor("Blend/Supply"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("#1434 an existing session binding wins over per-call projectTag (recall symmetry)", async () => {
|
|
128
|
+
// Session is bound to project A; this write also passes per-call projectTag B.
|
|
129
|
+
// The write MUST resolve to A — the same project the session's recall searches
|
|
130
|
+
// (recall is session-first: maybeAttachCodingContext returns early when a
|
|
131
|
+
// context is already attached). A per-call-wins write would land in B, which
|
|
132
|
+
// that session's recall never searches, so the memory would be undiscoverable.
|
|
133
|
+
const orch = makeOrchestratorStub();
|
|
134
|
+
orch.setCodingContextForSession("sess-reuse", {
|
|
135
|
+
projectId: projectTagProjectId("Project/A"),
|
|
136
|
+
branch: null,
|
|
137
|
+
rootPath: projectTagProjectId("Project/A"),
|
|
138
|
+
defaultBranch: null,
|
|
139
|
+
});
|
|
140
|
+
const service = new EngramAccessService(orch);
|
|
141
|
+
const resolved = await resolver(service)({
|
|
142
|
+
sessionKey: "sess-reuse",
|
|
143
|
+
authenticatedPrincipal: "alice",
|
|
144
|
+
projectTag: "Project/B",
|
|
145
|
+
content: "x",
|
|
146
|
+
});
|
|
147
|
+
assert.equal(resolved, projectNamespaceFor("Project/A"));
|
|
148
|
+
assert.notEqual(resolved, projectNamespaceFor("Project/B"));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("#1434 explicit namespace wins and bypasses coding overlay", async () => {
|
|
152
|
+
const orch = makeOrchestratorStub();
|
|
153
|
+
const service = new EngramAccessService(orch);
|
|
154
|
+
const resolved = await resolver(service)({
|
|
155
|
+
sessionKey: "sess-2",
|
|
156
|
+
authenticatedPrincipal: "alice",
|
|
157
|
+
namespace: "default",
|
|
158
|
+
projectTag: "Blend/Supply",
|
|
159
|
+
content: "x",
|
|
160
|
+
});
|
|
161
|
+
assert.equal(resolved, "default");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("#1434 unqualified write (self policy) stays on config.defaultNamespace", async () => {
|
|
165
|
+
// Even when principal "alice" has a self policy, an UNQUALIFIED write (no
|
|
166
|
+
// coding overlay) stays on config.defaultNamespace — exactly the pre-#1434
|
|
167
|
+
// behavior. #1434 only re-scopes project-identified writes; it must not
|
|
168
|
+
// silently move plain unqualified writes to a principal self namespace (Codex
|
|
169
|
+
// review). The symmetry fix applies to the coding-overlay path only.
|
|
170
|
+
const orch = makeOrchestratorStub({
|
|
171
|
+
namespacePolicies: [
|
|
172
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
173
|
+
],
|
|
174
|
+
} as Partial<PluginConfig>);
|
|
175
|
+
const service = new EngramAccessService(orch);
|
|
176
|
+
const resolved = await resolver(service)({
|
|
177
|
+
sessionKey: "sess-3",
|
|
178
|
+
authenticatedPrincipal: "alice",
|
|
179
|
+
content: "x",
|
|
180
|
+
});
|
|
181
|
+
assert.equal(resolved, "default");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("#1434 unqualified write with no principal policy stays on the default namespace", async () => {
|
|
185
|
+
// No policy named after the principal => base is defaultNamespace, so behavior
|
|
186
|
+
// is unchanged for the common deployment.
|
|
187
|
+
const orch = makeOrchestratorStub();
|
|
188
|
+
const service = new EngramAccessService(orch);
|
|
189
|
+
const resolved = await resolver(service)({
|
|
190
|
+
sessionKey: "sess-3b",
|
|
191
|
+
authenticatedPrincipal: "alice",
|
|
192
|
+
content: "x",
|
|
193
|
+
});
|
|
194
|
+
assert.equal(resolved, "default");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("#1434 project write overlays onto the principal self base (recall symmetry)", async () => {
|
|
198
|
+
// With a self policy, a project-scoped write overlays onto the principal self
|
|
199
|
+
// base (defaultNamespaceForPrincipal) — the SAME base recall/observe/buffer
|
|
200
|
+
// overlay onto — so the store is discoverable by that principal's
|
|
201
|
+
// project-scoped recall (Cursor review / rule 42).
|
|
202
|
+
const orch = makeOrchestratorStub({
|
|
203
|
+
namespacePolicies: [
|
|
204
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
205
|
+
],
|
|
206
|
+
} as Partial<PluginConfig>);
|
|
207
|
+
const service = new EngramAccessService(orch);
|
|
208
|
+
const resolved = await resolver(service)({
|
|
209
|
+
sessionKey: "sess-3c",
|
|
210
|
+
authenticatedPrincipal: "alice",
|
|
211
|
+
projectTag: "Blend/Supply",
|
|
212
|
+
content: "x",
|
|
213
|
+
});
|
|
214
|
+
assert.equal(
|
|
215
|
+
resolved,
|
|
216
|
+
combineNamespaces("alice", projectNamespaceName(projectTagProjectId("Blend/Supply"))),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("#1434 an explicit coding-overlay namespace string is NOT a writable target", async () => {
|
|
221
|
+
// Project scoping is requested via cwd/projectTag, never by naming the derived
|
|
222
|
+
// overlay namespace. A caller naming an overlay-shaped namespace directly is
|
|
223
|
+
// authorized strictly through canWriteNamespace and rejected, so the persist
|
|
224
|
+
// allow-list can never be bypassed by guessing an overlay name.
|
|
225
|
+
const orch = makeOrchestratorStub();
|
|
226
|
+
const service = new EngramAccessService(orch);
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
resolver(service)({
|
|
229
|
+
sessionKey: "sess-explicit-overlay",
|
|
230
|
+
authenticatedPrincipal: "alice",
|
|
231
|
+
namespace: projectNamespaceFor("Blend/Supply"), // "default-project-…"
|
|
232
|
+
content: "x",
|
|
233
|
+
}),
|
|
234
|
+
/not writable/,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("#1434 a prefix-colliding principal namespace cannot be written cross-tenant (Codex P1)", async () => {
|
|
239
|
+
// Policies for both `alice` and `alice-project-team`. An authenticated `alice`
|
|
240
|
+
// must NOT be able to write `alice-project-team-project-foo` (the OTHER
|
|
241
|
+
// principal's project-scoped namespace) by exploiting a shared `alice-project-`
|
|
242
|
+
// prefix. Strict canWriteNamespace authorization rejects it.
|
|
243
|
+
const orch = makeOrchestratorStub({
|
|
244
|
+
namespacePolicies: [
|
|
245
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
246
|
+
{
|
|
247
|
+
name: "alice-project-team",
|
|
248
|
+
readPrincipals: ["teamuser"],
|
|
249
|
+
writePrincipals: ["teamuser"],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
} as Partial<PluginConfig>);
|
|
253
|
+
const service = new EngramAccessService(orch);
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
resolver(service)({
|
|
256
|
+
sessionKey: "sess-collide",
|
|
257
|
+
authenticatedPrincipal: "alice",
|
|
258
|
+
namespace: "alice-project-team-project-foo",
|
|
259
|
+
content: "x",
|
|
260
|
+
}),
|
|
261
|
+
/not writable/,
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("#1434 a derived overlay base the principal cannot write is rejected (Codex P1)", async () => {
|
|
266
|
+
// The principal has a self policy but NO write access to the configured
|
|
267
|
+
// default namespace. An explicit `default-project-foo` must be rejected —
|
|
268
|
+
// overlay namespaces are never accepted as caller strings, and the base must
|
|
269
|
+
// pass canWriteNamespace.
|
|
270
|
+
const orch = makeOrchestratorStub({
|
|
271
|
+
defaultNamespace: "default",
|
|
272
|
+
namespacePolicies: [
|
|
273
|
+
{ name: "alice", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
274
|
+
{ name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
|
|
275
|
+
],
|
|
276
|
+
} as Partial<PluginConfig>);
|
|
277
|
+
const service = new EngramAccessService(orch);
|
|
278
|
+
await assert.rejects(
|
|
279
|
+
resolver(service)({
|
|
280
|
+
sessionKey: "sess-base-noauth",
|
|
281
|
+
authenticatedPrincipal: "alice",
|
|
282
|
+
namespace: "default-project-foo",
|
|
283
|
+
content: "x",
|
|
284
|
+
}),
|
|
285
|
+
/not writable/,
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("#1434 a forged cross-principal namespace cannot widen access", async () => {
|
|
290
|
+
// A caller naming a namespace that is not writable for this principal is
|
|
291
|
+
// rejected by canWriteNamespace — it can't escalate to another principal's
|
|
292
|
+
// namespace.
|
|
293
|
+
const orch = makeOrchestratorStub();
|
|
294
|
+
const service = new EngramAccessService(orch);
|
|
295
|
+
await assert.rejects(
|
|
296
|
+
resolver(service)({
|
|
297
|
+
sessionKey: "sess-forge",
|
|
298
|
+
authenticatedPrincipal: "alice",
|
|
299
|
+
namespace: "victim-secret",
|
|
300
|
+
content: "x",
|
|
301
|
+
}),
|
|
302
|
+
/not writable/,
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("#1434 namespaces disabled: cwd/projectTag are a no-op (common single-tenant MCP case)", async () => {
|
|
307
|
+
const orch = makeOrchestratorStub({ namespacesEnabled: false } as Partial<PluginConfig>);
|
|
308
|
+
const service = new EngramAccessService(orch);
|
|
309
|
+
const resolved = await resolver(service)({
|
|
310
|
+
sessionKey: "sess-4",
|
|
311
|
+
projectTag: "Blend/Supply",
|
|
312
|
+
content: "x",
|
|
313
|
+
});
|
|
314
|
+
assert.equal(resolved, "default");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
function makeAttachOrchestrator() {
|
|
318
|
+
const contexts = new Map<string, CodingContext>();
|
|
319
|
+
const getStorageCalls: Array<string | undefined> = [];
|
|
320
|
+
const orch = {
|
|
321
|
+
config: {
|
|
322
|
+
namespacesEnabled: true,
|
|
323
|
+
defaultNamespace: "default",
|
|
324
|
+
sharedNamespace: "shared",
|
|
325
|
+
namespacePolicies: [],
|
|
326
|
+
codingMode: { projectScope: true },
|
|
327
|
+
memoryDir: "/synthetic/remnic-coding-write-attach",
|
|
328
|
+
recallCrossNamespaceBudgetEnabled: false,
|
|
329
|
+
recallCrossNamespaceBudgetWindowMs: 60_000,
|
|
330
|
+
recallCrossNamespaceBudgetSoftLimit: 10,
|
|
331
|
+
recallCrossNamespaceBudgetHardLimit: 30,
|
|
332
|
+
},
|
|
333
|
+
getCodingContextForSession: (sk: string) => contexts.get(sk) ?? null,
|
|
334
|
+
setCodingContextForSession: (sk: string, ctx: CodingContext) => {
|
|
335
|
+
contexts.set(sk, ctx);
|
|
336
|
+
},
|
|
337
|
+
getStorage: async (ns?: string) => {
|
|
338
|
+
getStorageCalls.push(ns);
|
|
339
|
+
return {
|
|
340
|
+
readAllMemories: async () => [],
|
|
341
|
+
writeMemory: async () => "mem-1",
|
|
342
|
+
appendMemoryLifecycleEvents: async () => {},
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
} as unknown as Orchestrator;
|
|
346
|
+
return { orch, contexts, getStorageCalls };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function storeRequest(
|
|
350
|
+
overrides: Record<string, unknown>,
|
|
351
|
+
): Parameters<EngramAccessService["memoryStore"]>[0] {
|
|
352
|
+
return {
|
|
353
|
+
authenticatedPrincipal: "alice",
|
|
354
|
+
content: "durable project memory",
|
|
355
|
+
category: "fact",
|
|
356
|
+
confidence: 0.9,
|
|
357
|
+
tags: [],
|
|
358
|
+
...overrides,
|
|
359
|
+
} as unknown as Parameters<EngramAccessService["memoryStore"]>[0];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
test("#1434 a real memory_store attaches coding context so a later bare recall on the session is scoped (Cursor review)", async () => {
|
|
363
|
+
// A store with sessionKey + per-call projectTag must seed the session's
|
|
364
|
+
// coding binding (like recall's maybeAttachCodingContext), so a SUBSEQUENT
|
|
365
|
+
// bare recall on the same session — one that omits cwd/projectTag — is scoped
|
|
366
|
+
// to the same project and finds the memory.
|
|
367
|
+
const { orch, contexts, getStorageCalls } = makeAttachOrchestrator();
|
|
368
|
+
const service = new EngramAccessService(orch);
|
|
369
|
+
|
|
370
|
+
const res = await service.memoryStore(
|
|
371
|
+
storeRequest({ sessionKey: "sess-attach", projectTag: "Blend/Supply" }),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
assert.equal(res.status, "stored");
|
|
375
|
+
assert.equal(res.namespace, projectNamespaceFor("Blend/Supply"));
|
|
376
|
+
// The store attached the coding context the recall path reads.
|
|
377
|
+
assert.equal(
|
|
378
|
+
contexts.get("sess-attach")?.projectId,
|
|
379
|
+
projectTagProjectId("Blend/Supply"),
|
|
380
|
+
);
|
|
381
|
+
assert.ok(
|
|
382
|
+
getStorageCalls.every((ns) => ns === projectNamespaceFor("Blend/Supply")),
|
|
383
|
+
`expected all getStorage calls on the project namespace, got ${JSON.stringify(getStorageCalls)}`,
|
|
384
|
+
);
|
|
385
|
+
// A later BARE resolve (no per-call context) on the same session — what a
|
|
386
|
+
// subsequent recall on this session uses — is now scoped to the same project.
|
|
387
|
+
const bare = await resolver(service)({
|
|
388
|
+
sessionKey: "sess-attach",
|
|
389
|
+
authenticatedPrincipal: "alice",
|
|
390
|
+
content: "y",
|
|
391
|
+
});
|
|
392
|
+
assert.equal(bare, projectNamespaceFor("Blend/Supply"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("#1434 an explicit-namespace store does NOT bind the session to a project (Codex review)", async () => {
|
|
396
|
+
// An explicit `namespace` bypasses the coding overlay, so the write must not
|
|
397
|
+
// seed a project binding the session never wrote to — else later bare recalls
|
|
398
|
+
// would search a project namespace with no committed memory.
|
|
399
|
+
const { orch, contexts } = makeAttachOrchestrator();
|
|
400
|
+
const service = new EngramAccessService(orch);
|
|
401
|
+
const res = await service.memoryStore(
|
|
402
|
+
storeRequest({ sessionKey: "sess-explicit", namespace: "default", projectTag: "Blend/Supply" }),
|
|
403
|
+
);
|
|
404
|
+
assert.equal(res.status, "stored");
|
|
405
|
+
assert.equal(res.namespace, "default");
|
|
406
|
+
assert.equal(contexts.get("sess-explicit"), undefined, "explicit-namespace write must not bind the session");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("#1434 a dryRun store does NOT bind the session to a project (Codex review)", async () => {
|
|
410
|
+
// A dryRun is a read-only preview; it must not mutate session coding context.
|
|
411
|
+
const { orch, contexts } = makeAttachOrchestrator();
|
|
412
|
+
const service = new EngramAccessService(orch);
|
|
413
|
+
const res = await service.memoryStore(
|
|
414
|
+
storeRequest({ sessionKey: "sess-dry", projectTag: "Blend/Supply", dryRun: true }),
|
|
415
|
+
);
|
|
416
|
+
assert.equal(res.status, "validated");
|
|
417
|
+
assert.equal(contexts.get("sess-dry"), undefined, "dryRun must not bind the session");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── Persist layer (#1434 P1/High): a pre-resolved project namespace must reach
|
|
421
|
+
// storage instead of being rejected by the static policy allow-list. ──────────
|
|
422
|
+
|
|
423
|
+
function makePersistOrchestrator() {
|
|
424
|
+
const getStorageCalls: Array<string | undefined> = [];
|
|
425
|
+
const orch = {
|
|
426
|
+
config: {
|
|
427
|
+
namespacesEnabled: true,
|
|
428
|
+
defaultNamespace: "default",
|
|
429
|
+
sharedNamespace: "shared",
|
|
430
|
+
namespacePolicies: [],
|
|
431
|
+
},
|
|
432
|
+
getStorage: async (ns?: string) => {
|
|
433
|
+
getStorageCalls.push(ns);
|
|
434
|
+
return {
|
|
435
|
+
readAllMemories: async () => [],
|
|
436
|
+
writeMemory: async () => "mem-1",
|
|
437
|
+
appendMemoryLifecycleEvents: async () => {},
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
} as unknown as Orchestrator;
|
|
441
|
+
return { orch, getStorageCalls };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function candidate(overrides: Partial<ValidExplicitCapture> = {}): ValidExplicitCapture {
|
|
445
|
+
return {
|
|
446
|
+
content: "durable project memory",
|
|
447
|
+
category: "fact",
|
|
448
|
+
confidence: 0.9,
|
|
449
|
+
tags: [],
|
|
450
|
+
namespace: "default-project-tag-abc123",
|
|
451
|
+
...overrides,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
test("#1434 persistExplicitCapture routes a pre-resolved project namespace to storage", async () => {
|
|
456
|
+
const { orch, getStorageCalls } = makePersistOrchestrator();
|
|
457
|
+
const res = await persistExplicitCapture(
|
|
458
|
+
orch,
|
|
459
|
+
candidate({ namespacePreResolved: true }),
|
|
460
|
+
"memory_store",
|
|
461
|
+
);
|
|
462
|
+
assert.equal(res.id, "mem-1");
|
|
463
|
+
// The dynamic project namespace must be used verbatim (dup-check + write),
|
|
464
|
+
// never rewritten or rejected.
|
|
465
|
+
assert.ok(
|
|
466
|
+
getStorageCalls.every((ns) => ns === "default-project-tag-abc123"),
|
|
467
|
+
`expected all getStorage calls on the project namespace, got ${JSON.stringify(getStorageCalls)}`,
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("#1434 persistExplicitCapture still rejects an unauthorized namespace when not pre-resolved", async () => {
|
|
472
|
+
const { orch } = makePersistOrchestrator();
|
|
473
|
+
await assert.rejects(
|
|
474
|
+
persistExplicitCapture(orch, candidate(), "memory_store"),
|
|
475
|
+
/unsupported namespace/,
|
|
476
|
+
"the policy allow-list guard must still apply to callers that do not pre-authorize",
|
|
477
|
+
);
|
|
478
|
+
});
|