@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.
- 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 +14 -2
- 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/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
package/src/access-service.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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.
|
|
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(
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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.
|
|
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
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
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);
|
package/src/explicit-capture.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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) {
|