@remnic/core 9.3.614 → 9.3.615

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.
Files changed (46) hide show
  1. package/dist/access-cli.js +3 -3
  2. package/dist/access-http.d.ts +1 -1
  3. package/dist/access-http.js +5 -5
  4. package/dist/access-mcp.d.ts +1 -1
  5. package/dist/access-mcp.js +4 -4
  6. package/dist/access-schema.d.ts +14 -2
  7. package/dist/access-schema.js +1 -1
  8. package/dist/{access-service-DGG_2xPK.d.ts → access-service-CBNEKjzN.d.ts} +70 -5
  9. package/dist/access-service.d.ts +1 -1
  10. package/dist/access-service.js +2 -2
  11. package/dist/{chunk-B6FDZPCF.js → chunk-5OHHEORR.js} +50 -15
  12. package/dist/chunk-5OHHEORR.js.map +1 -0
  13. package/dist/{chunk-T5XWMMU2.js → chunk-EXUAP5LH.js} +2 -2
  14. package/dist/{chunk-EUML3N6B.js → chunk-IMA6GU4Y.js} +3 -3
  15. package/dist/chunk-IMA6GU4Y.js.map +1 -0
  16. package/dist/{chunk-7YQFWOF7.js → chunk-KGLPJROV.js} +4 -4
  17. package/dist/{chunk-VPGUMLBA.js → chunk-NM5NQYJE.js} +16 -16
  18. package/dist/chunk-NM5NQYJE.js.map +1 -0
  19. package/dist/{chunk-QEMCQFDW.js → chunk-WD2W4234.js} +8 -2
  20. package/dist/chunk-WD2W4234.js.map +1 -0
  21. package/dist/{chunk-ADNZVFXG.js → chunk-ZK32E74R.js} +142 -31
  22. package/dist/chunk-ZK32E74R.js.map +1 -0
  23. package/dist/{cli-DWeu7eTY.d.ts → cli-Cw729yLf.d.ts} +1 -1
  24. package/dist/cli.d.ts +2 -2
  25. package/dist/cli.js +6 -6
  26. package/dist/explicit-capture.d.ts +10 -0
  27. package/dist/explicit-capture.js +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +7 -7
  30. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  31. package/dist/orchestrator.js +2 -2
  32. package/package.json +1 -1
  33. package/src/access-http.ts +21 -10
  34. package/src/access-mcp.test.ts +109 -0
  35. package/src/access-mcp.ts +46 -2
  36. package/src/access-schema.ts +11 -0
  37. package/src/access-service-coding-write.test.ts +478 -0
  38. package/src/access-service.ts +237 -32
  39. package/src/explicit-capture.ts +19 -2
  40. package/dist/chunk-ADNZVFXG.js.map +0 -1
  41. package/dist/chunk-B6FDZPCF.js.map +0 -1
  42. package/dist/chunk-EUML3N6B.js.map +0 -1
  43. package/dist/chunk-QEMCQFDW.js.map +0 -1
  44. package/dist/chunk-VPGUMLBA.js.map +0 -1
  45. /package/dist/{chunk-T5XWMMU2.js.map → chunk-EXUAP5LH.js.map} +0 -0
  46. /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
+ });