@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
@@ -7,7 +7,11 @@ import { AccessIdempotencyStore, hashAccessIdempotencyPayload } from "./access-i
7
7
  import { AccessAuditAdapter, type AccessAuditConfig, type AccessAuditResult } from "./access-audit.js";
8
8
  import type { AnomalyDetectorResult } from "./recall-audit-anomaly.js";
9
9
  import { resolveGitContext } from "./coding/git-context.js";
10
- import { projectTagProjectId } from "./coding/coding-namespace.js";
10
+ import {
11
+ combineNamespaces,
12
+ projectTagProjectId,
13
+ resolveCodingNamespaceOverlay,
14
+ } from "./coding/coding-namespace.js";
11
15
  import { WorkStorage } from "./work/storage.js";
12
16
  import {
13
17
  exportWorkBoardMarkdown,
@@ -91,6 +95,7 @@ import type {
91
95
  EntityFile,
92
96
  MemoryFile,
93
97
  MemoryActionOutcome,
98
+ CodingContext,
94
99
  MemoryActionType,
95
100
  MemoryLifecycleEvent,
96
101
  MemoryStatus,
@@ -755,9 +760,25 @@ export interface EngramAccessWriteEnvelope {
755
760
  authenticatedPrincipal?: string;
756
761
  }
757
762
 
758
- export interface EngramAccessMemoryStoreRequest extends EngramAccessWriteEnvelope, ExplicitCaptureInput {}
763
+ /**
764
+ * Optional git/project context for project-scoped writes (#1434). When no
765
+ * explicit `namespace` is supplied, these route the write to the same project
766
+ * namespace recall/observe resolve from `cwd`/`projectTag` (rule 42 symmetry).
767
+ */
768
+ export interface CodingScopedWriteInput {
769
+ cwd?: string;
770
+ projectTag?: string;
771
+ }
759
772
 
760
- export interface EngramAccessSuggestionSubmitRequest extends EngramAccessWriteEnvelope, ExplicitCaptureInput {}
773
+ export interface EngramAccessMemoryStoreRequest
774
+ extends EngramAccessWriteEnvelope,
775
+ ExplicitCaptureInput,
776
+ CodingScopedWriteInput {}
777
+
778
+ export interface EngramAccessSuggestionSubmitRequest
779
+ extends EngramAccessWriteEnvelope,
780
+ ExplicitCaptureInput,
781
+ CodingScopedWriteInput {}
761
782
 
762
783
  export interface EngramAccessWriteResponse {
763
784
  schemaVersion: 1;
@@ -1115,6 +1136,144 @@ export class EngramAccessService {
1115
1136
  return resolved;
1116
1137
  }
1117
1138
 
1139
+ /**
1140
+ * Resolve a coding context from `cwd`/`projectTag` WITHOUT persisting it to
1141
+ * any session — the read-only half of `maybeAttachCodingContext`. Returns
1142
+ * null when project scoping is off or nothing resolves. `projectTag` takes
1143
+ * priority over `cwd` (matching `maybeAttachCodingContext`).
1144
+ */
1145
+ private async resolveCodingContextFromOptions(
1146
+ options: CodingScopedWriteInput,
1147
+ ): Promise<CodingContext | null> {
1148
+ if (!this.orchestrator.config.codingMode?.projectScope) return null;
1149
+ if (typeof options.projectTag === "string" && options.projectTag.trim().length > 0) {
1150
+ const projectId = projectTagProjectId(options.projectTag);
1151
+ return { projectId, branch: null, rootPath: projectId, defaultBranch: null };
1152
+ }
1153
+ if (typeof options.cwd === "string" && options.cwd.trim().length > 0) {
1154
+ try {
1155
+ const gitCtx = await resolveGitContext(options.cwd);
1156
+ if (gitCtx) {
1157
+ return {
1158
+ projectId: gitCtx.projectId,
1159
+ branch: gitCtx.branch,
1160
+ rootPath: gitCtx.rootPath,
1161
+ defaultBranch: gitCtx.defaultBranch,
1162
+ };
1163
+ }
1164
+ } catch {
1165
+ // resolveGitContext never throws, but stay defensive — not being in a
1166
+ // repo is normal and must not break the write.
1167
+ }
1168
+ }
1169
+ return null;
1170
+ }
1171
+
1172
+ /**
1173
+ * Resolve the write namespace for explicit-write tools (memory_store /
1174
+ * suggestion_submit), project-scoping the write the same way recall does so a
1175
+ * memory stored with a client-injected `cwd`/`projectTag` is discoverable by
1176
+ * project-scoped recall (#1434, rule 42).
1177
+ *
1178
+ * Precedence:
1179
+ * - An explicit `namespace` always wins and is authorized strictly via
1180
+ * `resolveWritableNamespace` → `canWriteNamespace`. A coding-overlay
1181
+ * namespace string (`<base>-project-*`) is NOT a writable target via the
1182
+ * explicit field — project scoping is requested with `cwd`/`projectTag`,
1183
+ * never by naming the derived namespace — so there is no way to bypass the
1184
+ * policy allow-list by guessing/forging an overlay name (Codex review).
1185
+ * - With NO coding overlay, the write stays on `config.defaultNamespace` —
1186
+ * exactly the pre-#1434 behavior, so an unqualified write is NOT silently
1187
+ * moved to a principal self namespace (Codex review).
1188
+ * - WITH a coding overlay, the base is the principal self namespace
1189
+ * (`defaultNamespaceForPrincipal`, write-checked) — the SAME base recall,
1190
+ * observe, and the orchestrator buffer-flush write path overlay onto
1191
+ * (rule 42 / Cursor) — so a project-scoped store lands exactly where
1192
+ * project-scoped recall searches. The overlay namespace is always REBUILT
1193
+ * from the authenticated principal's base, never accepted as a caller
1194
+ * string, so a caller can never reach another principal's subtree.
1195
+ *
1196
+ * Read-only: this NEVER mutates session coding context, so the idempotency
1197
+ * peeks and dryRun preflights that call it stay side-effect free (Codex
1198
+ * review). It prefers the per-call `cwd`/`projectTag` (the project explicitly
1199
+ * identified for this write), else the session's existing context. The HTTP
1200
+ * surface lets the peek and the write each resolve independently; the peek's
1201
+ * namespace only gates rate-limiting (memory_store/suggestion_submit run their
1202
+ * own idempotency check), so a benign session-context change between the two
1203
+ * never fails a write — there is no namespace to "pin".
1204
+ */
1205
+ private async resolveCodingScopedWriteNamespace(
1206
+ request: CodingScopedWriteInput & {
1207
+ namespace?: string;
1208
+ sessionKey?: string;
1209
+ authenticatedPrincipal?: string;
1210
+ },
1211
+ ): Promise<string> {
1212
+ const hasExplicitNamespace =
1213
+ typeof request.namespace === "string" && request.namespace.trim().length > 0;
1214
+ if (hasExplicitNamespace) {
1215
+ return this.resolveWritableNamespace(
1216
+ request.namespace,
1217
+ request.sessionKey,
1218
+ request.authenticatedPrincipal,
1219
+ );
1220
+ }
1221
+ // Project scoping only applies when namespaces are enabled (else overlaying
1222
+ // would create false isolation over a single storage dir) and projectScope
1223
+ // is on. The coding context MUST be resolved exactly as the recall path
1224
+ // resolves it, or a scoped store won't be discoverable by scoped recall
1225
+ // (the whole point of #1434). Recall calls `maybeAttachCodingContext`, which
1226
+ // returns early when the session already has a context — so recall is
1227
+ // SESSION-FIRST: an existing session binding wins, and the per-call
1228
+ // cwd/projectTag is only used to seed a context when none is attached yet.
1229
+ // Mirror that precedence here: session context first, per-call as fallback
1230
+ // (Codex review — a per-call-wins write would land in a project that the
1231
+ // same session's recall, still on the bound project, never searches).
1232
+ //
1233
+ // A sessionKey is REQUIRED to apply the overlay. The recall path can only
1234
+ // attach/look up coding context per session (`maybeAttachCodingContext` and
1235
+ // `applyCodingNamespaceOverlay` both no-op without a sessionKey), so a
1236
+ // sessionless recall always searches the base namespace. A sessionless
1237
+ // write must therefore also stay on the base — otherwise a client that
1238
+ // injects cwd/projectTag but no sessionKey would store into
1239
+ // `default-project-*` that its own recall never searches (Codex review).
1240
+ const hasSession =
1241
+ typeof request.sessionKey === "string" && request.sessionKey.length > 0;
1242
+ const overlay =
1243
+ hasSession &&
1244
+ this.orchestrator.config.namespacesEnabled &&
1245
+ this.orchestrator.config.codingMode?.projectScope
1246
+ ? resolveCodingNamespaceOverlay(
1247
+ this.orchestrator.getCodingContextForSession(request.sessionKey) ??
1248
+ (await this.resolveCodingContextFromOptions(request)),
1249
+ this.orchestrator.config.codingMode,
1250
+ this.orchestrator.config.defaultNamespace,
1251
+ )
1252
+ : null;
1253
+ if (!overlay) {
1254
+ // No coding overlay → unqualified write stays on config.defaultNamespace,
1255
+ // exactly the pre-#1434 behavior (auth-checked, like the legacy path).
1256
+ return this.resolveWritableNamespace(
1257
+ undefined,
1258
+ request.sessionKey,
1259
+ request.authenticatedPrincipal,
1260
+ );
1261
+ }
1262
+ // Coding overlay → overlay onto the principal self base, the SAME namespace
1263
+ // recall/observe/buffer-flush use. The result is a principal-owned
1264
+ // `project-*` sub-namespace derived from this authorized base, so it needs
1265
+ // no separate write policy.
1266
+ const principal = this.resolveRequestPrincipal(
1267
+ request.sessionKey,
1268
+ request.authenticatedPrincipal,
1269
+ );
1270
+ const base = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
1271
+ if (!canWriteNamespace(principal, base, this.orchestrator.config)) {
1272
+ throw new EngramAccessInputError(`namespace is not writable: ${base}`);
1273
+ }
1274
+ return combineNamespaces(base, overlay.namespace);
1275
+ }
1276
+
1118
1277
  private async objectiveStateStoreLocationForNamespace(namespace: string): Promise<{
1119
1278
  memoryDir: string;
1120
1279
  objectiveStateStoreDir?: string;
@@ -1384,6 +1543,16 @@ export class EngramAccessService {
1384
1543
  idempotencyKey?: string;
1385
1544
  requestFingerprint: unknown;
1386
1545
  skip?: boolean;
1546
+ /**
1547
+ * Invoked exactly once, immediately before an ACTUAL (non-replay, non-skip)
1548
+ * write is committed — atomically with the idempotency miss determination.
1549
+ * The HTTP surface uses this to enforce the write rate limit against the
1550
+ * real write/miss (and the real resolved namespace), so a namespace-divergent
1551
+ * idempotency peek can never let a fresh write skip the quota check (#1434
1552
+ * Codex review). It is NOT called on dryRun (skip) or replay, preserving the
1553
+ * replay-bypasses-a-full-window behavior.
1554
+ */
1555
+ beforeExecute?: () => void | Promise<void>;
1387
1556
  execute: () => Promise<T>;
1388
1557
  }): Promise<T> {
1389
1558
  if (options.skip === true) {
@@ -1391,6 +1560,7 @@ export class EngramAccessService {
1391
1560
  }
1392
1561
  const key = options.idempotencyKey?.trim();
1393
1562
  if (!key) {
1563
+ if (options.beforeExecute) await options.beforeExecute();
1394
1564
  return options.execute();
1395
1565
  }
1396
1566
  return this.withIdempotencyLock(key, async () => {
@@ -1409,6 +1579,7 @@ export class EngramAccessService {
1409
1579
  idempotencyReplay: true,
1410
1580
  };
1411
1581
  }
1582
+ if (options.beforeExecute) await options.beforeExecute();
1412
1583
  const response = await options.execute();
1413
1584
  await this.idempotency.put(key, requestHash, response);
1414
1585
  return response;
@@ -1757,6 +1928,28 @@ export class EngramAccessService {
1757
1928
  }
1758
1929
  }
1759
1930
 
1931
+ /**
1932
+ * Seed the session's coding binding AFTER a committed, project-scoped explicit
1933
+ * write (memory_store / suggestion_submit), mirroring the recall path's
1934
+ * `maybeAttachCodingContext` so a later bare recall/write on the same session
1935
+ * is scoped to the same project. Called only from the post-persist path, so it
1936
+ * never fires on dryRun, replay/conflict, or quota-rejected requests. Skips
1937
+ * when an explicit `namespace` was supplied — that write bypassed the coding
1938
+ * overlay, so binding the session to a project it never wrote to would make
1939
+ * later bare recalls miss (Codex review).
1940
+ */
1941
+ private async attachCodingContextAfterScopedWrite(
1942
+ request: CodingScopedWriteInput & { namespace?: string; sessionKey?: string },
1943
+ ): Promise<void> {
1944
+ const hasExplicitNamespace =
1945
+ typeof request.namespace === "string" && request.namespace.trim().length > 0;
1946
+ if (hasExplicitNamespace) return;
1947
+ await this.maybeAttachCodingContext(request.sessionKey, {
1948
+ cwd: request.cwd,
1949
+ projectTag: request.projectTag,
1950
+ });
1951
+ }
1952
+
1760
1953
  async recall(request: EngramAccessRecallRequest): Promise<EngramAccessRecallResponse> {
1761
1954
  const query = request.query.trim();
1762
1955
  if (query.length === 0) {
@@ -2662,12 +2855,11 @@ export class EngramAccessService {
2662
2855
  // per-tenant) do not block each other.
2663
2856
  private xrayQueue: Promise<void> = Promise.resolve();
2664
2857
 
2665
- async memoryStore(request: EngramAccessMemoryStoreRequest): Promise<EngramAccessWriteResponse> {
2666
- const namespace = this.resolveWritableNamespace(
2667
- request.namespace,
2668
- request.sessionKey,
2669
- request.authenticatedPrincipal,
2670
- );
2858
+ async memoryStore(
2859
+ request: EngramAccessMemoryStoreRequest,
2860
+ hooks?: { enforceWriteQuota?: () => void | Promise<void> },
2861
+ ): Promise<EngramAccessWriteResponse> {
2862
+ const namespace = await this.resolveCodingScopedWriteNamespace(request);
2671
2863
  const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
2672
2864
  if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
2673
2865
  throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
@@ -2687,6 +2879,14 @@ export class EngramAccessService {
2687
2879
  };
2688
2880
  }
2689
2881
  const result = await persistExplicitCapture(this.orchestrator, candidate, "memory_store");
2882
+ // Seed the session's coding binding ONLY after a real write commits, and
2883
+ // only when the namespace came from project scoping (no explicit
2884
+ // namespace). This mirrors recall's maybeAttachCodingContext so a LATER
2885
+ // bare recall/write on the same session is scoped to the same project —
2886
+ // but never binds the session on a dryRun, replay/conflict, quota
2887
+ // rejection, or an explicit-namespace write (which bypasses the overlay),
2888
+ // since those don't reach this point or aren't project-scoped (Codex review).
2889
+ await this.attachCodingContextAfterScopedWrite(request);
2690
2890
  const response: EngramAccessWriteResponse = {
2691
2891
  schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
2692
2892
  operation: "memory_store",
@@ -2719,16 +2919,13 @@ export class EngramAccessService {
2719
2919
  sourceReason: request.sourceReason,
2720
2920
  },
2721
2921
  skip: request.dryRun === true,
2922
+ beforeExecute: hooks?.enforceWriteQuota,
2722
2923
  execute,
2723
2924
  });
2724
2925
  }
2725
2926
 
2726
2927
  async peekMemoryStoreIdempotency(request: EngramAccessMemoryStoreRequest): Promise<EngramAccessIdempotencyStatus> {
2727
- const namespace = this.resolveWritableNamespace(
2728
- request.namespace,
2729
- request.sessionKey,
2730
- request.authenticatedPrincipal,
2731
- );
2928
+ const namespace = await this.resolveCodingScopedWriteNamespace(request);
2732
2929
  const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
2733
2930
  if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
2734
2931
  throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
@@ -2751,12 +2948,11 @@ export class EngramAccessService {
2751
2948
  });
2752
2949
  }
2753
2950
 
2754
- async suggestionSubmit(request: EngramAccessSuggestionSubmitRequest): Promise<EngramAccessWriteResponse> {
2755
- const namespace = this.resolveWritableNamespace(
2756
- request.namespace,
2757
- request.sessionKey,
2758
- request.authenticatedPrincipal,
2759
- );
2951
+ async suggestionSubmit(
2952
+ request: EngramAccessSuggestionSubmitRequest,
2953
+ hooks?: { enforceWriteQuota?: () => void | Promise<void> },
2954
+ ): Promise<EngramAccessWriteResponse> {
2955
+ const namespace = await this.resolveCodingScopedWriteNamespace(request);
2760
2956
  const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
2761
2957
  if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
2762
2958
  throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
@@ -2781,6 +2977,10 @@ export class EngramAccessService {
2781
2977
  "suggestion_submit",
2782
2978
  new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit"),
2783
2979
  );
2980
+ // Seed the session binding only after a real, project-scoped submit commits
2981
+ // (mirrors memory_store / recall; skips dryRun, replay, quota-reject, and
2982
+ // explicit-namespace writes — Codex review).
2983
+ await this.attachCodingContextAfterScopedWrite(request);
2784
2984
  const response: EngramAccessWriteResponse = {
2785
2985
  schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
2786
2986
  operation: "suggestion_submit",
@@ -2813,6 +3013,7 @@ export class EngramAccessService {
2813
3013
  sourceReason: request.sourceReason,
2814
3014
  },
2815
3015
  skip: request.dryRun === true,
3016
+ beforeExecute: hooks?.enforceWriteQuota,
2816
3017
  execute,
2817
3018
  });
2818
3019
  }
@@ -2820,11 +3021,7 @@ export class EngramAccessService {
2820
3021
  async peekSuggestionSubmitIdempotency(
2821
3022
  request: EngramAccessSuggestionSubmitRequest,
2822
3023
  ): Promise<EngramAccessIdempotencyStatus> {
2823
- const namespace = this.resolveWritableNamespace(
2824
- request.namespace,
2825
- request.sessionKey,
2826
- request.authenticatedPrincipal,
2827
- );
3024
+ const namespace = await this.resolveCodingScopedWriteNamespace(request);
2828
3025
  const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
2829
3026
  if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
2830
3027
  throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
@@ -2852,13 +3049,21 @@ export class EngramAccessService {
2852
3049
  namespace: string,
2853
3050
  ): ValidExplicitCapture {
2854
3051
  try {
2855
- return validateExplicitCaptureInput(
2856
- {
2857
- ...request,
2858
- namespace,
2859
- },
2860
- "legacy_tool",
2861
- );
3052
+ return {
3053
+ ...validateExplicitCaptureInput(
3054
+ {
3055
+ ...request,
3056
+ namespace,
3057
+ },
3058
+ "legacy_tool",
3059
+ ),
3060
+ // The namespace was resolved AND authorized by
3061
+ // resolveCodingScopedWriteNamespace (explicit namespaces via
3062
+ // resolveWritableNamespace; otherwise an auth-checked base + a
3063
+ // session-owned project overlay), so the persist/queue layer must not
3064
+ // re-reject a legitimately-derived dynamic project namespace (#1434).
3065
+ namespacePreResolved: true,
3066
+ };
2862
3067
  } catch (error) {
2863
3068
  const message = error instanceof Error ? error.message : String(error);
2864
3069
  throw new EngramAccessInputError(message);
@@ -25,6 +25,16 @@ export type ValidExplicitCapture = {
25
25
  entityRef?: string;
26
26
  expiresAt?: string;
27
27
  sourceReason?: string;
28
+ /**
29
+ * When true, `namespace` was already resolved AND authorized by the caller
30
+ * (the access service's `resolveCodingScopedWriteNamespace`, which auth-checks
31
+ * the base and derives a session-owned `project-*` overlay). The persist /
32
+ * queue layer then routes to it directly instead of re-validating against the
33
+ * static policy allow-list — which would otherwise reject legitimately-derived
34
+ * dynamic project namespaces (#1434). Callers that do NOT pre-authorize the
35
+ * namespace must leave this unset so the allow-list guard still applies.
36
+ */
37
+ namespacePreResolved?: boolean;
28
38
  };
29
39
 
30
40
  export type ExplicitCaptureSource = "memory_store" | "memory_capture" | "suggestion_submit" | "inline";
@@ -404,7 +414,9 @@ export async function persistExplicitCapture(
404
414
  candidate: ValidExplicitCapture,
405
415
  source: ExplicitCaptureSource,
406
416
  ): Promise<{ id: string; duplicateOf?: string }> {
407
- const resolvedNamespace = resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);
417
+ const resolvedNamespace = candidate.namespacePreResolved
418
+ ? asTrimmed(candidate.namespace)
419
+ : resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);
408
420
  const duplicateOf = await findDuplicateExplicitCapture(orchestrator, resolvedNamespace, candidate);
409
421
  if (duplicateOf) {
410
422
  return { id: duplicateOf, duplicateOf };
@@ -490,7 +502,12 @@ export async function queueExplicitCaptureForReview(
490
502
  ): Promise<{ id: string; duplicateOf?: string }> {
491
503
  const reason = sanitizeReviewText(normalizeExplicitCaptureError(error), "explicit capture failed");
492
504
  const requestedNamespace = asTrimmed(input.namespace);
493
- const queueNamespace = resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);
505
+ // A caller-pre-authorized namespace (e.g. a session-owned project overlay
506
+ // from the access service) routes directly; otherwise apply the static
507
+ // policy allow-list guard (#1434).
508
+ const queueNamespace = (input as { namespacePreResolved?: boolean }).namespacePreResolved
509
+ ? requestedNamespace
510
+ : resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);
494
511
  const content = buildExplicitCaptureReviewContent(input, reason);
495
512
  const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content);
496
513
  if (duplicateOf) {