@remnic/core 9.69.96 → 9.69.97

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 +9 -9
  2. package/dist/access-http.d.ts.map +1 -1
  3. package/dist/access-http.js +7 -7
  4. package/dist/adapters/claude-code.js +2 -2
  5. package/dist/adapters/codex.js +2 -2
  6. package/dist/adapters/hermes.js +2 -2
  7. package/dist/adapters/index.js +6 -6
  8. package/dist/adapters/registry.js +6 -6
  9. package/dist/adapters/replit.js +2 -2
  10. package/dist/adapters/types.d.ts +2 -0
  11. package/dist/adapters/types.d.ts.map +1 -1
  12. package/dist/adapters/types.js +1 -1
  13. package/dist/{chunk-F23U5YE7.js → chunk-2KB7TTAP.js} +12 -10
  14. package/dist/chunk-2KB7TTAP.js.map +1 -0
  15. package/dist/{chunk-J64TK33U.js → chunk-3JKYQBLC.js} +5 -4
  16. package/dist/{chunk-J64TK33U.js.map → chunk-3JKYQBLC.js.map} +1 -1
  17. package/dist/{chunk-RSUYKGGZ.js → chunk-7RKSJVOQ.js} +5 -4
  18. package/dist/{chunk-RSUYKGGZ.js.map → chunk-7RKSJVOQ.js.map} +1 -1
  19. package/dist/{chunk-KNI4SGQE.js → chunk-CTEUPJMV.js} +3 -3
  20. package/dist/{chunk-42NQ7AVG.js → chunk-HCAGBFE7.js} +5 -4
  21. package/dist/{chunk-42NQ7AVG.js.map → chunk-HCAGBFE7.js.map} +1 -1
  22. package/dist/{chunk-R5B3OWI4.js → chunk-HJ3CBZIG.js} +2 -2
  23. package/dist/{chunk-7RXCMVFQ.js → chunk-NJAIR57U.js} +1 -1
  24. package/dist/{chunk-7RXCMVFQ.js.map → chunk-NJAIR57U.js.map} +1 -1
  25. package/dist/{chunk-HQCUUVG2.js → chunk-RMV2JYOT.js} +9 -2
  26. package/dist/chunk-RMV2JYOT.js.map +1 -0
  27. package/dist/{chunk-TMSXWOBZ.js → chunk-SI36RWC3.js} +5 -4
  28. package/dist/{chunk-TMSXWOBZ.js.map → chunk-SI36RWC3.js.map} +1 -1
  29. package/dist/cli.js +8 -8
  30. package/dist/index.js +9 -9
  31. package/dist/orchestrator.js +9 -9
  32. package/package.json +2 -2
  33. package/src/access-http-adapter-namespace.test.ts +217 -0
  34. package/src/access-http.ts +8 -0
  35. package/src/adapters/claude-code.ts +4 -4
  36. package/src/adapters/codex.ts +3 -3
  37. package/src/adapters/grok-opencode.test.ts +2 -0
  38. package/src/adapters/grok.ts +3 -3
  39. package/src/adapters/hermes.ts +3 -3
  40. package/src/adapters/opencode.ts +3 -3
  41. package/src/adapters/replit.ts +3 -3
  42. package/src/adapters/types.ts +2 -0
  43. package/dist/chunk-F23U5YE7.js.map +0 -1
  44. package/dist/chunk-HQCUUVG2.js.map +0 -1
  45. /package/dist/{chunk-KNI4SGQE.js.map → chunk-CTEUPJMV.js.map} +0 -0
  46. /package/dist/{chunk-R5B3OWI4.js.map → chunk-HJ3CBZIG.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  headerValue
3
- } from "./chunk-7RXCMVFQ.js";
3
+ } from "./chunk-NJAIR57U.js";
4
4
 
5
5
  // src/adapters/replit.ts
6
6
  var ReplitAdapter = class {
@@ -14,9 +14,10 @@ var ReplitAdapter = class {
14
14
  }
15
15
  resolveIdentity(context) {
16
16
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
17
- const namespace = headerValue(context.headers, "x-engram-namespace") || "replit";
17
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
18
18
  return {
19
- namespace,
19
+ namespace: namespaceHeader ?? "replit",
20
+ namespaceExplicit: namespaceHeader !== void 0,
20
21
  principal: "replit-agent",
21
22
  sessionKey: mcpSessionId ?? context.sessionKey,
22
23
  adapterId: this.id
@@ -27,4 +28,4 @@ var ReplitAdapter = class {
27
28
  export {
28
29
  ReplitAdapter
29
30
  };
30
- //# sourceMappingURL=chunk-TMSXWOBZ.js.map
31
+ //# sourceMappingURL=chunk-SI36RWC3.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapters/replit.ts"],"sourcesContent":["import { headerValue, type AdapterContext, type EngramAdapter, type ResolvedIdentity } from \"./types.js\";\n\n/**\n * Replit Agent adapter.\n *\n * Detection: Replit Agent supports MCP natively (HTTP-only, configured\n * via the Integrations pane). It does NOT send identifying headers\n * automatically — detection relies on user-configured custom headers\n * in Replit's MCP Integrations UI.\n *\n * Replit provides env vars to running code (REPL_ID, REPL_OWNER,\n * REPL_SLUG) but these are NOT sent as HTTP headers to MCP servers.\n *\n * To identify Replit connections, users should configure a custom header\n * in the Replit Integrations pane when adding the Engram MCP server:\n * Header Name: X-Engram-Client-Id\n * Header Value: replit\n *\n * Optionally also set X-Engram-Namespace for project scoping. Principal\n * overrides are intentionally handled only by the HTTP server's\n * trustPrincipalHeader gate. Adapters must not independently trust\n * X-Engram-Principal.\n */\nexport class ReplitAdapter implements EngramAdapter {\n readonly id = \"replit\";\n\n matches(context: AdapterContext): boolean {\n // Primary: user-configured client identifier header\n const clientId = headerValue(context.headers, \"x-engram-client-id\");\n if (clientId?.toLowerCase() === \"replit\") return true;\n\n // MCP clientInfo (Replit's MCP client name is not publicly documented,\n // but check for it in case it becomes available)\n const clientName = context.clientInfo?.name?.toLowerCase() ?? \"\";\n if (clientName.includes(\"replit\")) return true;\n\n return false;\n }\n\n resolveIdentity(context: AdapterContext): ResolvedIdentity {\n const mcpSessionId = headerValue(context.headers, \"mcp-session-id\");\n\n const namespace = headerValue(context.headers, \"x-engram-namespace\")\n || \"replit\";\n\n return {\n namespace,\n principal: \"replit-agent\",\n sessionKey: mcpSessionId ?? context.sessionKey,\n adapterId: this.id,\n };\n }\n}\n"],"mappings":";;;;;AAuBO,IAAM,gBAAN,MAA6C;AAAA,EACzC,KAAK;AAAA,EAEd,QAAQ,SAAkC;AAExC,UAAM,WAAW,YAAY,QAAQ,SAAS,oBAAoB;AAClE,QAAI,UAAU,YAAY,MAAM,SAAU,QAAO;AAIjD,UAAM,aAAa,QAAQ,YAAY,MAAM,YAAY,KAAK;AAC9D,QAAI,WAAW,SAAS,QAAQ,EAAG,QAAO;AAE1C,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgB,SAA2C;AACzD,UAAM,eAAe,YAAY,QAAQ,SAAS,gBAAgB;AAElE,UAAM,YAAY,YAAY,QAAQ,SAAS,oBAAoB,KAC9D;AAEL,WAAO;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX,YAAY,gBAAgB,QAAQ;AAAA,MACpC,WAAW,KAAK;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/adapters/replit.ts"],"sourcesContent":["import { headerValue, type AdapterContext, type EngramAdapter, type ResolvedIdentity } from \"./types.js\";\n\n/**\n * Replit Agent adapter.\n *\n * Detection: Replit Agent supports MCP natively (HTTP-only, configured\n * via the Integrations pane). It does NOT send identifying headers\n * automatically — detection relies on user-configured custom headers\n * in Replit's MCP Integrations UI.\n *\n * Replit provides env vars to running code (REPL_ID, REPL_OWNER,\n * REPL_SLUG) but these are NOT sent as HTTP headers to MCP servers.\n *\n * To identify Replit connections, users should configure a custom header\n * in the Replit Integrations pane when adding the Engram MCP server:\n * Header Name: X-Engram-Client-Id\n * Header Value: replit\n *\n * Optionally also set X-Engram-Namespace for project scoping. Principal\n * overrides are intentionally handled only by the HTTP server's\n * trustPrincipalHeader gate. Adapters must not independently trust\n * X-Engram-Principal.\n */\nexport class ReplitAdapter implements EngramAdapter {\n readonly id = \"replit\";\n\n matches(context: AdapterContext): boolean {\n // Primary: user-configured client identifier header\n const clientId = headerValue(context.headers, \"x-engram-client-id\");\n if (clientId?.toLowerCase() === \"replit\") return true;\n\n // MCP clientInfo (Replit's MCP client name is not publicly documented,\n // but check for it in case it becomes available)\n const clientName = context.clientInfo?.name?.toLowerCase() ?? \"\";\n if (clientName.includes(\"replit\")) return true;\n\n return false;\n }\n\n resolveIdentity(context: AdapterContext): ResolvedIdentity {\n const mcpSessionId = headerValue(context.headers, \"mcp-session-id\");\n\n const namespaceHeader = headerValue(context.headers, \"x-engram-namespace\");\n\n return {\n namespace: namespaceHeader ?? \"replit\",\n namespaceExplicit: namespaceHeader !== undefined,\n principal: \"replit-agent\",\n sessionKey: mcpSessionId ?? context.sessionKey,\n adapterId: this.id,\n };\n }\n}\n"],"mappings":";;;;;AAuBO,IAAM,gBAAN,MAA6C;AAAA,EACzC,KAAK;AAAA,EAEd,QAAQ,SAAkC;AAExC,UAAM,WAAW,YAAY,QAAQ,SAAS,oBAAoB;AAClE,QAAI,UAAU,YAAY,MAAM,SAAU,QAAO;AAIjD,UAAM,aAAa,QAAQ,YAAY,MAAM,YAAY,KAAK;AAC9D,QAAI,WAAW,SAAS,QAAQ,EAAG,QAAO;AAE1C,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgB,SAA2C;AACzD,UAAM,eAAe,YAAY,QAAQ,SAAS,gBAAgB;AAElE,UAAM,kBAAkB,YAAY,QAAQ,SAAS,oBAAoB;AAEzE,WAAO;AAAA,MACL,WAAW,mBAAmB;AAAA,MAC9B,mBAAmB,oBAAoB;AAAA,MACvC,WAAW;AAAA,MACX,YAAY,gBAAgB,QAAQ;AAAA,MACpC,WAAW,KAAK;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
package/dist/cli.js CHANGED
@@ -91,7 +91,7 @@ import {
91
91
  runWorkProductStatusCliCommand,
92
92
  runWorkProjectCliCommand,
93
93
  runWorkTaskCliCommand
94
- } from "./chunk-R5B3OWI4.js";
94
+ } from "./chunk-HJ3CBZIG.js";
95
95
  import "./chunk-H4CKS5B2.js";
96
96
  import "./chunk-MMACM5G2.js";
97
97
  import "./chunk-54BGIBCR.js";
@@ -189,16 +189,16 @@ import "./chunk-UHESJ6TQ.js";
189
189
  import "./chunk-27NO2KVB.js";
190
190
  import "./chunk-KFZY2OFH.js";
191
191
  import "./chunk-B32WYRXX.js";
192
- import "./chunk-HQCUUVG2.js";
192
+ import "./chunk-RMV2JYOT.js";
193
193
  import "./chunk-V7IZKJI3.js";
194
194
  import "./chunk-XW2T4QAW.js";
195
- import "./chunk-F23U5YE7.js";
196
- import "./chunk-42NQ7AVG.js";
197
- import "./chunk-TMSXWOBZ.js";
195
+ import "./chunk-2KB7TTAP.js";
196
+ import "./chunk-HCAGBFE7.js";
197
+ import "./chunk-SI36RWC3.js";
198
198
  import "./chunk-TB6CBZ5V.js";
199
- import "./chunk-J64TK33U.js";
200
- import "./chunk-RSUYKGGZ.js";
201
- import "./chunk-7RXCMVFQ.js";
199
+ import "./chunk-3JKYQBLC.js";
200
+ import "./chunk-7RKSJVOQ.js";
201
+ import "./chunk-NJAIR57U.js";
202
202
  import "./chunk-ZSEFGDCI.js";
203
203
  import "./chunk-P6P35FY4.js";
204
204
  import "./chunk-3HSRWJTC.js";
package/dist/index.js CHANGED
@@ -181,7 +181,7 @@ import {
181
181
  writeManifest,
182
182
  writeMeetingEpisodeMemory,
183
183
  writeTimelineState
184
- } from "./chunk-KNI4SGQE.js";
184
+ } from "./chunk-CTEUPJMV.js";
185
185
  import {
186
186
  WEARABLE_SOURCE_PREFIX,
187
187
  buildExtractionTurns,
@@ -558,7 +558,7 @@ import {
558
558
  setVaultPublisherTestHooks,
559
559
  summarizeVaultPublish,
560
560
  validateVaultProperties
561
- } from "./chunk-R5B3OWI4.js";
561
+ } from "./chunk-HJ3CBZIG.js";
562
562
  import "./chunk-H4CKS5B2.js";
563
563
  import "./chunk-MMACM5G2.js";
564
564
  import "./chunk-54BGIBCR.js";
@@ -895,19 +895,19 @@ import "./chunk-QW27DOHG.js";
895
895
  import {
896
896
  EngramAccessHttpServer,
897
897
  MeetingsInputError
898
- } from "./chunk-HQCUUVG2.js";
898
+ } from "./chunk-RMV2JYOT.js";
899
899
  import {
900
900
  WearablesInputError,
901
901
  describeErrorForOperator
902
902
  } from "./chunk-V7IZKJI3.js";
903
903
  import "./chunk-XW2T4QAW.js";
904
- import "./chunk-F23U5YE7.js";
905
- import "./chunk-42NQ7AVG.js";
906
- import "./chunk-TMSXWOBZ.js";
904
+ import "./chunk-2KB7TTAP.js";
905
+ import "./chunk-HCAGBFE7.js";
906
+ import "./chunk-SI36RWC3.js";
907
907
  import "./chunk-TB6CBZ5V.js";
908
- import "./chunk-J64TK33U.js";
909
- import "./chunk-RSUYKGGZ.js";
910
- import "./chunk-7RXCMVFQ.js";
908
+ import "./chunk-3JKYQBLC.js";
909
+ import "./chunk-7RKSJVOQ.js";
910
+ import "./chunk-NJAIR57U.js";
911
911
  import "./chunk-ZSEFGDCI.js";
912
912
  import "./chunk-P6P35FY4.js";
913
913
  import "./chunk-3HSRWJTC.js";
@@ -4,7 +4,7 @@ import {
4
4
  graphPathRelativeToStorage,
5
5
  mergeGraphExpandedResults,
6
6
  qmdStartupCollectionChecksWithTimeout
7
- } from "./chunk-KNI4SGQE.js";
7
+ } from "./chunk-CTEUPJMV.js";
8
8
  import "./chunk-JDOBIFBK.js";
9
9
  import "./chunk-KYOZ5E6V.js";
10
10
  import {
@@ -116,7 +116,7 @@ import "./chunk-PYIR4SSB.js";
116
116
  import "./chunk-4G2YCKLT.js";
117
117
  import "./chunk-GDVORIFM.js";
118
118
  import "./chunk-3I4FUAVJ.js";
119
- import "./chunk-R5B3OWI4.js";
119
+ import "./chunk-HJ3CBZIG.js";
120
120
  import "./chunk-H4CKS5B2.js";
121
121
  import "./chunk-MMACM5G2.js";
122
122
  import "./chunk-54BGIBCR.js";
@@ -231,16 +231,16 @@ import "./chunk-27NO2KVB.js";
231
231
  import "./chunk-KFZY2OFH.js";
232
232
  import "./chunk-B32WYRXX.js";
233
233
  import "./chunk-QW27DOHG.js";
234
- import "./chunk-HQCUUVG2.js";
234
+ import "./chunk-RMV2JYOT.js";
235
235
  import "./chunk-V7IZKJI3.js";
236
236
  import "./chunk-XW2T4QAW.js";
237
- import "./chunk-F23U5YE7.js";
238
- import "./chunk-42NQ7AVG.js";
239
- import "./chunk-TMSXWOBZ.js";
237
+ import "./chunk-2KB7TTAP.js";
238
+ import "./chunk-HCAGBFE7.js";
239
+ import "./chunk-SI36RWC3.js";
240
240
  import "./chunk-TB6CBZ5V.js";
241
- import "./chunk-J64TK33U.js";
242
- import "./chunk-RSUYKGGZ.js";
243
- import "./chunk-7RXCMVFQ.js";
241
+ import "./chunk-3JKYQBLC.js";
242
+ import "./chunk-7RKSJVOQ.js";
243
+ import "./chunk-NJAIR57U.js";
244
244
  import "./chunk-ZSEFGDCI.js";
245
245
  import "./chunk-P6P35FY4.js";
246
246
  import "./chunk-3HSRWJTC.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.69.96",
3
+ "version": "9.69.97",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3327,7 +3327,7 @@
3327
3327
  "core"
3328
3328
  ],
3329
3329
  "peerDependencies": {
3330
- "@remnic/coding-graph": "^9.69.96"
3330
+ "@remnic/coding-graph": "^9.69.97"
3331
3331
  },
3332
3332
  "peerDependenciesMeta": {
3333
3333
  "@remnic/coding-graph": {
@@ -0,0 +1,217 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { EngramAccessHttpServer } from "./access-http.js";
8
+ import { EngramAccessService, type EngramAccessRecallRequest, type EngramAccessRecallResponse } from "./access-service.js";
9
+ import { parseConfig } from "./config.js";
10
+ import { Orchestrator } from "./orchestrator.js";
11
+ import type { PluginConfig } from "./types.js";
12
+
13
+ const FLAT_ADAPTER_IDS = ["claude-code", "codex", "replit", "grok", "opencode", "hermes"];
14
+
15
+ const NAMESPACED_ADAPTERS = [
16
+ { clientId: "claude-code", principal: "claude-code" },
17
+ { clientId: "codex", principal: "codex" },
18
+ { clientId: "replit", principal: "replit-agent" },
19
+ { clientId: "grok", principal: "grok" },
20
+ { clientId: "opencode", principal: "opencode" },
21
+ { clientId: "hermes", principal: "hermes-agent" },
22
+ ];
23
+
24
+ interface TestDaemon {
25
+ service: EngramAccessService;
26
+ port: number;
27
+ close(): Promise<void>;
28
+ }
29
+
30
+ function makeConfig(memoryDir: string, overrides: Partial<PluginConfig> = {}) {
31
+ return parseConfig({
32
+ openaiApiKey: "sk-test",
33
+ memoryDir,
34
+ workspaceDir: path.join(memoryDir, "workspace"),
35
+ qmdEnabled: false,
36
+ embeddingFallbackEnabled: false,
37
+ recallPlannerEnabled: false,
38
+ sharedContextEnabled: false,
39
+ initGateTimeoutMs: 1000,
40
+ ...overrides,
41
+ });
42
+ }
43
+
44
+ async function startServer(overrides: Partial<PluginConfig> = {}): Promise<TestDaemon> {
45
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-3166-"));
46
+ const orchestrator = new Orchestrator(makeConfig(memoryDir, overrides));
47
+ const service = new EngramAccessService(orchestrator);
48
+ const server = new EngramAccessHttpServer({
49
+ service,
50
+ host: "127.0.0.1",
51
+ port: 0,
52
+ authToken: "secret-token",
53
+ adminConsoleEnabled: false,
54
+ });
55
+ const { port } = await server.start();
56
+ return {
57
+ service,
58
+ port,
59
+ async close() {
60
+ await server.stop();
61
+ await rm(memoryDir, { recursive: true, force: true });
62
+ },
63
+ };
64
+ }
65
+
66
+ interface RecallOutcome {
67
+ status: number;
68
+ isError: boolean;
69
+ text: string;
70
+ payload?: EngramAccessRecallResponse;
71
+ }
72
+
73
+ interface McpToolReply {
74
+ result?: { content?: Array<{ text?: string }>; isError?: boolean };
75
+ }
76
+
77
+ async function mcpRecall(
78
+ port: number,
79
+ args: EngramAccessRecallRequest,
80
+ headers: Record<string, string> = {},
81
+ ): Promise<RecallOutcome> {
82
+ const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
83
+ method: "POST",
84
+ headers: {
85
+ authorization: "Bearer secret-token",
86
+ "content-type": "application/json",
87
+ ...headers,
88
+ },
89
+ body: JSON.stringify({
90
+ jsonrpc: "2.0",
91
+ id: 1,
92
+ method: "tools/call",
93
+ params: { name: "engram.recall", arguments: args },
94
+ }),
95
+ });
96
+ const body: McpToolReply = JSON.parse(await res.text());
97
+ const text = body.result?.content?.[0]?.text ?? "";
98
+ const outcome: RecallOutcome = {
99
+ status: res.status,
100
+ isError: body.result?.isError === true,
101
+ text,
102
+ };
103
+ if (!outcome.isError) outcome.payload = JSON.parse(text);
104
+ return outcome;
105
+ }
106
+
107
+ test("flat daemon: all six built-in adapters resolve the configured default namespace over MCP recall", async () => {
108
+ const daemon = await startServer({
109
+ namespacesEnabled: false,
110
+ defaultNamespace: "flat-tenant",
111
+ });
112
+ try {
113
+ await daemon.service.memoryStore({ content: "the launch codeword is PERIHELION" });
114
+ for (const clientId of FLAT_ADAPTER_IDS) {
115
+ const outcome = await mcpRecall(
116
+ daemon.port,
117
+ { query: "launch codeword" },
118
+ { "x-engram-client-id": clientId },
119
+ );
120
+ assert.equal(outcome.status, 200, `${clientId}: recall must reach the op`);
121
+ assert.equal(outcome.isError, false, `${clientId}: ${outcome.text}`);
122
+ assert.equal(outcome.payload?.namespace, "flat-tenant", `${clientId}: default namespace`);
123
+ assert.ok(
124
+ outcome.payload?.context.includes("PERIHELION"),
125
+ `${clientId}: default-namespace memory must be recalled`,
126
+ );
127
+ }
128
+ } finally {
129
+ await daemon.close();
130
+ }
131
+ });
132
+
133
+ test("flat daemon: explicit X-Engram-Namespace is enforced, not silently dropped", async () => {
134
+ const daemon = await startServer({
135
+ namespacesEnabled: false,
136
+ defaultNamespace: "flat-tenant",
137
+ });
138
+ try {
139
+ await daemon.service.memoryStore({ content: "the launch codeword is PERIHELION" });
140
+
141
+ const validDefault = await mcpRecall(daemon.port, { query: "launch codeword" }, {
142
+ "x-engram-client-id": "claude-code",
143
+ "x-engram-namespace": "flat-tenant",
144
+ });
145
+ assert.equal(validDefault.isError, false, validDefault.text);
146
+ assert.equal(validDefault.payload?.namespace, "flat-tenant");
147
+ assert.ok(validDefault.payload?.context.includes("PERIHELION"));
148
+
149
+ for (const outcome of [
150
+ await mcpRecall(daemon.port, { query: "launch codeword" }, {
151
+ "x-engram-client-id": "claude-code",
152
+ "x-engram-namespace": "off-books",
153
+ }),
154
+ await mcpRecall(daemon.port, { query: "launch codeword", namespace: "off-books" }, {
155
+ "x-engram-client-id": "claude-code",
156
+ }),
157
+ ]) {
158
+ assert.equal(outcome.isError, true, "non-default namespace must be rejected");
159
+ assert.match(outcome.text, /unsupported namespace: off-books/);
160
+ }
161
+ } finally {
162
+ await daemon.close();
163
+ }
164
+ });
165
+
166
+ test("namespaced daemon: adapters keep their self-namespaces over MCP recall", async () => {
167
+ const daemon = await startServer({
168
+ namespacesEnabled: true,
169
+ defaultNamespace: "main",
170
+ namespacePolicies: NAMESPACED_ADAPTERS.map(({ clientId, principal }) => ({
171
+ name: clientId,
172
+ readPrincipals: [principal],
173
+ writePrincipals: [principal],
174
+ })),
175
+ });
176
+ try {
177
+ await daemon.service.memoryStore({
178
+ content: "main vault code is QUASAR-main",
179
+ namespace: "main",
180
+ authenticatedPrincipal: "operator",
181
+ });
182
+ for (const { clientId, principal } of NAMESPACED_ADAPTERS) {
183
+ await daemon.service.memoryStore({
184
+ content: `the ${clientId} vault code is NOVA-${clientId}`,
185
+ namespace: clientId,
186
+ authenticatedPrincipal: principal,
187
+ });
188
+ }
189
+
190
+ for (const { clientId } of NAMESPACED_ADAPTERS) {
191
+ const outcome = await mcpRecall(daemon.port, { query: "vault code" }, {
192
+ "x-engram-client-id": clientId,
193
+ });
194
+ assert.equal(outcome.status, 200, `${clientId}: recall must reach the op`);
195
+ assert.equal(outcome.isError, false, `${clientId}: ${outcome.text}`);
196
+ assert.equal(outcome.payload?.namespace, clientId, `${clientId}: self-namespace kept`);
197
+ assert.ok(
198
+ outcome.payload?.context.includes(`NOVA-${clientId}`),
199
+ `${clientId}: own namespace memory must be recalled`,
200
+ );
201
+ assert.ok(
202
+ !outcome.payload?.context.includes("QUASAR-main"),
203
+ `${clientId}: default namespace must stay isolated`,
204
+ );
205
+ }
206
+
207
+ const crossTenant = await mcpRecall(daemon.port, { query: "vault code" }, {
208
+ "x-engram-client-id": "claude-code",
209
+ });
210
+ assert.ok(
211
+ !crossTenant.payload?.context.includes("NOVA-codex"),
212
+ "one adapter's namespace must not leak into another's recall",
213
+ );
214
+ } finally {
215
+ await daemon.close();
216
+ }
217
+ });
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { fileURLToPath, URL } from "node:url";
8
8
  import { gunzipSync } from "node:zlib";
9
9
  import { log } from "./logger.js";
10
+ import { resolveNamespaceCapabilities } from "./capabilities.js";
10
11
  import { WriteRateLimiter, type WriteRateLimitReservation } from "./write-rate-limiter.js";
11
12
  import { abortError, isAbortError } from "./abort-error.js";
12
13
  import { EngramAccessForbiddenError } from "./access-errors.js";
@@ -716,6 +717,13 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
716
717
  principal = adapterIdentity.principal;
717
718
  }
718
719
  namespace = adapterIdentity.namespace;
720
+ const config = this.service.configRef;
721
+ if (
722
+ adapterIdentity.namespaceExplicit === false &&
723
+ !(config && resolveNamespaceCapabilities(config).namespaces)
724
+ ) {
725
+ namespace = undefined;
726
+ }
719
727
  sessionKey = adapterIdentity.sessionKey;
720
728
  }
721
729
 
@@ -41,12 +41,12 @@ export class ClaudeCodeAdapter implements EngramAdapter {
41
41
  // MCP session ID (standard MCP header, server-assigned)
42
42
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
43
43
 
44
- // Namespace: explicit header > default
45
- const namespace = headerValue(context.headers, "x-engram-namespace")
46
- || "claude-code";
44
+ // Namespace: explicit header > adapter default
45
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
47
46
 
48
47
  return {
49
- namespace,
48
+ namespace: namespaceHeader ?? "claude-code",
49
+ namespaceExplicit: namespaceHeader !== undefined,
50
50
  principal: "claude-code",
51
51
  sessionKey: mcpSessionId ?? context.sessionKey,
52
52
  adapterId: this.id,
@@ -43,11 +43,11 @@ export class CodexAdapter implements EngramAdapter {
43
43
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
44
44
 
45
45
  // Namespace: explicit header > default
46
- const namespace = headerValue(context.headers, "x-engram-namespace")
47
- || "codex";
46
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
48
47
 
49
48
  return {
50
- namespace,
49
+ namespace: namespaceHeader ?? "codex",
50
+ namespaceExplicit: namespaceHeader !== undefined,
51
51
  principal: "codex",
52
52
  sessionKey: mcpSessionId ?? context.sessionKey,
53
53
  adapterId: this.id,
@@ -49,6 +49,7 @@ test("GrokAdapter resolves identity with adapter-owned principal and header name
49
49
  })),
50
50
  {
51
51
  namespace: "my-project",
52
+ namespaceExplicit: true,
52
53
  principal: "grok",
53
54
  sessionKey: "sess-grok-1",
54
55
  adapterId: "grok",
@@ -91,6 +92,7 @@ test("OpenCodeAdapter resolves identity with adapter-owned principal", () => {
91
92
  }));
92
93
  assert.deepEqual(resolved, {
93
94
  namespace: "opencode",
95
+ namespaceExplicit: false,
94
96
  principal: "opencode",
95
97
  sessionKey: "sess-oc-1",
96
98
  adapterId: "opencode",
@@ -40,11 +40,11 @@ export class GrokAdapter implements EngramAdapter {
40
40
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
41
41
 
42
42
  // Namespace: explicit header > default
43
- const namespace = headerValue(context.headers, "x-engram-namespace")
44
- || "grok";
43
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
45
44
 
46
45
  return {
47
- namespace,
46
+ namespace: namespaceHeader ?? "grok",
47
+ namespaceExplicit: namespaceHeader !== undefined,
48
48
  principal: "grok",
49
49
  sessionKey: mcpSessionId ?? context.sessionKey,
50
50
  adapterId: this.id,
@@ -47,11 +47,11 @@ export class HermesAdapter implements EngramAdapter {
47
47
  resolveIdentity(context: AdapterContext): ResolvedIdentity {
48
48
  const sessionId = headerValue(context.headers, "x-hermes-session-id");
49
49
 
50
- const namespace = headerValue(context.headers, "x-engram-namespace")
51
- || "hermes";
50
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
52
51
 
53
52
  return {
54
- namespace,
53
+ namespace: namespaceHeader ?? "hermes",
54
+ namespaceExplicit: namespaceHeader !== undefined,
55
55
  principal: "hermes-agent",
56
56
  sessionKey: sessionId ?? context.sessionKey,
57
57
  adapterId: this.id,
@@ -39,11 +39,11 @@ export class OpenCodeAdapter implements EngramAdapter {
39
39
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
40
40
 
41
41
  // Namespace: explicit header > default
42
- const namespace = headerValue(context.headers, "x-engram-namespace")
43
- || "opencode";
42
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
44
43
 
45
44
  return {
46
- namespace,
45
+ namespace: namespaceHeader ?? "opencode",
46
+ namespaceExplicit: namespaceHeader !== undefined,
47
47
  principal: "opencode",
48
48
  sessionKey: mcpSessionId ?? context.sessionKey,
49
49
  adapterId: this.id,
@@ -40,11 +40,11 @@ export class ReplitAdapter implements EngramAdapter {
40
40
  resolveIdentity(context: AdapterContext): ResolvedIdentity {
41
41
  const mcpSessionId = headerValue(context.headers, "mcp-session-id");
42
42
 
43
- const namespace = headerValue(context.headers, "x-engram-namespace")
44
- || "replit";
43
+ const namespaceHeader = headerValue(context.headers, "x-engram-namespace");
45
44
 
46
45
  return {
47
- namespace,
46
+ namespace: namespaceHeader ?? "replit",
47
+ namespaceExplicit: namespaceHeader !== undefined,
48
48
  principal: "replit-agent",
49
49
  sessionKey: mcpSessionId ?? context.sessionKey,
50
50
  adapterId: this.id,
@@ -22,6 +22,8 @@ export interface AdapterContext {
22
22
  export interface ResolvedIdentity {
23
23
  /** Engram namespace (scopes memory access) */
24
24
  namespace: string;
25
+ /** False for an inferred namespace; omitted values retain explicit-namespace validation. */
26
+ namespaceExplicit?: boolean;
25
27
  /** Engram principal (authorization subject) */
26
28
  principal: string;
27
29
  /** Session key for continuity tracking */
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/adapters/grok.ts","../src/adapters/opencode.ts","../src/adapters/registry.ts"],"sourcesContent":["import { headerValue, type AdapterContext, type EngramAdapter, type ResolvedIdentity } from \"./types.js\";\n\n/**\n * Grok adapter.\n *\n * Detection: Grok clients identify themselves with a clientInfo name\n * containing \"grok\" in the MCP initialize handshake, and send\n * User-Agent: grok/<version> on HTTP requests. The exact clientInfo\n * name is not yet stable across Grok surfaces (desktop app, CLI), so\n * detection uses a contains-match for forward compatibility.\n *\n * User-configured identification is also supported via headers:\n * \"headers\": { \"X-Engram-Client-Id\": \"grok\" }\n *\n * Principal overrides are intentionally handled only by the HTTP server's\n * trustPrincipalHeader gate. Adapters must not independently trust\n * X-Engram-Principal.\n */\nexport class GrokAdapter implements EngramAdapter {\n readonly id = \"grok\";\n\n matches(context: AdapterContext): boolean {\n // Primary: MCP clientInfo from initialize handshake\n const clientName = context.clientInfo?.name?.toLowerCase() ?? \"\";\n if (clientName.includes(\"grok\")) return true;\n\n // Fallback: User-Agent header (\"grok/<version>\")\n const ua = headerValue(context.headers, \"user-agent\");\n if (ua && ua.toLowerCase().startsWith(\"grok/\")) return true;\n\n // Fallback: user-configured client identifier header\n const clientId = headerValue(context.headers, \"x-engram-client-id\");\n if (clientId?.toLowerCase() === \"grok\") return true;\n\n return false;\n }\n\n resolveIdentity(context: AdapterContext): ResolvedIdentity {\n // MCP session ID (standard MCP header, server-assigned)\n const mcpSessionId = headerValue(context.headers, \"mcp-session-id\");\n\n // Namespace: explicit header > default\n const namespace = headerValue(context.headers, \"x-engram-namespace\")\n || \"grok\";\n\n return {\n namespace,\n principal: \"grok\",\n sessionKey: mcpSessionId ?? context.sessionKey,\n adapterId: this.id,\n };\n }\n}\n","import { headerValue, type AdapterContext, type EngramAdapter, type ResolvedIdentity } from \"./types.js\";\n\n/**\n * OpenCode adapter.\n *\n * Detection: OpenCode identifies itself as \"opencode\" in the MCP\n * clientInfo of its initialize handshake, and sends\n * User-Agent: opencode/<version> on HTTP requests.\n *\n * User-configured identification is also supported via headers in the\n * OpenCode MCP server config:\n * \"headers\": { \"X-Engram-Client-Id\": \"opencode\" }\n *\n * Principal overrides are intentionally handled only by the HTTP server's\n * trustPrincipalHeader gate. Adapters must not independently trust\n * X-Engram-Principal.\n */\nexport class OpenCodeAdapter implements EngramAdapter {\n readonly id = \"opencode\";\n\n matches(context: AdapterContext): boolean {\n // Primary: MCP clientInfo from initialize handshake\n const clientName = context.clientInfo?.name?.toLowerCase() ?? \"\";\n if (clientName.includes(\"opencode\")) return true;\n\n // Fallback: User-Agent header (\"opencode/<version>\")\n const ua = headerValue(context.headers, \"user-agent\");\n if (ua && ua.toLowerCase().startsWith(\"opencode/\")) return true;\n\n // Fallback: user-configured client identifier header\n const clientId = headerValue(context.headers, \"x-engram-client-id\");\n if (clientId?.toLowerCase() === \"opencode\") return true;\n\n return false;\n }\n\n resolveIdentity(context: AdapterContext): ResolvedIdentity {\n // MCP session ID (standard MCP header, server-assigned)\n const mcpSessionId = headerValue(context.headers, \"mcp-session-id\");\n\n // Namespace: explicit header > default\n const namespace = headerValue(context.headers, \"x-engram-namespace\")\n || \"opencode\";\n\n return {\n namespace,\n principal: \"opencode\",\n sessionKey: mcpSessionId ?? context.sessionKey,\n adapterId: this.id,\n };\n }\n}\n","import type { AdapterContext, EngramAdapter, ResolvedIdentity } from \"./types.js\";\nimport { ClaudeCodeAdapter } from \"./claude-code.js\";\nimport { CodexAdapter } from \"./codex.js\";\nimport { GrokAdapter } from \"./grok.js\";\nimport { OpenCodeAdapter } from \"./opencode.js\";\nimport { ReplitAdapter } from \"./replit.js\";\nimport { HermesAdapter } from \"./hermes.js\";\n\n/**\n * Adapter registry. Attempts to identify which external system is\n * connecting by checking each registered adapter in order. Falls back\n * to explicit namespace/principal from the request args.\n */\nexport class AdapterRegistry {\n private readonly adapters: EngramAdapter[];\n\n constructor(adapters?: EngramAdapter[]) {\n this.adapters = adapters ?? [\n new HermesAdapter(),\n new ReplitAdapter(),\n new CodexAdapter(),\n new ClaudeCodeAdapter(),\n new GrokAdapter(),\n new OpenCodeAdapter(),\n ];\n }\n\n /**\n * Try each adapter in order. Return the first match, or null if\n * no adapter recognizes the request context.\n */\n resolve(context: AdapterContext): ResolvedIdentity | null {\n for (const adapter of this.adapters) {\n if (adapter.matches(context)) {\n return adapter.resolveIdentity(context);\n }\n }\n const namespace = context.namespace?.trim();\n const principal = context.principal?.trim();\n if (namespace && principal) {\n return {\n namespace,\n principal,\n sessionKey: context.sessionKey,\n adapterId: \"explicit\",\n };\n }\n return null;\n }\n\n /** List registered adapter IDs */\n list(): string[] {\n return this.adapters.map((a) => a.id);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBO,IAAM,cAAN,MAA2C;AAAA,EACvC,KAAK;AAAA,EAEd,QAAQ,SAAkC;AAExC,UAAM,aAAa,QAAQ,YAAY,MAAM,YAAY,KAAK;AAC9D,QAAI,WAAW,SAAS,MAAM,EAAG,QAAO;AAGxC,UAAM,KAAK,YAAY,QAAQ,SAAS,YAAY;AACpD,QAAI,MAAM,GAAG,YAAY,EAAE,WAAW,OAAO,EAAG,QAAO;AAGvD,UAAM,WAAW,YAAY,QAAQ,SAAS,oBAAoB;AAClE,QAAI,UAAU,YAAY,MAAM,OAAQ,QAAO;AAE/C,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgB,SAA2C;AAEzD,UAAM,eAAe,YAAY,QAAQ,SAAS,gBAAgB;AAGlE,UAAM,YAAY,YAAY,QAAQ,SAAS,oBAAoB,KAC9D;AAEL,WAAO;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX,YAAY,gBAAgB,QAAQ;AAAA,MACpC,WAAW,KAAK;AAAA,IAClB;AAAA,EACF;AACF;;;ACnCO,IAAM,kBAAN,MAA+C;AAAA,EAC3C,KAAK;AAAA,EAEd,QAAQ,SAAkC;AAExC,UAAM,aAAa,QAAQ,YAAY,MAAM,YAAY,KAAK;AAC9D,QAAI,WAAW,SAAS,UAAU,EAAG,QAAO;AAG5C,UAAM,KAAK,YAAY,QAAQ,SAAS,YAAY;AACpD,QAAI,MAAM,GAAG,YAAY,EAAE,WAAW,WAAW,EAAG,QAAO;AAG3D,UAAM,WAAW,YAAY,QAAQ,SAAS,oBAAoB;AAClE,QAAI,UAAU,YAAY,MAAM,WAAY,QAAO;AAEnD,WAAO;AAAA,EACT;AAAA,EAEA,gBAAgB,SAA2C;AAEzD,UAAM,eAAe,YAAY,QAAQ,SAAS,gBAAgB;AAGlE,UAAM,YAAY,YAAY,QAAQ,SAAS,oBAAoB,KAC9D;AAEL,WAAO;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX,YAAY,gBAAgB,QAAQ;AAAA,MACpC,WAAW,KAAK;AAAA,IAClB;AAAA,EACF;AACF;;;ACtCO,IAAM,kBAAN,MAAsB;AAAA,EACV;AAAA,EAEjB,YAAY,UAA4B;AACtC,SAAK,WAAW,YAAY;AAAA,MAC1B,IAAI,cAAc;AAAA,MAClB,IAAI,cAAc;AAAA,MAClB,IAAI,aAAa;AAAA,MACjB,IAAI,kBAAkB;AAAA,MACtB,IAAI,YAAY;AAAA,MAChB,IAAI,gBAAgB;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQ,SAAkD;AACxD,eAAW,WAAW,KAAK,UAAU;AACnC,UAAI,QAAQ,QAAQ,OAAO,GAAG;AAC5B,eAAO,QAAQ,gBAAgB,OAAO;AAAA,MACxC;AAAA,IACF;AACA,UAAM,YAAY,QAAQ,WAAW,KAAK;AAC1C,UAAM,YAAY,QAAQ,WAAW,KAAK;AAC1C,QAAI,aAAa,WAAW;AAC1B,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,YAAY,QAAQ;AAAA,QACpB,WAAW;AAAA,MACb;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAiB;AACf,WAAO,KAAK,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,EACtC;AACF;","names":[]}